Tenlyr Developer Docs

The complete reference for building multi-tenant SaaS with Tenlyr. Provision, monitor, meter, isolate, and bill tenants from a single SDK call - in under 200 ms.

Quickstart
Provision your first tenant in under 30 minutes with one SDK call.
📡
API Reference
Full REST API - all endpoints, parameters, and response shapes.
📦
SDK Reference
TypeScript, Python, and Go SDKs with typed clients, helpers, and full error types.
🔔
Webhooks
Real-time events for tenant lifecycle, billing, and health changes.
💡
Why Tenlyr
The months of infrastructure work Tenlyr eliminates for every SaaS team.
🏗️
Architecture
How Tenlyr works internally - from SDK call to worker pipeline.

What Tenlyr does

Tenlyr provides the infrastructure layer every B2B SaaS product needs but nobody wants to build from scratch: tenant provisioning, per-tenant usage metering, health monitoring, data isolation, and billing sync - all in a single @tenlyr/sdk call.

Authentication

All API requests require a Bearer token in the Authorization header. Tenlyr uses two key types:

Key typeFormatScope
Admin keytenlyr_live_tf_…Full platform access - tenants, health, alerts, dashboard
Tenant keytenlyr_live_…Scoped to a single tenant - usage, billing, isolation
Never expose admin keys in client-side code. Set them as server-side environment variables only. Tenant keys can be used in server-side request handlers scoped to that tenant.

Base URL

base url
https://api.tenlyr.app

Response format

All responses are JSON. Successful responses return the resource directly. Error responses return:

json
{
  "message": "Tenant not found",
  "error": "Not Found",
  "statusCode": 404
}

Why Tenlyr Exists

Every B2B SaaS team rebuilds the same infrastructure. Tenlyr exists to make that problem disappear - permanently.

4–6
months average engineering time
to build tenant infrastructure
1
SDK call to replace all of it
<200ms
to provision a fully isolated,
metered, billable tenant

The problem every SaaS team faces

The moment you decide to build B2B SaaS, your roadmap quietly gains five invisible infrastructure projects. None of them are your product. All of them are required before you can ship to a second customer.

Tenant provisioning
You need a database schema per customer, row-level security policies, an API key system with bcrypt hashing, a key-rotation flow, and a provisioning queue that handles failures gracefully. Most teams spend 3–4 weeks on this alone - and still get key storage wrong the first time.
Tenlyr solves this tenlyr.tenants.create() - schema, RLS, hashed key, in under 200ms.
~3 weeks
Usage metering
Tracking API calls, storage, and seats per tenant sounds simple. In practice it means an event ingestion pipeline, per-tenant counters, monthly rollups, plan-limit enforcement, and a UI that shows customers how much they've consumed. Teams that skip this can't charge based on usage - ever.
Tenlyr solves this tenlyr.usage.track() - one line per event, rollups computed automatically.
~4 weeks
Billing sync
Connecting usage to Stripe requires webhook handlers, subscription lifecycle management, proration logic, dunning flows, and a billing portal for self-serve plan changes. Most teams spend months on this and still end up with billing bugs that silently lose revenue.
Tenlyr solves this tenlyr.billing.portalUrl() - Stripe synced, MRR tracked, portal ready.
~5 weeks
Isolation management
Enterprise customers want dedicated infrastructure. Building shared → dedicated schema → dedicated database migrations with zero downtime is one of the hardest SaaS engineering problems. Teams that haven't built it can't close $10K+ ACV deals - the security question kills the sale.
Tenlyr solves this tenlyr.isolation.migrate() - zero-downtime, async, under 5 minutes.
~6 weeks
Tenant health monitoring
You need to know when a tenant is degraded before they file a support ticket. That means per-tenant error rates, SLA risk flags, noisy-neighbour detection, automatic alerting, and a rate-limiting system you can trigger in one call. Without this, incidents surface as churn, not bugs.
Tenlyr solves this tenlyr.health.overview() - error rates, SLA risk, throttling, all automated.
~3 weeks
Total engineering time without Tenlyr
~21 weeks vs 1 afternoon with Tenlyr

What you build instead

When you're not building infrastructure, you're building product. The teams using Tenlyr spend those recovered months on the features that actually differentiate their SaaS - the things that make customers choose them over the competition.

🚀
Ship faster
Go from idea to first paying multi-tenant customer in days, not quarters. Your first customer is provisioned before your infrastructure sprint would even be planned.
📈
Scale without rewrites
Tenlyr's isolation tiers grow with your customers. Start on shared schema, migrate enterprise customers to dedicated databases when the deal requires it - no rewrite, no downtime.
🔒
Close enterprise deals
Dedicated database isolation and SOC 2 evidence generation turn the security questionnaire from a blocker into a checkbox. Tenlyr gives you the answer to "how is our data isolated?"
💸
Charge what you're worth
Usage-based billing requires metering. With Tenlyr tracking every API call, storage byte, and seat, you can launch usage-based or seat-based plans without any additional engineering.

Who Tenlyr is for

If you're…Tenlyr gives you…
An early-stage founder building your first SaaSThe multi-tenant foundation in a weekend, so your first sprint is product, not plumbing
A growing team approaching your first enterprise customerDedicated isolation and audit-ready health records - close the deal this quarter
A platform team tired of maintaining internal toolingA managed, versioned, API-first replacement for your homegrown tenant registry
A solo engineer who wants to build B2B SaaS on the sideThe entire infrastructure layer as a single npm package - so you can actually finish it
Ready to start? The Quickstart gets you from zero to a provisioned tenant in under 30 minutes. The Starter plan is free - no credit card required.

Architecture

How a single SDK call flows through Tenlyr's infrastructure - from your application to provisioning, metering, health monitoring, and billing sync.

Your SaaS App
Node.js / Python / Go
tenlyr.tenants.create()
@tenlyr/sdk
Auth · Retry · Timeout · User-Agent
HTTPS · Bearer token · tenlyr_live_…
Tenlyr API
api.tenlyr.app · Auth · Rate limit · Routing
async dispatch
Worker Queue
Redis Streams · Kafka · SQS
provisioning metering health billing
Dispatches to workers
Provisioning Worker
Schema · RLS · API key · <200ms
live
Metering Pipeline
Events · Rollups · Plan limits
streaming
Health Worker
Error rate · SLA risk · Alerts
60s cycle
Billing Sync
Stripe · MRR · Usage cost
5min cycle
Results persisted
Postgres · Redis
Tenant data · Health records · Usage events · Billing
{ tenant, apiKey } returned in <200ms

Layer breakdown

SDK layer

The @tenlyr/sdk handles authentication (injecting your tenlyr_live_… key as a Bearer token), sets the User-Agent header for server-side tracing, enforces per-request timeouts, and retries on 5xx errors with exponential back-off. No request ever reaches the API without a valid key.

API gateway

The Tenlyr API at api.tenlyr.app authenticates every request, enforces rate limits, validates the request body, and routes to the correct handler. All 4xx errors are returned immediately with a typed error body - the SDK surfaces them as AuthenticationError, RateLimitError, NotFoundError, or ValidationError.

Worker pipeline

WorkerTriggerResponsibility
Provisioning WorkerOn POST /tenantsCreates the Postgres schema, applies Row-Level Security policies, hashes the API key, and returns the plaintext key once - completes in under 200ms
Metering PipelineOn POST /usage/trackWrites the usage event, increments rolling counters, enforces plan limits, and triggers a plan-exceeded error at 100% of limit
Health WorkerEvery 60s (background)Scans the last 5 minutes of events per tenant, calculates error rate and request rate, sets slaRisk, and auto-creates health_critical alerts via AlertWorker every 5 minutes
Billing SyncEvery 5 minutesReconciles usage counters against Stripe subscriptions, calculates MRR and usage cost in cents, and keeps billing records in sync

Data layer

Postgres is the system of record for all tenant data, usage events, health records, isolation migrations, and billing. Each isolation tier stores data differently: shared_schema uses RLS on shared tables; dedicated_schema puts each tenant in its own Postgres schema; dedicated_database provisions a separate Postgres instance.

Redis caches API key lookups (60s TTL) and rate-limit counters, keeping authentication latency below 2ms per request.

All workers are stateless and horizontally scalable. The provisioning worker and metering pipeline run synchronously in the request path; the health worker and billing sync run as background cron jobs and never block your API calls.

Why this architecture

Each layer and worker is designed around a specific latency and reliability contract. These aren't arbitrary choices - they map directly to what developers and tenants actually need.

synchronous
Tenant provisioning must be <200ms

Your signup handler awaits this call. If it takes 3 seconds, you've broken your onboarding flow. Provisioning runs in-process with the API request - schema creation, RLS, and key hashing happen in a single Postgres transaction with no queue hop. The 200ms SLA is a hard contract, not a guideline.

Tradeoff Synchronous means a slow Postgres instance can block signups. We mitigate this with connection pooling and a read-replica for non-mutating lookups.
streaming
Usage metering must be a streaming pipeline

Usage events can arrive thousands of times per second per tenant at peak. Writing each event synchronously to Postgres would create lock contention and degrade API latency for everyone. Instead, events are written to the queue immediately (<1ms) and consumed by the metering pipeline, which batches writes and increments counters atomically in Redis before flushing to Postgres.

Tradeoff Counters can be up to a few seconds stale. Plan-limit enforcement uses the Redis counter, which is eventually consistent with the Postgres record.
async / cron
Health monitoring must run asynchronously

Computing error rates requires scanning the last 5 minutes of usage events across all tenants - an expensive aggregation that should never block a live request. The Health Worker runs every 60 seconds as a background cron job. This means health status can lag by up to a minute, which is acceptable: you don't need millisecond health updates, you need reliable trend detection.

Tradeoff A spike that resolves within 60 seconds may not produce a warning. For sub-minute alerting, use your own APM and Tenlyr webhooks together.
periodic
Billing sync must run periodically

Stripe is an external system with its own rate limits and consistency model. Reconciling MRR and usage cost on every API call would add latency and create coupling to Stripe's availability. Billing Sync runs every 5 minutes, reading the Postgres usage counters and writing back to Stripe. This keeps your billing data accurate without making Stripe a dependency in your hot path.

Tradeoff Billing records can be up to 5 minutes stale. For real-time billing data, query Stripe directly via tenlyr.billing.portalUrl().

Scaling model

Tenlyr is designed to scale horizontally at every layer. Here's how each component behaves under load.

API instances
Horizontal
Stateless - any instance can handle any request. Load balanced with sticky sessions only for WebSocket connections (not used in v1). Auto-scales on CPU and request queue depth.
Worker Queue
Partitioned
Redis Streams partitioned by tenantId. Each partition is consumed by exactly one worker instance, preventing duplicate processing. Consumer group rebalancing handles worker failures automatically.
Provisioning Worker
Concurrent
Runs in the API request path but uses a semaphore to cap concurrent schema creations at 20. Above that, requests queue - provisioning is rare enough that this limit is never reached in practice.
Metering Pipeline
Horizontal
Multiple consumer instances read from the queue in parallel. Counters are incremented with Redis INCRBY - atomic, conflict-free, and sub-millisecond. Postgres writes are batched every 10 seconds to reduce I/O.
Redis
Shared state
Single Redis cluster provides shared rate-limit counters, API key lookup cache (60s TTL), and the metering pipeline queue. Redis Cluster mode shards by key for high availability. All API instances read from the same Redis, ensuring consistent rate-limit enforcement across the fleet.
Postgres
Pooled
PgBouncer connection pooler sits in front of Postgres in transaction mode. Each API instance uses a max of 10 connections; the pooler multiplexes these across hundreds of concurrent requests. Read-heavy endpoints (tenant list, health overview) are routed to a read replica.

Quickstart

From zero to a provisioned tenant in under 30 minutes.

1
Get your admin key

Sign up at tenlyr.app and copy your admin API key from the dashboard. It looks like tenlyr_live_tf_4a8b2c9d…

The Starter plan is free with up to 5 tenants - no credit card required.
2
Install the SDK
bash
npm install @tenlyr/sdk
3
Provision your first tenant

Wire this into your signup handler. The call returns a tenant object and a one-time apiKey - store the key securely.

typescript
import { Tenlyr } from '@tenlyr/sdk';

const tos = new Tenlyr({
  apiKey: process.env.TENLYR_ADMIN_KEY,
});

// In your signup handler:
const { tenant, apiKey } = await tenlyr.tenants.create({
  name: 'Acme Corp',
  ownerEmail: '[email protected]',
  plan: 'growth',
});

// Store apiKey immediately - it cannot be retrieved again
await db.tenants.save({ id: tenant.id, apiKey });
4
Track usage events

Call track() on your key API actions. Use the tenant's own API key (not admin).

typescript
// In your API middleware or handler:
await tenlyr.usage.track({
  tenantKey: req.user.tenantApiKey,
  metric: 'api_call',
  value: 1,
  endpoint: req.path,
});
5
Open your dashboard

Visit your Tenlyr dashboard to see the tenant, its health, usage, and billing in real time. The dashboard polls every 30 seconds automatically.

Install SDK

Tenlyr SDKs are available for JavaScript/TypeScript, Python, and Go.

JavaScript / TypeScript

bash
npm install @tenlyr/sdk
# or
yarn add @tenlyr/sdk
# or
pnpm add @tenlyr/sdk

Requirements

Node.js ≥ 18 (uses native fetch). Works in edge runtimes (Cloudflare Workers, Vercel Edge) by injecting a custom fetchFn.

Initialize

typescript
import { Tenlyr } from '@tenlyr/sdk';

const tos = new Tenlyr({
  apiKey:     process.env.TENLYR_ADMIN_KEY, // required
  baseUrl:    'https://api.tenlyr.app',   // default
  timeout:    10_000,                         // ms, default 10 000
  retries:    3,                              // 5xx retries, default 3
  retryDelay: 200,                           // initial back-off ms
});

Python Coming soon

The Python SDK is under development and not yet published to PyPI. Sign up at tenlyr.app to be notified on release.

Go Coming soon

The Go SDK is under development and not yet published. Use the REST API directly in the meantime - all endpoints work with any HTTP client.

Direct API (cURL)

All SDKs are thin wrappers around the REST API. You can call it directly:

bash
curl https://api.tenlyr.app/api/v1/tenants/dashboard \
  -H "Authorization: Bearer tenlyr_live_tf_..." \
  -H "User-Agent: tenlyr-sdk/1.0.0"

Tenants

A tenant is the top-level unit in Tenlyr - one per customer, organization, or workspace in your product.

Tenant object

FieldTypeDescription
iduuidUnique tenant identifier
namestringDisplay name (e.g. "Acme Corp")
ownerEmailstringEmail of the account owner - must be unique
planstarter | growth | scale | enterpriseActive billing plan
statuspending | active | suspended | terminatedLifecycle state
regionstringDeployment region (e.g. us-east-1)
isolationLevelshared_schema | dedicated_schema | dedicated_databaseData isolation tier
apiKeyPrefixstringFirst 16 chars of the hashed API key - for display only
lastActiveAttimestamp | nullLast time a tenant API call was authenticated
createdAttimestampProvisioning time

Plans

PlanPriceTenantsEvents/moBest for
starter$0/moUp to 51KIndie / evaluating
growth$99/moUp to 100100KSeed-stage SaaS
scale$399/moUnlimited1MSeries A+
enterpriseCustomUnlimitedCustomRegulated industries

API key security

Tenlyr stores only a bcrypt hash of each API key - the raw key is returned exactly once at provisioning time and cannot be retrieved again. The first 16 characters (apiKeyPrefix) are stored in plaintext for display and fast prefix-based lookup.

🔑
Store the apiKey returned by tenants.create() immediately and securely (e.g. your secrets manager or encrypted DB column). If lost, you must rotate via POST /api/v1/tenants/:id/rotate-key.

Isolation Model

Tenlyr supports three levels of data isolation, from shared to fully dedicated. You can upgrade a tenant between levels at any time with zero downtime.

Isolation levels

🔀
shared_schema
All tenants share the same database and schema. Row-Level Security (RLS) enforces data boundaries. Default for new tenants. Lowest cost.
📁
dedicated_schema
Each tenant gets a separate Postgres schema within the shared database. Stronger isolation, enables per-tenant backups and migrations.
🗄️
dedicated_database
Each tenant gets its own Postgres instance. Maximum isolation for enterprise and regulated customers. Supports SOC 2 evidence generation. Closes $10K+ ACV deals immediately.

Migration behaviour

Migrations are asynchronous and non-destructive. When you call POST /api/v1/isolation/migrate, Tenlyr:

  1. Creates a new migration record with status: pending, then transitions to in_progress when the job fires
  2. Copies data to the new isolation context
  3. Updates the tenant's isolationLevel on success
  4. Sets status: completed or failed with an error message

Average migration time is under 5 minutes. Use sdk.isolation.waitForMigration(id) to poll until completion.

Isolation levels are one-directional - you can upgrade shared_schema → dedicated_schema → dedicated_database but not downgrade. Downgrade requires a support ticket.

Usage Metering

Tenlyr tracks usage in real time. One line of code per event type. Rollups are computed hourly, daily, and monthly.

Metric types

MetricValue unitUse case
api_callinteger (count)Track every API request your tenants make
storage_gbfloat (GB)Track data storage consumed
seatsinteger (count)Track active user seats per tenant
computeintegerGeneral-purpose compute unit tracking

Plan limits and enforcement

Each plan defines limits per metric. When a tenant's api_call count reaches their plan's apiCallLimit, subsequent track calls return HTTP 400 with a limit-exceeded error. The Growth plan allows 100,000 API calls/month; Scale allows 1,000,000.

Track usage events with the tenant's own API key, not your admin key. This ensures usage is attributed to the correct tenant automatically.

Usage event object

FieldTypeDescription
iduuidEvent ID
tenantIduuidOwning tenant
metricUsageMetricWhich metric was tracked
valueintegerAmount to increment by (minimum 1)
endpointstring | nullOptional: which API endpoint triggered the event
timestamptimestampEvent creation time

Tenant Lifecycle

Every tenant moves through a defined set of states. Transitions are explicit API calls - nothing happens automatically except provisioning.

State machine

pending
on create
active
after provision
suspended
on suspend
active
on reactivate
terminated
irreversible

State descriptions

StateAPI accessDataReversible
pendingBlockedPreservedYes - becomes active after internal provisioning
activeFullPreservedYes
suspendedBlocked - 401 on all requestsPreservedYes - call /reactivate
terminatedBlockedPending deletionNo
Termination is permanent. Call DELETE /api/v1/tenants/:id to permanently remove a tenant and all associated data. This action cannot be undone. Use suspend for temporary access blocks.

Provision tenants

Wire Tenlyr into your signup flow. Provisioning creates the tenant, issues an API key, and sets up schema and RLS in under 200ms.

Basic provisioning

typescript
const { tenant, apiKey } = await tenlyr.tenants.create({
  name:       'Acme Corp',         // required
  ownerEmail: '[email protected]',      // required, must be unique
  plan:       'growth',             // optional, defaults to 'starter'
  region:     'us-east-1',          // optional, defaults to 'us-east-1'
});

console.log(tenant.id);   // "51fad484-7f82-4c97-..."
console.log(apiKey);      // "tenlyr_live_abc123..." - store this immediately

Error handling

If a tenant with the same ownerEmail already exists, the API returns HTTP 409 Conflict.

typescript
import { Tenlyr, ConflictError } from '@tenlyr/sdk';

try {
  const result = await tenlyr.tenants.create({ name, ownerEmail, plan });
} catch (err) {
  if (err instanceof ConflictError) {
    // Tenant already exists - fetch existing instead
    const { data } = await tenlyr.tenants.list();
    return data.find(t => t.ownerEmail === ownerEmail);
  }
  throw err;
}

Post-provisioning checklist

  1. Store tenant.id and apiKey in your user/org record
  2. Pass apiKey to your tenant - they use it to authenticate usage calls
  3. Optionally seed billing via tenlyr.billing.get(tenantApiKey) once the tenant makes their first request
  4. Optionally send a welcome webhook to your own system

Usage billing

Track events, enforce plan limits, and surface billing data - all from the same metering API.

Track in middleware

The most reliable pattern is to track in an Express/NestJS middleware so every request is counted automatically:

typescript
// middleware/track-usage.ts
export function trackUsage(tenlyr: Tenlyr) {
  return async (req, res, next) => {
    res.on('finish', () => {
      if (req.tenantApiKey && res.statusCode < 500) {
        tenlyr.usage.track({
          tenantKey: req.tenantApiKey,
          metric: 'api_call',
          value: 1,
          endpoint: req.path,
        }).catch(console.error); // fire-and-forget
      }
    });
    next();
  };
}

Check usage before expensive operations

typescript
const metrics = awaittenlyr.usage.metrics(tenantApiKey);

if (metrics.apiCalls >= planLimit * 0.8) {
  // Warn the user at 80%
  await notifyUser('Approaching API call limit');
}

if (metrics.apiCalls >= planLimit) {
  return res.status(429).json({ error: 'Plan limit reached' });
}

Billing portal

Redirect tenants to their Stripe billing portal for self-serve plan changes and invoice history:

typescript
const { url } = awaittenlyr.billing.portalUrl(tenantApiKey);
res.redirect(url);

Isolation upgrades

Move a tenant to a higher isolation tier to close enterprise deals, meet compliance requirements, or eliminate noisy-neighbour risk.

Start a migration

typescript
// Start migration - returns immediately, runs async
const migration = awaittenlyr.isolation.migrate(tenantId, {
  targetLevel: 'dedicated_schema',
});

console.log(migration.status); // 'pending' → transitions to 'in_progress' immediately

Wait for completion

typescript
// Poll until done (throws on failure or timeout)
const completed = awaittenlyr.isolation.waitForMigration(migration.id, {
  pollIntervalMs: 500,
  timeoutMs:      600_000, // 10 min max
});

console.log(completed.status);  // 'completed'
console.log(completed.toLevel);  // 'dedicated_schema'
Only one migration can be in progress per tenant at a time. Attempting a second migration while one is running returns HTTP 400.

Health monitoring

Tenlyr runs a background health worker every minute. It calculates error rates, request rates, and SLA risk per tenant automatically.

How health is calculated

The HealthWorker runs every 60 seconds and checks the last 5 minutes of usage events per tenant:

ConditionStatusAction
Error rate > 10%criticalAlert created automatically; investigate immediately
Error rate 3–10%warningSLA risk flagged
Error rate < 3%healthy-
No dataunknown-

Throttle a noisy tenant

typescript
// Cap to 60 requests per minute
awaittenlyr.health.throttle(tenantId, 60);

// Remove throttle when issue is resolved
awaittenlyr.health.removeThrottle(tenantId);

Alert worker

The AlertWorker runs every 5 minutes and automatically creates health_critical alerts for any tenant in critical status. You can also create alerts manually from your own monitoring systems via POST /api/v1/alerts.

Tenants API

All tenant CRUD operations. Requires admin key unless noted.

admin = requires admin API key  |  tenant = requires tenant API key  |  public = no auth required
GET/api/v1/tenants/dashboard admin
Aggregated tenant + health + usage + billing

Returns all tenants with their health, usage metrics, and billing records merged into a single array. Used to power the Tenlyr dashboard.

Response fields (per tenant)

FieldType
id, name, ownerEmail, plan, status, region, isolationLevelTenant base fields
apiCalls, storageGb, seatsinteger / float
errorRate, requestRate, slaRisk, rateLimit, healthStatusHealth fields
mrrCents, usageCostCentsinteger (cents)
apiCallLimit, storageGbLimit, seatsLimitinteger (from plan)
GET/api/v1/tenants admin
List tenants (cursor-paginated)

Query params

ParamTypeDefaultMaxDescription
limitinteger50100Number of tenants per page
cursorstring--Opaque cursor from previous response nextCursor

Response

json
{
  "data": [ /* Tenant[] */ ],
  "nextCursor": "eyJjcmVhdGVkQXQiOi4uLn0="
}

nextCursor is null on the last page. Pass it as ?cursor= on the next request to advance the page.

POST/api/v1/tenants admin
Provision a new tenant

Request body

FieldTypeRequiredDescription
namestringrequired2–255 chars
ownerEmailstringrequiredMust be unique across all tenants
planTenantPlanoptionalDefaults to starter
regionstringoptionalDefaults to us-east-1

Response

json
{
  "tenant": { /* Tenant object */ },
  "apiKey": "tenlyr_live_abc123def456ghi789…"
}

Returns 409 if ownerEmail already exists.

POST/api/v1/tenants/:id/suspend admin
Block all API access immediately

Request body

FieldTypeRequired
reasonstringoptional

Returns 400 if already suspended or terminated.

POST/api/v1/tenants/:id/rotate-key admin
Invalidate current key, issue new one

The old key is invalidated immediately (the in-memory cache clears within 60s). Returns same shape as POST /tenants.

Update the tenant's stored key before the cache TTL expires to avoid service interruption.
GET/api/v1/tenants/:id/export admin
Download tenant data as JSON or CSV

Query params

ParamValuesDefault
formatjson | csvjson

Streams the file directly as a Content-Disposition: attachment response. Includes tenant metadata, usage, health, and billing.

Metering API

Track and query per-tenant usage. All metering endpoints require a tenant API key.

POST/api/v1/usage/track tenant
Track a usage event

Request body

FieldTypeRequired
metricapi_call | storage_gb | seats | computerequired
valueinteger ≥ 1required
endpointstringoptional

Returns 400 if api_call limit is reached for the tenant's plan.

GET/api/v1/usage/metrics tenant
Aggregated usage totals
json
{
  "tenantId": "51fad484-...",
  "apiCalls": 4821,
  "storageGb": 2.4,
  "seats": 7
}
GET/api/v1/usage/events?limit=100 tenant
Raw event log (newest first, max 1000)

Query params

ParamDefaultMax
limit1001000

Isolation API

Start and monitor data isolation migrations.

POST/api/v1/isolation/migrate admin
Start isolation migration

Request body

FieldTypeRequired
tenantIduuidrequired (admin key)
targetLeveldedicated_schema | dedicated_databaserequired
json - response
{
  "id": "mig-uuid",
  "tenantId": "51fad484-...",
  "fromLevel": "shared_schema",
  "toLevel": "dedicated_schema",
  "status": "pending",
  "error": null,
  "createdAt": "2026-03-14T10:00:00.000Z"
}
GET/api/v1/isolation/migrations/:id tenant
Poll migration status

Returns the migration record. Poll until status is completed or failed.

Health API

Query and manage per-tenant health records. All endpoints require admin key.

GET/api/v1/health/overview admin
Fleet-wide health summary
json
{
  "total": 42,
  "healthy": 38,
  "warning": 3,
  "critical": 1,
  "unknown": 0,
  "avgErrorRate": 0.4
}
POST/api/v1/health/:tenantId/throttle admin
Apply rate limit to tenant

Request body

FieldType
limitRpminteger ≥ 1
DELETE/api/v1/health/:tenantId/throttle admin
Remove rate limit

Clears the rateLimit field on the tenant's health record.

JavaScript SDK

Full TypeScript types, exponential backoff retries, per-request timeout, and injectable fetch for testing.

All methods

tenlyr.tenants

typescript
tenlyr.tenants.create({ name, ownerEmail, plan?, region? })  // → { tenant, apiKey }
tenlyr.tenants.list({ limit?, cursor? })                       // → { data: Tenant[], nextCursor: string | null }
tenlyr.tenants.get(id)                                       // → Tenant
tenlyr.tenants.health()                                      // → DashboardTenant[]
tenlyr.tenants.update(id, { name?, region? })                // → Tenant
tenlyr.tenants.suspend(id, reason?)                          // → Tenant
tenlyr.tenants.reactivate(id)                                // → Tenant
tenlyr.tenants.terminate(id)                                 // → Tenant
tenlyr.tenants.rotateApiKey(id)                             // → { tenant, apiKey }
tenlyr.tenants.changePlan(id, plan)                         // → Tenant
tenlyr.tenants.delete(id)                                   // → void

tenlyr.usage

typescript
tenlyr.usage.track({ tenantKey, metric, value, endpoint? })      // → UsageEvent
tenlyr.usage.metrics(tenantKey)                           // → TenantUsageMetric
tenlyr.usage.summary(tenantKey)                           // → UsageSummary
tenlyr.usage.events(tenantKey, limit?)                    // → UsageEvent[]

tenlyr.health

typescript
tenlyr.health.overview()                                 // → HealthOverview
tenlyr.health.list()                                     // → TenantHealth[]
tenlyr.health.get(tenantId)                              // → TenantHealth
tenlyr.health.throttle(tenantId, limitRpm)              // → TenantHealth
tenlyr.health.removeThrottle(tenantId)                  // → TenantHealth

tenlyr.isolation

typescript
tenlyr.isolation.migrate(tenantId, { targetLevel })     // → IsolationMigration
tenlyr.isolation.getMigration(id)                       // → IsolationMigration
tenlyr.isolation.migrations(tenantKey)                   // → IsolationMigration[]
tenlyr.isolation.waitForMigration(id, options?)          // → IsolationMigration

tenlyr.billing /tenlyr.alerts

typescript
tenlyr.billing.get(tenantKey)                            // → TenantBilling
tenlyr.billing.portalUrl(tenantKey)                      // → { url: string }

tenlyr.alerts.list(key?)                                // → Alert[]
tenlyr.alerts.unresolved(key?)                          // → Alert[]
tenlyr.alerts.resolve(alertId)                          // → Alert
tenlyr.alerts.resolveAll(key?)                          // → { resolved: number }

Error types

ClassHTTP statusWhen
AuthenticationError401API key missing, malformed, or revoked
PermissionError403Valid key but insufficient scope (e.g. tenant key on admin endpoint)
NotFoundError404Tenant, migration, alert, or resource does not exist
ConflictError409Duplicate - most commonly ownerEmail already exists
InvalidRequestError422Request body failed validation - inspect .body for field errors
RateLimitError429Rate limit exceeded - SDK retries automatically; has .retryAfter
TenlyrError4xx/5xxBase class - all above extend this; has .statusCode and .body
TenlyrTimeoutError-Request exceeded configured timeout - not retried
TenlyrNetworkError-Network failure before response (ECONNREFUSED, DNS, etc.) - retried
TenlyrNotImplementedError-Feature not yet available in this API version
typescript
import {
  AuthenticationError, PermissionError, NotFoundError,
  ConflictError, InvalidRequestError, RateLimitError,
  TenlyrError, TenlyrTimeoutError, TenlyrNetworkError,
} from '@tenlyr/sdk';

try {
  await tenlyr.tenants.create({ name, ownerEmail, plan });
} catch (err) {
  if (err instanceof ConflictError)       console.error('Email already exists');
  else if (err instanceof RateLimitError)   await sleep((err.retryAfter ?? 60) * 1000);
  else if (err instanceof TenlyrError)       console.error(err.statusCode, err.body);
  else throw err;
}

Python SDK Coming soon

Async-first Python client using httpx under the hood. Mirrors the JavaScript SDK with snake_case naming.

The Python SDK is under development and not yet published to PyPI. The API design below is a preview - method signatures may change before release. Use the REST API directly in the meantime.

Planned install

bash
pip install tenlyr  # not yet available

Planned API preview

python
import tenlyr

client = tenlyr.Tenlyr(
    api_key="tenlyr_live_tf_...",
    base_url="https://api.tenlyr.app",  # optional
    timeout=10.0,                        # seconds
)
python
import asyncio

async def main():
    # Provision a new tenant
    result = await client.tenants.create(
        name="Acme Corp",
        owner_email="[email protected]",
        plan="growth",
    )
    tenant, api_key = result.tenant, result.api_key

    # Track usage
    await client.usage.track(
        tenant_key=api_key,
        metric="api_call",
        value=1,
    )

    # Dashboard
    dashboard = await client.tenants.health()

asyncio.run(main())

Go SDK Coming soon

Idiomatic Go client with context support, structured errors, and zero external dependencies.

The Go SDK is under development and not yet published. The API design below is a preview. Use the REST API directly in the meantime.

Planned install

bash
go get github.com/tenlyr/tenlyr-go  // not yet available

Initialize

go
import tenlyr "github.com/tenlyr/tenlyr-go"

client := tenlyr.New(tenlyr.Config{
    APIKey:  os.Getenv("TENLYR_ADMIN_KEY"),
    Timeout: 10 * time.Second,
})

Usage

go
ctx := context.Background()

// Provision
result, err := client.Provision(ctx, tenlyr.ProvisionRequest{
    Name:       "Acme Corp",
    OwnerEmail: "[email protected]",
    Plan:       "growth",
})
if err != nil { log.Fatal(err) }

// Track usage
err = client.Usage.Track(ctx, tenlyr.TrackRequest{
    Metric:       "api_call",
    Value:        1,
    TenantAPIKey: result.APIKey,
})

Webhook Events

Tenlyr emits events for tenant lifecycle changes, billing updates, and health state changes. Configure your endpoint in the dashboard under Settings → Webhooks.

Tenant lifecycle

tenant.provisioned
A new tenant was successfully provisioned
Fires after schema, RLS, and API key setup complete
tenant.suspended
Tenant was suspended - all API access blocked
Fires immediately on POST /tenants/:id/suspend
tenant.reactivated
Suspended tenant was reactivated
Fires on POST /tenants/:id/reactivate
tenant.terminated
Tenant was permanently terminated
Fires on POST /tenants/:id/terminate
tenant.plan_changed
Tenant moved to a different billing plan
Fires on PATCH /tenants/:id/plan
tenant.key_rotated
API key was rotated - old key invalidated
Fires on POST /tenants/:id/rotate-key

Health & alerts

health.critical
Tenant crossed the critical error rate threshold (>10%)
Fired by AlertWorker every 5 minutes
health.recovered
Tenant returned to healthy status from warning/critical
Fired by HealthWorker every minute
alert.created
New unresolved alert created for a tenant
Fires on POST /alerts or AlertWorker auto-create

Billing (Stripe)

customer.subscription.created
Stripe subscription started
Forwarded from Stripe webhook
invoice.payment_succeeded
Monthly invoice paid successfully
Forwarded from Stripe webhook
invoice.payment_failed
Payment failed - consider suspending tenant
Forwarded from Stripe webhook

Isolation

isolation.migration_started
Isolation upgrade migration began
Fires on POST /isolation/migrate
isolation.migration_completed
Migration completed successfully
Fires when migration status becomes completed
isolation.migration_failed
Migration failed - tenant remains at original level
Fires when migration status becomes failed

Webhook Payloads

All webhook deliveries use the same envelope structure with HMAC-SHA256 signature verification.

Envelope

json
{
  "id": "evt_01HZ...",
  "type": "tenant.provisioned",
  "createdAt": "2026-03-14T10:00:00.000Z",
  "data": { /* event-specific payload */ }
}

Signature verification

Every request includes a Stripe-Signature-style header. Verify it using your webhook secret:

typescript
import * as crypto from 'crypto';

function verifySignature(
  payload: Buffer,
  signature: string,
  secret: string
): boolean {
  const parts = signature.split(',')
    .reduce((acc, part) => {
      const [k, v] = part.split('=');
      acc[k] = v; return acc;
    }, {} as Record<string, string>);

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts['t']}.${payload.toString()}`)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(parts['v1']),
    Buffer.from(expected)
  );
}

Example payloads

tenant.provisioned

json
{
  "type": "tenant.provisioned",
  "data": {
    "tenantId": "51fad484-7f82-4c97-a3e1-b0c29d4f8201",
    "name": "Acme Corp",
    "plan": "growth",
    "region": "us-east-1"
  }
}

health.critical

json
{
  "type": "health.critical",
  "data": {
    "tenantId": "51fad484-...",
    "errorRate": 12.4,
    "requestRate": 340,
    "slaRisk": true
  }
}

isolation.migration_completed

json
{
  "type": "isolation.migration_completed",
  "data": {
    "migrationId": "mig-uuid",
    "tenantId": "51fad484-...",
    "fromLevel": "shared_schema",
    "toLevel": "dedicated_schema"
  }
}
Return HTTP 200 from your webhook endpoint quickly (within 5s). Process events asynchronously using a queue. Tenlyr retries failed deliveries with exponential backoff for up to 72 hours.