The Tephlo API lets your application talk to your workspace: start conversations answered by your assistant, look up orders with customer verification, capture support tickets, manage knowledge and guidance, and receive signed webhooks.
Everything on this page works against sandbox keys and synthetic sandbox data, so you can build and test an integration before your workspace is approved for production traffic. What separates the two environments is covered at the end.
Every request carries an API key in the Authorization header as a bearer token. Sandbox keys start with tphlo_sk_test_, production keys with tphlo_sk_live_:
Authorization: Bearer tphlo_sk_test_...Keys are created in the tenant console on the Developers page. Each key carries scopes, and a request outside a key’s scopes is refused — so a key that only needs to read conversations should be created with only that, not with everything.
The examples below use a placeholder base URL — https://api.example-tephlo.app — replace it with your backend origin, shown on the console’s Developers page. Export both values once and every example becomes copy-paste-run:
BASE="https://api.example-tephlo.app" # replace with your backend origin
KEY="tphlo_sk_test_..." # from Console -> DevelopersGET /v1/me returns the workspace, environment and scopes the key resolves to — the fastest way to confirm authentication works before anything else.
curl "$BASE/v1/me" \
-H "Authorization: Bearer $KEY"POST /v1/conversations opens a conversation for a customer you identify by your own reference. It is a consequential POST, so it takes an Idempotency-Key.
curl "$BASE/v1/conversations" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-conv-1" \
-d '{"customer_ref":"demo-1"}'The response includes the conversation’s id; the next two calls use it.
curl "$BASE/v1/conversations/{id}/messages" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-msg-1" \
-d '{"text":"What'\''s your return policy?"}'The call returns as soon as the message is accepted — the assistant’s reply is generated asynchronously, exactly as it is for a customer writing on WhatsApp. Poll the message list (step 4) until the reply appears, or register a webhook and be told the moment it exists.
curl "$BASE/v1/conversations/{id}/messages" \
-H "Authorization: Bearer $KEY"POST /v1/tickets records a callback request, complaint or refund request against a customer — the same write action the assistant itself uses.
curl "$BASE/v1/tickets" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-ticket-1" \
-d '{"kind":"callback","customer_ref":"demo-1","summary":"Please call me"}'Networks fail mid-request, and retrying a POST that already succeeded must not create a second conversation or a second ticket. Every consequential POST therefore requires an Idempotency-Key header — any unique string you choose, one per logical operation.
409: the key names an operation, and one key cannot name two.Every error is the same envelope, so one error handler covers the whole API:
{"error":{"code":"rate_limited","message":"Too many requests. Retry after the indicated delay."}}Every response — success or failure — also carries an x-request-id header. Include it when you contact support: it lets the request be found in the logs without guessing.
| Code | Status | Meaning |
|---|---|---|
unauthorized | 401 | Missing, malformed, revoked or expired key. |
forbidden | 403 | The key is valid but lacks the scope for this endpoint. |
capability_unavailable | 409 | The workspace cannot perform this action right now — for example a ticket kind the workspace has not enabled. |
policy_unavailable | 503 | The platform could not load the policy needed to answer safely. Retry with backoff. |
rate_limited | 429 | Too many requests. Honour the retry delay before trying again. |
sandbox_quota_exhausted | 429 | The sandbox’s daily allowance is used up; it resets automatically. |
idempotency_key_conflict | 409 | The Idempotency-Key was already used with a different body — see idempotency. |
Webhooks are managed from the console’s Developers page — endpoints, signing secrets, a test ping and the delivery log for each endpoint — or through the API with a key that carries the webhooks:manage scope. Registering an endpoint and the events it should receive looks like this:
curl "$BASE/v1/webhook-endpoints" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-hook-1" \
-d '{"url":"https://example.com/tephlo-webhook","events":["tephlo.v1.conversation.reply"]}'Endpoints must be public HTTPS on port 443, and redirects are not followed — the URL you register is exactly where deliveries go.
Every delivery is signed. The Tphlo-Signature header carries a timestamp and an HMAC-SHA256, computed with your endpoint’s signing secret over the string {t}.{body}:
Tphlo-Signature: t=<unix>,v1=<hex hmac-sha256 of "{t}.{body}">Verify it before trusting the payload: recompute the HMAC over the raw request body, compare in constant time, and reject anything where |now − t| > 300 seconds — the timestamp is what stops a captured delivery being replayed later.
const crypto = require("node:crypto");
function verifyTphloSignature(header, rawBody, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(t + "." + rawBody)
.digest("hex");
const given = Buffer.from(parts.v1 || "", "hex");
const want = Buffer.from(expected, "hex");
return given.length === want.length && crypto.timingSafeEqual(given, want);
}Tphlo-Event-Id header that stays the same across retries of the same event — record the ids you have processed and skip any you have seen before.The two environments are separated by the key, not by different URLs: a tphlo_sk_test_ key works in the sandbox, a tphlo_sk_live_ key in production. What a sandbox key can and cannot reach is precise:
403. Order-status lookups run against the workspace’s real order system and still require the customer’s verifier.