Workflow API reference
Esta página aún no está disponible en tu idioma.
connect.synapsai.app hosts the SynapsAI API; REST is under /v1.
Base URL: https://connect.synapsai.app. Every request carries exactly Authorization: Bearer sk_live_… — one
space, no other spelling; anything else is 401 INVALID_KEY (see
keys and scopes). Bodies are JSON. Every error is
{"code": "…", "message": "…"} with the status below.
The API is part of the GROWTH, PRO and ENTERPRISE plans: on a lower plan every endpoint, GET /v1/me included,
answers 403 PLAN_FEATURE_UNAVAILABLE.
Endpoints
Section titled “Endpoints”| Method · path | Scope | Answers |
|---|---|---|
GET /v1/me | any | {accountId, keyName, keyPrefix, scopes, plan} — checks a key |
GET /v1/agents | agents:read | a numbered page of your agents, all in one page; each has key (the agentKey the other endpoints take) and title |
POST /v1/agents/{agentKey}/messages | messages:write | send a message to an agent (below) |
GET /v1/messages/{messageId} | messages:write | status and reply of a message (below) |
GET /v1/conversations | conversations:read | a numbered page of conversations |
GET /v1/conversations/{chatId} | conversations:read | one conversation with csat and its messages |
GET /v1/contacts | contacts:read | a numbered page of contacts; search, page, size, and inWindow=WHATSAPP|MESSENGER|INSTAGRAM for the contacts inside that channel’s 24-hour window (each then has windowExpiresAt) |
GET /v1/contacts/{id} | contacts:read | one contact |
POST /v1/contacts:upsert | contacts:write | create or update a contact (below) |
GET /v1/leads | leads:read | a cursor page of leads |
GET /v1/agents/{agentKey}/templates | campaigns:read | the approved WhatsApp templates — {items[{name, language, category, body, variables, examples}], lastSyncedAt} |
POST /v1/agents/{agentKey}/campaigns | campaigns:write | send a campaign, Idempotency-Key required → 202 (guide) |
GET /v1/campaigns | campaigns:read | a numbered page of campaigns; agentKey, origin, status |
GET /v1/campaigns/{id} | campaigns:read | one campaign with its sent, delivered, read and failed counts |
GET /v1/campaigns/{id}/deliveries | campaigns:read | a numbered page of per-recipient rows; status |
POST /v1/campaigns/{id}:cancel | campaigns:write | cancel a scheduled campaign — 409 CAMPAIGN_NOT_CANCELLABLE once it has started sending |
GET /v1/agents/{agentKey}/hooks | hooks:write | a numbered page of this agent’s API subscriptions, all in one page |
POST /v1/agents/{agentKey}/hooks | hooks:write | subscribe (below) → 201 |
DELETE /v1/hooks/{id} | hooks:write | unsubscribe → 204; 404 for a hook created in the console or by another account |
Conversation filters
Section titled “Conversation filters”GET /v1/conversations takes agentKey, from, to (ISO-8601), status, origin (e.g. API, CHAT,
WHATSAPP), csatMin, csatMax (1–5), page (from 0) and size (1–100, default 20).
- With
csatMinorcsatMaxset, conversations that have no rating are left out.csatMingreater thancsatMaxis400 INVALID_REQUEST. - A list item is a summary:
chatId(the idGET /v1/conversations/{chatId}takes),agentKey,origin,status,closeReason,externalUserId,contactId,lastMessageText,lastMessageAt,userMessageCount,occurredAt. The rating itself (csat: {score, feedback, submittedAt, source}) is on the conversation.
| Endpoint | Shape | Next page |
|---|---|---|
GET /v1/conversations, GET /v1/contacts | {"items": [...], "page": 0, "size": 20, "total": 230} | ask page + 1 while (page + 1) × size < total |
GET /v1/agents, GET /v1/agents/{agentKey}/hooks | the same shape, everything in page 0 (size = total) | — |
GET /v1/leads | {"items": [...], "nextCursor": "…", "total": 12, "deliveryIssues": [...]} | pass cursor=<nextCursor>; nextCursor is null on the last page |
Every list is one of these two shapes — never a bare array. Keep your own page counter: request page n, then n + 1.
GET /v1/leads?agentKey=shop-bot (agentKey required) also takes outcome — a CRM outcome such as WRITTEN_DEAL,
WRITTEN_CONTACT, SKIPPED_BELOW_THRESHOLD — cursor and limit (1–100). Each item:
{ "leadId": "job_7f2", "chatId": "6713f0c2a1b2c3d4e5f60718", "contactId": "c_91", "contactName": "Ana", "recordedAt": "2026-10-28T12:10:00Z", "origin": "API", "score": 82, "interest": "3 units, delivery Friday", "dealTitle": "3 × drill 4 mm", "qualified": true, "status": "NO_DESTINATION", "destinations": [] }status is SENT, PARTIAL, NOT_SENT or NO_DESTINATION (captured, with no CRM or hook to send it to).
Errors (all endpoints above)
Section titled “Errors (all endpoints above)”| Status | code | When |
|---|---|---|
| 400 | INVALID_REQUEST | a parameter or body field breaks its rules (the message names it) |
| 400 | IDENTIFIER_REQUIRED | contacts:upsert without any identifier |
| 400 | INVALID_PHONE, INVALID_EMAIL | contacts:upsert with a phone that is not E.164, or an invalid e-mail |
| 400 | INVALID_DESTINATION | a subscription url that is not a public https:// host on port 443 |
| 401 | INVALID_KEY | missing, unknown, revoked or expired key — one answer for all |
| 403 | SCOPE_MISSING | the key lacks the scope |
| 403 | PLAN_FEATURE_UNAVAILABLE | the account is below GROWTH |
| 404 | NOT_FOUND, MESSAGE_NOT_FOUND, HOOK_NOT_FOUND, AGENT_NOT_FOUND | nothing with that id in your account |
| 409 | HOOK_LIMIT_REACHED | a 21st API subscription on the agent |
| 422 | NO_EVENTS, INVALID_EVENT, INVALID_EXTERNAL_ID | a subscription with no events, an event that cannot be subscribed through the API, or an externalSubscriptionId over 128 characters |
| 429 | RATE_LIMITED | more than 60 requests a minute for the key (Retry-After: 1), or the edge limit per key / per IP (Retry-After: 60); wait that many seconds |
| 503 | UPSTREAM_UNAVAILABLE, AUTH_UNAVAILABLE | SynapsAI could not answer in time (a timeout or a store that did not answer, e.g. a message status that cannot be read is a 503, never a 404) — see Retrying a 503 |
| 503 | SERVICE_DISABLED | the service is switched off for now; retry later |
| 500 | INTERNAL_ERROR | unexpected; nothing reveals more |
Retrying a 503. GET requests and POST /v1/agents/{agentKey}/messages sent with an idempotencyKey are safe to
repeat as they are: within 24 h, a repeated message with the same key and body returns the first messageId. The other writes —
POST /v1/contacts:upsert, POST /v1/agents/{agentKey}/hooks, DELETE /v1/hooks/{id} — may have been applied
before the 503 reached you: read first (the contact, GET /v1/agents/{agentKey}/hooks), then retry only what is
missing. The n8n node and the Make app retry the safe calls only.
Send a message
Section titled “Send a message”POST /v1/agents/shop-bot/messagesAuthorization: Bearer sk_live_…Content-Type: application/json
{ "externalUserId": "crm-123", "text": "Do you have the 4 mm drill in stock?", "contact": { "name": "Ana", "phone": "+5071234567", "email": "ana@example.com" }, "metadata": { "dealId": "D-9" }, "callbackUrl": "https://n8n.example.com/webhook-waiting/42/…", "idempotencyKey": "order-7781-q1"}| Field | Rules |
|---|---|
externalUserId | required, 1–128 characters — your stable id for the person; the same id continues the conversation |
text | required, 1–4000 characters |
contactId | optional, ≤ 64 characters; attaches the conversation to an existing contact of your account. Needs contacts:write too |
contact | optional name (≤ 256), phone (≤ 32, E.164), email (≤ 254), strings only: they fill blank fields of this conversation’s contact. Contacts are never merged automatically |
metadata | optional object of ≤ 20 scalar values (string, number, boolean), ≤ 2 KB; echoed back in the reply webhook, never shown to the agent |
callbackUrl | optional; https://, port 443, ≤ 2048 characters, a host name (no IP address, no user:password). When set, the reply goes only there; otherwise to your message.replied subscriptions |
idempotencyKey | optional, 1–128 characters; kept 24 h. After 24 h the same key starts a new message |
The whole body may be at most 32 KB. Attachments are not accepted yet.
Answers:
202 {"messageId": "msg_…", "status": "QUEUED"}— accepted. AmessageIdis random (msg_+ 22 characters); keep it to correlate the reply.200 {"messageId": "msg_…", "status": "DUPLICATE"}— the sameidempotencyKeywith the same body within 24 h; the firstmessageId, no second turn.
Errors
Section titled “Errors”| Status | code | When |
|---|---|---|
| 400 | INVALID_JSON | the body is not a JSON object |
| 400 | EXTERNAL_USER_REQUIRED / INVALID_EXTERNAL_USER_ID | externalUserId missing / not a 1–128 character string |
| 400 | TEXT_REQUIRED / TEXT_TOO_LONG | text missing / over 4000 characters |
| 400 | INVALID_CONTACT, INVALID_CONTACT_ID, INVALID_METADATA, INVALID_IDEMPOTENCY_KEY | that field breaks the rules above |
| 400 | INVALID_CALLBACK_URL | not https on 443, an IP address, credentials in the URL, or too long |
| 400 | ATTACHMENTS_UNSUPPORTED | the body carries attachments |
| 401 | INVALID_KEY | missing, unknown, revoked or expired key, or a header that is not exactly Bearer sk_live_… |
| 403 | SCOPE_MISSING | no messages:write, or contactId without contacts:write |
| 403 | AGENT_FORBIDDEN | the agent is not yours, or its API channel is off (same answer for both) |
| 403 | PLAN_FEATURE_UNAVAILABLE | the account is below GROWTH |
| 404 | NOT_FOUND | wrong path |
| 405 | METHOD_NOT_ALLOWED | anything but POST |
| 409 | IDEMPOTENCY_MISMATCH | the same idempotencyKey with a different body within 24 h |
| 413 | PAYLOAD_TOO_LARGE | body over 32 KB |
| 429 | LOOP_GUARD | more than 20 messages a minute for this externalUserId (that user only) |
| 429 | RATE_LIMITED | more than 600 messages a minute for the whole account |
| 429 | RATE_LIMITED | the edge limit per key (120/min) or per IP — with Retry-After: 60 |
| 503 | AUTH_UNAVAILABLE, PLAN_UNAVAILABLE, UPSTREAM_UNAVAILABLE, ROUTING_UNAVAILABLE, INGRESS_STORE_UNAVAILABLE, QUEUE_UNAVAILABLE, CONFIG_UNAVAILABLE | a dependency did not answer; nothing was queued — retry with the same idempotencyKey |
| 500 | INTERNAL | unexpected |
A revoked key stops new calls at once; a turn already queued still completes and its reply is delivered.
Message status
Section titled “Message status”GET /v1/messages/{messageId} →
{ "messageId": "msg_4kQ2v9XbT1nE7sLp0aZc3R", "agentKey": "shop-bot", "conversationId": "6713f0c2a1b2c3d4e5f60718", "status": "REPLIED", "reply": { "text": "Yes, we have 12 in stock.", "components": [], "repliedAt": "2026-10-28T12:00:04Z" }, "errorCode": null}status is QUEUED, REPLIED or FAILED (errorCode says why, e.g. INSUFFICIENT_CREDITS).
Create or update a contact
Section titled “Create or update a contact”POST /v1/contacts:upsert{ "phone": "+5071234567", "email": "ana@example.com", "name": "Ana", "mode": "fill" }SynapsAI looks for a contact of your account in this order — contactId, phone (E.164), email (lower-case),
externalUserId (API channel) — and the first hit wins. Nothing found: a contact is created. mode is fill
(default: only blank fields are set) or overwrite.
→ 200 {"contactId": "…", "created": false, "matchedBy": "PHONE"} (CONTACT_ID, PHONE, EMAIL,
EXTERNAL_USER_ID, or NONE when created). 400 IDENTIFIER_REQUIRED when none of contactId, phone, email,
externalUserId is given.
This is the only way two contacts become one, and it happens only when you call it.
Subscriptions (triggers)
Section titled “Subscriptions (triggers)”POST /v1/agents/shop-bot/hooks{ "url": "https://hooks.example.com/synapsai", "events": ["message.replied", "lead.captured"], "includes": { "contact": true }, "externalSubscriptionId": "my-flow-17" }→ 201 {"id": "…", "url": "…", "events": [...], "signingSecret": "whsec_…", …} — the secret is shown once; keep
it to verify deliveries. GET /v1/agents/{agentKey}/hooks lists them (items).
- Events:
message.replied,message.operator,lead.captured, the campaign eventscampaign.message.delivered,campaign.message.read,campaign.message.failedandcampaign.completed(scopecampaigns:read), and the lifecycle eventsconversation.started,conversation.ended,handoff.requested,operator.joined,operator.left,handoff.ended(conversation.idleis console-only:422 INVALID_EVENT). Amessage.repliedsubscription also receivesmessage.failed. includesis an object of sections, none by default. The key must hold the scope that reads the same data through the API (403 SCOPE_MISSINGotherwise):contact,operatorEmail→contacts:read;intel,lead,handoff,operator,messages,transcript,memory→conversations:read;- the
message.repliedandmessage.operatorevents (they carry conversation text) →conversations:read; - the
lead.capturedevent →leads:read; - the
campaign.*events →campaigns:read.
409 HOOK_LIMIT_REACHEDpast 20 API subscriptions per agent.- Subscriptions follow the lifecycle hooks rules: retries, quarantine after
repeated failures,
hook.pingto come back. If the account moves below GROWTH they are paused.
Reply webhooks
Section titled “Reply webhooks”Every delivery is a signed POST with the lifecycle-hook headers:
X-Synapsai-Event, X-Synapsai-Webhook-Id (the same on every retry — deduplicate on it), X-Synapsai-Timestamp
and X-Synapsai-Signature. The secret is the agent’s API signing secret for a callbackUrl, and the
subscription’s secret for a subscription. See verifying signatures. The Make app
cannot check the signature (Make has no raw body), so its triggers treat each delivery as a notification and re-read
the event by id with your key — a forged POST to a Make webhook URL cannot inject data.
{ "id": "evt_5d1c9e0b7a2f4c3e8b6a1d0f9e8c7b6a", "type": "message.replied", "apiVersion": "2026-10", "createdAt": "2026-10-28T12:00:04Z", "account": { "id": "acc_0123" }, "agent": { "key": "shop-bot", "name": "Shop assistant" }, "conversation": { "id": "6713f0c2a1b2c3d4e5f60718", "channel": "API", "language": "en" }, "message": { "id": "msg_4kQ2v9XbT1nE7sLp0aZc3R", "externalUserId": "crm-123", "contactId": "c_91", "text": "Yes, we have 12 in stock. Want me to reserve one?", "components": [ { "type": "buttons", "buttons": [ { "label": "Reserve", "value": "reserve" } ] } ], "metadata": { "dealId": "D-9" }, "repliedAt": "2026-10-28T12:00:04Z" }}message.idis themessageIdyour request got back — correlate on it.conversation.idis thechatId.componentsare the agent’s rich answers (buttons, carousels, lists, charts) in SynapsAI’s channel-neutral form;textis always a plain-text version you can send anywhere.message.operatorhas the samemessageblock plusoperator: {name}: a console operator answered.message.failedhasmessage: {id, externalUserId, contactId, metadata}anderror: {code, message}.lead.capturedcarrieslead—{score, interest, dealTitle, budget: {amount, currency}, timeframe, contactDeclined}, with no name, e-mail or phone. To send the person to a CRM, subscribe withincludes: {"contact": true}(needscontacts:read) and readcontact: {id, name, email, phone, channelUserId, tags}.- The delivery
id(=X-Synapsai-Webhook-Id) isevt_+ 32 hex characters, stable across retries.