Build a Platform
Model each of your customers as a profile, connect their accounts, scope API keys per tenant, and route webhooks back to the right customer.
If you're building social features into your own product, your customers each bring their own social accounts. Zernio's tenant boundary for this is the profile: one profile per customer, their connected accounts inside it, and your database holding the mapping.
This page covers the core model. Once it's in place, the use-case recipes show how to build specific features on top without fighting rate limits:
Tenant publishing
Retry-safe posting for every customer: idempotency, media, per-tenant queues
Tenant analytics dashboards
Show each customer their own metrics: sync workers, freshness expectations, request budgets
Tenant inbox & DMs
A unified social inbox per customer: webhook-first architecture, replying, backfills
Requests flow one way, events flow back — and your database is what connects the two:
Every new team starts with a profile named "Default". You can use it for your own accounts and create one profile per customer alongside it.
Step 1: Create a profile per customer
When a customer signs up in your app, create their profile and store the returned profile._id on your customer record. Profile names must be unique within your team, so your internal customer ID makes a good name.
const { data } = await zernio.profiles.createProfile({
body: {
name: 'customer_8f3a2', // unique per team
description: 'Acme Corp',
},
});
// Store data.profile._id on your customer recordresult = client.profiles.create_profile(
name="customer_8f3a2", # unique per team
description="Acme Corp",
)
# Store result["profile"]["_id"] on your customer recordcurl -X POST "https://zernio.com/api/v1/profiles" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "customer_8f3a2", "description": "Acme Corp" }'See Create profile for all fields.
Step 2: Let them connect their accounts
Start the OAuth flow with the customer's profileId so the account lands in their profile. For a fully white-label experience (your UI, no Zernio-hosted selection screens), use headless mode.
curl "https://zernio.com/api/v1/connect/instagram?profileId=PROFILE_ID&redirect_url=https://your-app.com/callback" \
-H "Authorization: Bearer YOUR_API_KEY"
# → { "authUrl": "..." } — redirect your customer's browser thereThe connecting accounts guide covers the full flow, including headless mode and platforms that need a secondary selection (Facebook Pages, LinkedIn orgs, Pinterest boards).
Subscribe to the account.connected webhook to learn about new connections server-side. Its payload carries both accountId and profileId, which is exactly the mapping you want to persist:
{
"event": "account.connected",
"account": {
"accountId": "665f1c2e8b3a4d0012345678",
"profileId": "664a0b1c2d3e4f0011223344",
"platform": "instagram",
"username": "acmecorp"
}
}account.disconnected fires with the same identifiers when a token dies or a customer removes access, so you can prompt them to reconnect.
The whole onboarding flow, end to end:
Step 3: Post on their behalf
List a customer's accounts by filtering on their profile, then create posts with those account IDs:
const { data } = await zernio.accounts.listAccounts({
query: { profileId: customer.zernioProfileId },
});
await zernio.posts.createPost({
body: {
content: 'Posted from your app!',
platforms: data.accounts.map((a) => ({
platform: a.platform,
accountId: a._id,
})),
publishNow: true,
},
});data = client.accounts.list_accounts(profile_id=customer.zernio_profile_id)
client.posts.create(
content="Posted from your app!",
platforms=[
{"platform": a["platform"], "accountId": a["_id"]} for a in data["accounts"]
],
publish_now=True,
)curl "https://zernio.com/api/v1/accounts?profileId=PROFILE_ID" \
-H "Authorization: Bearer YOUR_API_KEY"Posts validate accountId against your whole team, not against a profile. The profile is an organizational boundary, so keep the account-to-customer mapping in your database (Step 2 gives it to you) and only pass a customer their own account IDs.
Step 4: Route webhooks back to the right customer
Webhook endpoints are configured once per team (up to 10), not per profile. Run a single endpoint and route internally:
| Events | Tenant key in the payload | Look it up in |
|---|---|---|
Post events (post.published, post.failed, …) | accountId on each platform entry | The accountId → customer map from Step 2 |
Account events (account.connected, account.disconnected) | profileId directly | Your customer record |
Inbox events (message.received, …) | account.id | The accountId → customer map from Step 2 |
See the webhooks overview for delivery, retries, and signature verification, and the tenant inbox recipe for a full webhook handler.
Rate limits with many tenants
One API key is enough for your whole integration. Rate limits are based on your team's total connected accounts, so your request budget grows automatically as you onboard customers:
| Connected accounts | Requests per minute |
|---|---|
| 0–2 | 60 |
| 3–2,000 | 600 |
| 2,001+ | 1,200 |
That budget serves all your tenants together, which has three practical consequences:
- Prefer webhooks over polling. Most per-tenant "is there anything new?" traffic disappears when webhooks push events to you. Save API calls for actions and scheduled syncs.
- Run background work as one smooth queue. A sync worker with a small concurrency cap uses the budget steadily. Per-tenant cron jobs that all fire at :00 create bursts that hit the limit even when your average usage is low.
- Fairness between tenants is yours to enforce. Zernio limits your key, not your customers — if one tenant triggers a huge burst (a bulk import, a mass reply), queue it so it can't starve everyone else.
When you do get a 429, honor the Retry-After header before retrying. And handle 402 PAYMENT_REQUIRED distinctly: it means your Zernio billing is suspended, which affects every tenant at once — alert your on-call, don't retry.
Monitor account health
Social tokens die: customers change passwords, revoke access, or trip a platform security check. In a multi-tenant app this is routine, so build the reconnect loop up front:
- Subscribe to
account.disconnected— it carriesaccountIdandprofileId, so you know exactly which customer to notify. - Prompt that customer to reconnect in your UI by starting a new connect flow with their same
profileId. - As a safety net, periodically call account health to catch anything a webhook missed, and reconcile.
Scoped API keys
By default an API key can touch every profile in your team. For stricter isolation, create keys scoped to specific profiles with the scope and profileIds fields, and optionally make them read-only:
curl -X POST "https://zernio.com/api/v1/api-keys" \
-H "Authorization: Bearer YOUR_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "acme-corp-readonly",
"scope": "profiles",
"profileIds": ["664a0b1c2d3e4f0011223344"],
"permission": "read"
}'Use cases:
- A per-tenant backend service that can only see its own customer's profile.
- Read-only keys for analytics dashboards.
- Keys with
expiresIn(days) for temporary access.
Scoped keys are an access-control tool, not a throughput tool. Your rate limit scales with your team's connected accounts, so one full-access key is enough to run even a large multi-tenant integration.
See Create API key for details.
Offboarding a customer
When a customer churns:
- Disconnect their accounts. Active connected accounts block profile deletion.
- Delete the profile. Any remaining disconnected accounts and provisioned WhatsApp numbers are moved to another of your profiles, never deleted, so nothing is lost by accident.
Related
- Tenant analytics dashboards - per-customer metrics without burning your rate limit
- Tenant inbox & DMs - a unified social inbox for each customer
- Connecting accounts - the full OAuth and headless flows
- Rate limits - windows, headers, and backoff
- Idempotency & safe retries - make your posting pipeline retry-safe
- Webhooks - delivery, signatures, and all event payloads