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.
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 type | Format | Scope |
|---|---|---|
| Admin key | tenlyr_live_tf_… | Full platform access - tenants, health, alerts, dashboard |
| Tenant key | tenlyr_live_… | Scoped to a single tenant - usage, billing, isolation |
Base URL
https://api.tenlyr.app
Response format
All responses are JSON. Successful responses return the resource directly. Error responses return:
{
"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.
to build tenant infrastructure
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.
tenlyr.tenants.create() - schema, RLS, hashed key, in under 200ms.tenlyr.usage.track() - one line per event, rollups computed automatically.tenlyr.billing.portalUrl() - Stripe synced, MRR tracked, portal ready.tenlyr.isolation.migrate() - zero-downtime, async, under 5 minutes.tenlyr.health.overview() - error rates, SLA risk, throttling, all automated.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.
Who Tenlyr is for
| If you're… | Tenlyr gives you… |
|---|---|
| An early-stage founder building your first SaaS | The multi-tenant foundation in a weekend, so your first sprint is product, not plumbing |
| A growing team approaching your first enterprise customer | Dedicated isolation and audit-ready health records - close the deal this quarter |
| A platform team tired of maintaining internal tooling | A managed, versioned, API-first replacement for your homegrown tenant registry |
| A solo engineer who wants to build B2B SaaS on the side | The entire infrastructure layer as a single npm package - so you can actually finish it |
Architecture
How a single SDK call flows through Tenlyr's infrastructure - from your application to provisioning, metering, health monitoring, and billing sync.
tenlyr.tenants.create()
tenlyr_live_…
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
| Worker | Trigger | Responsibility |
|---|---|---|
| Provisioning Worker | On POST /tenants | Creates the Postgres schema, applies Row-Level Security policies, hashes the API key, and returns the plaintext key once - completes in under 200ms |
| Metering Pipeline | On POST /usage/track | Writes the usage event, increments rolling counters, enforces plan limits, and triggers a plan-exceeded error at 100% of limit |
| Health Worker | Every 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 Sync | Every 5 minutes | Reconciles 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.
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.
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.
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.
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.
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.
tenlyr.billing.portalUrl().
Scaling model
Tenlyr is designed to scale horizontally at every layer. Here's how each component behaves under load.
tenantId. Each partition is consumed by exactly one worker instance, preventing duplicate processing. Consumer group rebalancing handles worker failures automatically.INCRBY - atomic, conflict-free, and sub-millisecond. Postgres writes are batched every 10 seconds to reduce I/O.Quickstart
From zero to a provisioned tenant in under 30 minutes.
Sign up at tenlyr.app and copy your admin API key from the dashboard. It looks like tenlyr_live_tf_4a8b2c9d…
npm install @tenlyr/sdk
Wire this into your signup handler. The call returns a tenant object and a one-time apiKey - store the key securely.
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 });
Call track() on your key API actions. Use the tenant's own API key (not admin).
// In your API middleware or handler: await tenlyr.usage.track({ tenantKey: req.user.tenantApiKey, metric: 'api_call', value: 1, endpoint: req.path, });
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
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
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
Go Coming soon
Direct API (cURL)
All SDKs are thin wrappers around the REST API. You can call it directly:
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
| Field | Type | Description |
|---|---|---|
| id | uuid | Unique tenant identifier |
| name | string | Display name (e.g. "Acme Corp") |
| ownerEmail | string | Email of the account owner - must be unique |
| plan | starter | growth | scale | enterprise | Active billing plan |
| status | pending | active | suspended | terminated | Lifecycle state |
| region | string | Deployment region (e.g. us-east-1) |
| isolationLevel | shared_schema | dedicated_schema | dedicated_database | Data isolation tier |
| apiKeyPrefix | string | First 16 chars of the hashed API key - for display only |
| lastActiveAt | timestamp | null | Last time a tenant API call was authenticated |
| createdAt | timestamp | Provisioning time |
Plans
| Plan | Price | Tenants | Events/mo | Best for |
|---|---|---|---|---|
starter | $0/mo | Up to 5 | 1K | Indie / evaluating |
growth | $99/mo | Up to 100 | 100K | Seed-stage SaaS |
scale | $399/mo | Unlimited | 1M | Series A+ |
enterprise | Custom | Unlimited | Custom | Regulated 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.
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
Migration behaviour
Migrations are asynchronous and non-destructive. When you call POST /api/v1/isolation/migrate, Tenlyr:
- Creates a new migration record with
status: pending, then transitions toin_progresswhen the job fires - Copies data to the new isolation context
- Updates the tenant's
isolationLevelon success - Sets
status: completedorfailedwith an error message
Average migration time is under 5 minutes. Use sdk.isolation.waitForMigration(id) to poll until completion.
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
| Metric | Value unit | Use case |
|---|---|---|
| api_call | integer (count) | Track every API request your tenants make |
| storage_gb | float (GB) | Track data storage consumed |
| seats | integer (count) | Track active user seats per tenant |
| compute | integer | General-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.
Usage event object
| Field | Type | Description |
|---|---|---|
| id | uuid | Event ID |
| tenantId | uuid | Owning tenant |
| metric | UsageMetric | Which metric was tracked |
| value | integer | Amount to increment by (minimum 1) |
| endpoint | string | null | Optional: which API endpoint triggered the event |
| timestamp | timestamp | Event 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
State descriptions
| State | API access | Data | Reversible |
|---|---|---|---|
pending | Blocked | Preserved | Yes - becomes active after internal provisioning |
active | Full | Preserved | Yes |
suspended | Blocked - 401 on all requests | Preserved | Yes - call /reactivate |
terminated | Blocked | Pending deletion | No |
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
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.
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
- Store
tenant.idandapiKeyin your user/org record - Pass
apiKeyto your tenant - they use it to authenticate usage calls - Optionally seed billing via
tenlyr.billing.get(tenantApiKey)once the tenant makes their first request - 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:
// 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
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:
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
// 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
// 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'
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:
| Condition | Status | Action |
|---|---|---|
| Error rate > 10% | critical | Alert created automatically; investigate immediately |
| Error rate 3–10% | warning | SLA risk flagged |
| Error rate < 3% | healthy | - |
| No data | unknown | - |
Throttle a noisy tenant
// 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.
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)
| Field | Type |
|---|---|
| id, name, ownerEmail, plan, status, region, isolationLevel | Tenant base fields |
| apiCalls, storageGb, seats | integer / float |
| errorRate, requestRate, slaRisk, rateLimit, healthStatus | Health fields |
| mrrCents, usageCostCents | integer (cents) |
| apiCallLimit, storageGbLimit, seatsLimit | integer (from plan) |
Query params
| Param | Type | Default | Max | Description |
|---|---|---|---|---|
| limit | integer | 50 | 100 | Number of tenants per page |
| cursor | string | - | - | Opaque cursor from previous response nextCursor |
Response
{
"data": [ /* Tenant[] */ ],
"nextCursor": "eyJjcmVhdGVkQXQiOi4uLn0="
}
nextCursor is null on the last page. Pass it as ?cursor= on the next request to advance the page.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required | 2–255 chars |
| ownerEmail | string | required | Must be unique across all tenants |
| plan | TenantPlan | optional | Defaults to starter |
| region | string | optional | Defaults to us-east-1 |
Response
{
"tenant": { /* Tenant object */ },
"apiKey": "tenlyr_live_abc123def456ghi789…"
}
Returns 409 if ownerEmail already exists.
Request body
| Field | Type | Required |
|---|---|---|
| reason | string | optional |
Returns 400 if already suspended or terminated.
The old key is invalidated immediately (the in-memory cache clears within 60s). Returns same shape as POST /tenants.
Query params
| Param | Values | Default |
|---|---|---|
| format | json | csv | json |
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.
Request body
| Field | Type | Required |
|---|---|---|
| metric | api_call | storage_gb | seats | compute | required |
| value | integer ≥ 1 | required |
| endpoint | string | optional |
Returns 400 if api_call limit is reached for the tenant's plan.
{
"tenantId": "51fad484-...",
"apiCalls": 4821,
"storageGb": 2.4,
"seats": 7
}
Query params
| Param | Default | Max |
|---|---|---|
| limit | 100 | 1000 |
Isolation API
Start and monitor data isolation migrations.
Request body
| Field | Type | Required |
|---|---|---|
| tenantId | uuid | required (admin key) |
| targetLevel | dedicated_schema | dedicated_database | required |
{
"id": "mig-uuid",
"tenantId": "51fad484-...",
"fromLevel": "shared_schema",
"toLevel": "dedicated_schema",
"status": "pending",
"error": null,
"createdAt": "2026-03-14T10:00:00.000Z"
}
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.
{
"total": 42,
"healthy": 38,
"warning": 3,
"critical": 1,
"unknown": 0,
"avgErrorRate": 0.4
}
Request body
| Field | Type |
|---|---|
| limitRpm | integer ≥ 1 |
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
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
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
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
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
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
| Class | HTTP status | When |
|---|---|---|
| AuthenticationError | 401 | API key missing, malformed, or revoked |
| PermissionError | 403 | Valid key but insufficient scope (e.g. tenant key on admin endpoint) |
| NotFoundError | 404 | Tenant, migration, alert, or resource does not exist |
| ConflictError | 409 | Duplicate - most commonly ownerEmail already exists |
| InvalidRequestError | 422 | Request body failed validation - inspect .body for field errors |
| RateLimitError | 429 | Rate limit exceeded - SDK retries automatically; has .retryAfter |
| TenlyrError | 4xx/5xx | Base 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 |
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.
Planned install
pip install tenlyr # not yet available
Planned API preview
import tenlyr client = tenlyr.Tenlyr( api_key="tenlyr_live_tf_...", base_url="https://api.tenlyr.app", # optional timeout=10.0, # seconds )
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.
Planned install
go get github.com/tenlyr/tenlyr-go // not yet available
Initialize
import tenlyr "github.com/tenlyr/tenlyr-go" client := tenlyr.New(tenlyr.Config{ APIKey: os.Getenv("TENLYR_ADMIN_KEY"), Timeout: 10 * time.Second, })
Usage
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
POST /tenants/:id/suspendPOST /tenants/:id/reactivatePOST /tenants/:id/terminatePATCH /tenants/:id/planPOST /tenants/:id/rotate-keyHealth & alerts
POST /alerts or AlertWorker auto-createBilling (Stripe)
Isolation
POST /isolation/migratecompletedfailedWebhook Payloads
All webhook deliveries use the same envelope structure with HMAC-SHA256 signature verification.
Envelope
{
"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:
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
{
"type": "tenant.provisioned",
"data": {
"tenantId": "51fad484-7f82-4c97-a3e1-b0c29d4f8201",
"name": "Acme Corp",
"plan": "growth",
"region": "us-east-1"
}
}
health.critical
{
"type": "health.critical",
"data": {
"tenantId": "51fad484-...",
"errorRate": 12.4,
"requestRate": 340,
"slaRisk": true
}
}
isolation.migration_completed
{
"type": "isolation.migration_completed",
"data": {
"migrationId": "mig-uuid",
"tenantId": "51fad484-...",
"fromLevel": "shared_schema",
"toLevel": "dedicated_schema"
}
}
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.