Introduction
The Tercela API is a JSON-over-HTTP REST API. Every tenant-owned resource is scoped to your active organization, and all requests share the same base URL. Send and receive JSON with Content-Type: application/json.
# Base URL https://tercela.com/api/v1 # Health check (no auth) curl https://tercela.com/healthz
Authentication
Register or sign in to get a token pair: a JWT access token (valid 4h) and an opaque refresh token (valid 7 days, single-use — it rotates on every refresh). Send the access token as a Bearer header. The access token also carries your active organization, so most endpoints need no extra tenant parameter.
# 1. Register (also creates your personal org)
curl -X POST https://tercela.com/api/v1/identity/register \
-H "Content-Type: application/json" \
-d '{"userName":"jane","email":"jane@acme.com","password":"Abcdef1!"}'
# 2. Login → tokens
curl -X POST https://tercela.com/api/v1/identity/login \
-H "Content-Type: application/json" \
-d '{"userName":"jane","password":"Abcdef1!"}'
# → { "userId": "...", "tokens": { "accessToken": "...", "refreshToken": "..." } }
# 3. Use the access token
curl https://tercela.com/api/v1/identity/me -H "Authorization: Bearer <accessToken>"Organizations
Users are global and can belong to many organizations. Everyone gets a personal org at sign-up; create a team org to invite others. The active org travels in the access token — switching orgs (or creating one) returns a fresh token pair. Roles are owner / admin / member.
# Create a team org (returns a new token pair for that org)
curl -X POST https://tercela.com/api/v1/organizations \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json" \
-d '{"name":"ACME Corp"}'
# Switch back to another org you belong to
curl -X POST https://tercela.com/api/v1/organizations/switch \
-H "Authorization: Bearer <accessToken>" \
-d '{"orgId":"<org-id>"}'Channels & Connections
A connection is a social account linked to your organization (WhatsApp, Instagram, X, TikTok). Access tokens are encrypted at rest and never returned. WhatsApp connects through Facebook Login (see below); the other platforms register a token directly for now. Sandbox mode simulates the platform APIs until the app reviews are approved.
# List the org's connections
curl https://tercela.com/api/v1/channels/connections \
-H "Authorization: Bearer <accessToken>"
# Send a WhatsApp message (also recorded in the conversation thread)
curl -X POST https://tercela.com/api/v1/channels/messages \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json" \
-d '{"connectionId":"<id>","to":"+5511999990000","text":"Hello from Tercela!"}'
# Inbox: list conversations, then read one thread
curl "https://tercela.com/api/v1/channels/conversations?platform=whatsapp" \
-H "Authorization: Bearer <accessToken>"
curl https://tercela.com/api/v1/channels/conversations/<id>/messages \
-H "Authorization: Bearer <accessToken>"Connecting WhatsApp
WhatsApp uses Meta's Facebook Login for Business. The browser runs FB Login (SDK), the backend lists the WhatsApp Business Accounts and numbers the resulting user token can access, the user picks a number, and the short-lived token is exchanged for a long-lived one server-side (the app secret never reaches the browser). The app is then subscribed to the WABA for inbound webhooks.
# After FB Login returns a user access token in the browser:
curl -X POST https://tercela.com/api/v1/channels/whatsapp/accounts \
-H "Authorization: Bearer <accessToken>" \
-d '{"accessToken":"<fb-user-token>"}'
# → { "accounts": [ { "wabaId": "...", "phoneNumbers": [ { "phoneNumberId": "...",
# "displayPhoneNumber": "+1 555-167-2101", "verifiedName": "..." } ] } ] }
# Connect the picked number
curl -X POST https://tercela.com/api/v1/channels/whatsapp/connect \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json" \
-d '{"accessToken":"<fb-user-token>","wabaId":"...","phoneNumberId":"...",
"displayPhoneNumber":"+1 555-167-2101","verifiedName":"..."}'Posts (publishing)
Publishing is asynchronous. A post fans out to one target per selected connection; a background worker delivers each target through the platform adapter, retries with backoff, and rolls the post status up to published / partial / failed. Poll the post to watch per-platform delivery.
# Create a post targeting two connections
curl -X POST https://tercela.com/api/v1/posts \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json" \
-d '{
"body": "Launching our new product! 🚀",
"media": [{"type":"image","url":"https://cdn.example.com/img.jpg"}],
"targets": [{"connectionId":"<ig-conn>"}, {"connectionId":"<x-conn>"}]
}'
# Poll delivery status
curl https://tercela.com/api/v1/posts/<post-id> -H "Authorization: Bearer <accessToken>"Webhooks
Register endpoints and Tercela pushes signed event envelopes to your CRM or any system: message.received, message.sent, message.delivered, message.read, message.failed, reaction.received, conversation.started, connection.connected and connection.disconnected. Each POST carries X-Tercela-Event-Id and X-Tercela-Signature (hex HMAC-SHA256 of the raw body with your secret). Answer any 2xx within 5 seconds; failures are retried 7 times with exponential backoff (10^(n-1) seconds, capped at 24h). Delivery is at-least-once — dedupe by event id. After 10 consecutive failures the webhook is disabled automatically; re-enable it after fixing your endpoint.
# Create a webhook subscription
curl -X POST https://tercela.com/api/v1/webhooks \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json" \
-d '{"name":"My CRM","url":"https://example.com/hooks/messaging",
"secret":"whsec_...","events":["message.received","message.sent"]}'
# Envelope delivered to your endpoint:
# POST https://example.com/hooks/messaging
# X-Tercela-Event-Id: 4fc4cacf-...
# X-Tercela-Signature: d5c35bdb... (hex HMAC-SHA256 of the raw body)
{ "id": "4fc4cacf-...", "event": "message.received",
"timestamp": "2026-07-04T16:46:50Z", "platform": "whatsapp",
"message": { "id": "...", "type": "text", "body": "Hello!", "direction": "in" },
"conversation": { "id": "...", "contactName": "...", "contactPhone": "..." },
"connection": { "id": "..." } }
# Verify the signature (Node.js)
const crypto = require('crypto')
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
const valid = crypto.timingSafeEqual(Buffer.from(expected),
Buffer.from(req.headers['x-tercela-signature']))Platform webhooks (inbound)
Meta calls Tercela's webhook endpoint to verify the subscription (GET challenge) and to deliver inbound WhatsApp events (POST). Events are stored raw and processed asynchronously, so the endpoint always answers fast. These are platform-internal — your systems consume the outbound Webhooks above.
Partner console (white-label)
For white-label partners: accounts holding the tenant-admin role administer their own workspace — every client organization with usage, the user directory, custom domains and your own Meta app — scoped to the tenant of the host you call. The WhatsApp provider is decided by the platform per workspace: either the platform's general tech-provider app (default; the Meta app settings are locked) or your own Meta app (required then — connections stay disabled until you configure it; Facebook Login runs directly on your domain and webhooks are validated against your app secret). Grant the role to another user of your workspace with the users endpoints (you cannot revoke your own). A granted role takes effect on the next sign-in. Tenant-admins see client usage, never client data.
# Workspace-wide usage (call it on YOUR workspace host) curl https://tercela.com/api/v1/tenant-admin/overview \ -H "Authorization: Bearer <accessToken>" # Your client organizations, with usage curl "https://tercela.com/api/v1/tenant-admin/orgs?limit=20" \ -H "Authorization: Bearer <accessToken>" # Promote a teammate to workspace admin curl -X PUT https://tercela.com/api/v1/tenant-admin/users/<userId>/admin \ -H "Authorization: Bearer <accessToken>"
Errors
Errors return the matching HTTP status and a JSON body { "error": "message" }. A 401 means the access token is missing or expired — refresh it and retry. A 403 on a tenant route means no active organization or an insufficient role.
{ "error": "invalid credentials" } # 401
{ "error": "insufficient organization role" } # 403
{ "error": "post not found" } # 404