Reference
API documentation
Version 1 (B4) · 2026-09-06
The Flowgento REST API lets server-side code send WhatsApp messages, manage contacts, list conversations, trigger broadcasts, and create and track ecommerce orders on behalf of a workspace. All requests are HTTPS, JSON, and authenticated by a per-tenant API key. Outbound webhooks deliver real-time events to your endpoint.
Base URL
https://api.flowgento.com/api/v1 Authentication
Every request to /api/v1 must include an API key as a Bearer token. Both header forms are accepted; if both are present, X-API-Key wins.
Authorization: Bearer fg_live_<43 random chars>
# or
X-API-Key: fg_live_<43 random chars> Keys are issued from Settings → API in the tenant app (OWNER / ADMIN only, requires Vistaar plan or higher). The full plaintext key is shown once at creation and never again — Flowgento stores only an argon2id hash. Lost keys must be revoked + recreated.
Sanity check
curl -H "Authorization: Bearer $KEY" \
https://api.flowgento.com/api/v1/ping
# 200 OK
{"ok":true,"tenantId":"...","timestamp":"..."} Scopes
Each API key carries a set of scopes that gate which endpoints it may call. Pick scopes when you create the key in Settings → API. A request to an endpoint whose scope is missing returns 403 API_SCOPE_REQUIRED. Scopes are explicit: a key can call only the endpoints its listed scopes cover, and every key must carry at least one scope (an empty list is refused with 400 SCOPES_REQUIRED). Keys created before scopes shipped were expanded to the full scope set once, so their access is unchanged — edit them to apply least-privilege.
Number scoping
A key can additionally be scoped to a single WhatsApp number (pick the number in the create-key dialog; the default is All numbers). A number-scoped key only sees and acts on that number's conversations — cross-number access returns 403 API_KEY_NUMBER_SCOPE — and POST /messages automatically sends from the scoped number, so from can be omitted. With an All numbers key on a workspace that has several connected numbers, pass from (the sender's phoneNumberId or display number) when messaging a brand-new contact; existing threads reply on their own number automatically. Existing keys are unaffected (they remain All numbers).
| Scope | Grants |
|---|---|
| messages:write | Send text / template messages on behalf of the workspace. |
| contacts:read | List or fetch contact records. |
| contacts:write | Create contacts and assign tags. |
| conversations:read | List conversations and read message history. |
| broadcasts:write | Create + schedule broadcasts. |
| webhooks:manage | Reserved for future endpoint-management calls (today only the SPA can edit endpoints). |
| templates:read | List approved templates and their send shapes (GET /templates). Keys created before this scope existed need re-editing to add it; legacy all-scope keys have it automatically. |
| orders:read | List and fetch ecommerce orders (requires a plan with ecommerce). |
| orders:write | Create orders and update order status (requires a plan with ecommerce). |
Rate limits
120 requests per minute per workspace, shared across every key in that workspace. The cap window resets on a sliding 60-second window from your first request. Every response includes:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 1707823840 # epoch seconds when the window resets
# Modern RFC draft-7 equivalents are also emitted:
RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset
When you exceed the cap you get 429 RATE_LIMITED plus a Retry-After: <seconds> header. Back off accordingly — retrying immediately will simply use up your next slot.
Error envelope
All 4xx and 5xx responses use a consistent shape. The HTTP status, the machine-readable code, and the human-readable message are all stable; treat code as your branching key.
{"error":{"code":"VALIDATION_ERROR","message":"Request validation failed","details":[...]}} | Code | HTTP | When |
|---|---|---|
| API_KEY_REQUIRED | 401 | No Authorization: Bearer and no X-API-Key header. |
| API_KEY_INVALID | 401 | Key format is wrong, prefix is unknown, or the argon2 hash check failed. |
| API_KEY_REVOKED | 401 | The key was revoked in Settings → API. |
| API_SCOPE_REQUIRED | 403 | Auth succeeded but the key lacks the scope the endpoint demands. |
| API_KEY_NUMBER_SCOPE | 403 | The key is scoped to one WhatsApp number and the target resource belongs to a different number. |
| FROM_NUMBER_REQUIRED | 400 | Sending a first message to a new contact with an all-numbers key while several numbers are connected — pass "from" or use a number-scoped key. |
| BAD_FROM_NUMBER | 400 | "from" does not match any connected number (use the phoneNumberId or the display number). |
| VALIDATION_ERROR | 400 | Body or query failed JSON-schema / zod validation. Details include the offending path(s). |
| RATE_LIMITED | 429 | Per-tenant rate cap exceeded. See Retry-After (seconds) and the X-RateLimit-* headers. |
| PLAN_CAPABILITY_REQUIRED | 403 | Tenant plan lacks the needed capability: API access needs Vistaar or higher; /orders endpoints additionally need ecommerce (any paid plan). |
| CONTACT_DUPLICATE | 409 | POST /contacts with a phone that already exists in this workspace. |
| CONTACT_NOT_FOUND | 404 | GET /contacts/:id for an id that does not belong to this tenant. |
| CONVERSATION_NOT_FOUND | 404 | GET /conversations/:id/messages for an unknown conversation. |
| TEMPLATE_NOT_FOUND | 404 | POST /messages (type=template) with a name that is not APPROVED in this workspace. |
| TEMPLATE_HEADER_REQUIRED | 400 | The template was approved with an image/video/document header, no template.header.url was supplied, and no approved sample media exists to fall back to. |
| TEMPLATE_HEADER_TYPE_MISMATCH | 400 | template.header.type does not match the header format the template was approved with (e.g. header.type "image" on a document-header template). |
| TEMPLATE_HEADER_TEXT_REQUIRED | 400 | The template has a TEXT header containing a {{1}} variable but template.header.text was not supplied. |
| TEMPLATE_VARIABLES_MISSING | 400 | The template body declares more {{n}} variables than the request supplied. The message lists the missing slots. |
| TEMPLATE_VARIABLES_EXTRA | 400 | The request supplied variable keys beyond the template’s declared count — Meta rejects mismatched counts (#132000), so we reject at the edge. |
| INVALID_COMPONENTS | 400 | template.components (raw escape hatch) is not an array of Meta component objects with a string "type". |
| SERVICE_WINDOW_CLOSED | 400 | POST /messages (type=text) when the 24h customer-initiated window is closed. Send an approved template to re-engage. |
| WABA_NOT_CONFIGURED | 400 | No live WhatsApp Business Account on the workspace — connect one in Settings → WhatsApp first. |
Endpoints
POST /messages
Scope: messages:write
Send a text or template message. The conversation thread is found-or-created on the workspace's primary live WABA — the resulting message appears in the SPA inbox UI exactly as if a human sent it. Text sends are subject to the 24h customer-initiated window; template sends are allowed any time.
# Text (window must be open)
curl -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"to":"+919876543210","type":"text","text":"Hello!"}' \
https://api.flowgento.com/api/v1/messages
# Template (body variables only)
curl -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"to":"+919876543210","type":"template","template":{"name":"welcome_message","language":"en_US","variables":{"1":"Alex"}}}' \
https://api.flowgento.com/api/v1/messages
# 201 Created
{"message":{"id":"cmq...","conversationId":"cmq...","status":"SENT","text":"Hello!","metaMessageId":"wamid..."}} Template media & text headers
Templates approved with an image, video or document header need a header parameter on every send — pass template.header with a public https URL. Validation happens at our edge against your synced template (header format, body variable count), so mistakes return descriptive 400s instead of opaque Meta #132012/#132000 errors. When header is omitted we fall back to the approved sample media. Advanced integrations can bypass the mapping with a raw Meta components array.
# Template approved with an IMAGE header — pass a public https URL
curl -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"to":"+919876543210","type":"template","template":{
"name":"diwali_offer","language":"en_US",
"header":{"type":"image","url":"https://cdn.example.com/offer.jpg"},
"variables":{"1":"Alex","2":"20%"}}}' \
https://api.flowgento.com/api/v1/messages
# VIDEO header: header.type "video". DOCUMENT header: header.type "document"
# + optional header.filename (shown to the recipient):
# "header":{"type":"document","url":"https://cdn.example.com/price-list.pdf","filename":"Price list.pdf"}
# TEXT header with a {{1}} variable:
# "header":{"type":"text","text":"Diwali"}
#
# Omitting header on a media-header template falls back to the APPROVED
# SAMPLE media. If no sample exists you get 400 TEMPLATE_HEADER_REQUIRED:
{"error":{"code":"TEMPLATE_HEADER_REQUIRED","message":"This template requires an image header — provide template.header.url (public https link)"}}
# Power-user escape hatch — supply Meta's raw components array verbatim
# (overrides header/variables mapping; validated minimally):
curl -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"to":"+919876543210","type":"template","template":{
"name":"diwali_offer","language":"en_US",
"components":[
{"type":"header","parameters":[{"type":"image","image":{"link":"https://cdn.example.com/offer.jpg"}}]},
{"type":"body","parameters":[{"type":"text","text":"Alex"},{"type":"text","text":"20%"}]}
]}}' \
https://api.flowgento.com/api/v1/messages GET /templates · GET /templates/:name
Scope: templates:read
Introspect approved templates before sending: header type (none / text / image / video / document), body text + variable count, button summary, and a ready-to-copy example payload. Optional ?name= filter. Served from the synced catalog — lastSyncedAt shows freshness; no live Meta call per request.
# Discover approved templates + their exact send shapes
curl -H "Authorization: Bearer $KEY" "https://api.flowgento.com/api/v1/templates?name=diwali_offer"
# 200 OK — served from the synced catalog (see lastSyncedAt), never a live Meta call
{"templates":[{
"name":"diwali_offer","language":"en_US","status":"APPROVED","category":"MARKETING",
"header":{"type":"image","exampleUrl":"https://..."},
"body":{"text":"Hi {{1}}, enjoy {{2}} off...","variableCount":2},
"buttons":[{"type":"QUICK_REPLY","text":"Show me"}],
"example":{"template":{"name":"diwali_offer","language":"en_US","header":{"type":"image","url":"https://example.com/media"},"variables":{"1":"value 1","2":"value 2"}}},
"lastSyncedAt":"2026-08-28T..."}]}
# GET https://api.flowgento.com/api/v1/templates/:name returns the same shape filtered to one
# name (404 TEMPLATE_NOT_FOUND if unknown). Number-scoped keys see only
# templates approved on their number. GET /contacts
Scope: contacts:read
List or search contacts. Returns an envelope { data, page, pageSize, total }. Query params: q (search name / phone / email), tag (comma-separated tag ids — OR semantics), page, pageSize (max 100).
curl -H "Authorization: Bearer $KEY" \
"https://api.flowgento.com/api/v1/contacts?page=1&pageSize=20&q=alex" POST /contacts
Scope: contacts:write
Create a contact. Phone is normalised to E.164. Returns 409 CONTACT_DUPLICATE if a contact with the same phone already exists in this workspace.
curl -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"phone":"+919876543210","name":"Alex","email":"alex@example.com","tags":["tag_id_1","tag_id_2"]}' \
https://api.flowgento.com/api/v1/contacts GET /contacts/:id
Scope: contacts:read
curl -H "Authorization: Bearer $KEY" https://api.flowgento.com/api/v1/contacts/cmq...
# 200 OK { "contact": {...} }
# 404 CONTACT_NOT_FOUND if the id does not belong to your workspace POST /contacts/:id/tags
Scope: contacts:write
curl -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"tagIds":["tag_id_1","tag_id_2"]}' \
https://api.flowgento.com/api/v1/contacts/cmq.../tags GET /conversations
Scope: conversations:read
List conversations. Query: status (OPEN / RESOLVED / SNOOZED / ALL, default ALL), page, pageSize. Envelope shape mirrors /contacts.
curl -H "Authorization: Bearer $KEY" \
"https://api.flowgento.com/api/v1/conversations?status=OPEN&page=1&pageSize=20" GET /conversations/:id/messages
Scope: conversations:read
Paginated message history, newest first. Use ?before=<ISO timestamp> for keyset pagination beyond the first page.
curl -H "Authorization: Bearer $KEY" \
"https://api.flowgento.com/api/v1/conversations/cmq.../messages?pageSize=50&before=2026-06-08T00:00:00Z" POST /broadcasts
Scope: broadcasts:write
Create and immediately schedule a broadcast. Provide either an inline recipients list or a segment spec (multi-criteria from the SPA wizard) — exactly one. The same backend pipeline that powers the inbox wizard runs the send.
curl -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"name":"Summer launch","templateId":"cmq...","variablesMapping":{"1":"Alex"},"recipients":[{"phone":"+919876543210"}]}' \
https://api.flowgento.com/api/v1/broadcasts POST /orders
Scope: orders:write · Plan: ecommerce (any paid plan)
Create an ecommerce order. Address the customer by contactId or phone (exactly one; an unknown phone returns 404 CONTACT_NOT_FOUND — create the contact first). items reference products from your catalog (1–100 lines); all money fields are in paise. paymentType is COD or PREPAID; for prepaid orders, generatePaymentLink: true creates a payment link through your connected gateway. sendSummary (default false on the API) sends the order summary to the customer on WhatsApp. Pass an Idempotency-Key header to make retries safe.
curl -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-H 'Idempotency-Key: order-20260905-0001' \
-d '{
"phone": "+919876543210",
"items": [{"productId": "cmq...", "quantity": 2}],
"paymentType": "COD",
"shippingCharge": 4900,
"deliveryAddress": {
"name": "Alex", "phone": "+919876543210",
"address1": "12 MG Road", "city": "Bengaluru",
"state": "Karnataka", "pincode": "560001"
},
"sendSummary": true
}' \
https://api.flowgento.com/api/v1/orders
# 201 Created {"order": {...}, "reused": false}
# Replaying the same Idempotency-Key returns 200 with "reused": true GET /orders · GET /orders/:id
Scope: orders:read · Plan: ecommerce (any paid plan)
List orders newest-first (standard envelope) or fetch one. Query: status (NEW / CONFIRMED / PACKED / SHIPPED / DELIVERED / CANCELLED / RETURNED), search (order number, contact name, or phone), contactId, page, pageSize. Number-scoped keys see only their own number's orders.
curl -H "Authorization: Bearer $KEY" \
"https://api.flowgento.com/api/v1/orders?status=NEW&page=1&pageSize=20"
# GET https://api.flowgento.com/api/v1/orders/cmq... → 200 {"order": {...}} PATCH /orders/:id/status
Scope: orders:write · Plan: ecommerce (any paid plan)
Move an order through its lifecycle. Status transitions notify the customer on WhatsApp from the order's number, so a number-scoped key must own that number. cancelReason is required when cancelling.
curl -X PATCH -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"status":"SHIPPED"}' \
https://api.flowgento.com/api/v1/orders/cmq.../status
# Cancelling requires a reason:
# {"status":"CANCELLED","cancelReason":"customer request"}
# 200 OK {"order": {...}} — allowedNextStatuses on the order tells you which
# transitions are valid from its current state. Webhooks
Subscribe to workspace events from Settings → Webhooks. Each endpoint POSTs a signed JSON envelope to your URL on every matching event, retries up to 5 times with exponential backoff (~1m, 2m, 4m, 8m, 16m) on non-2xx responses, and times out at 10 seconds per attempt.
Number scoping. An endpoint can be scoped to a single WhatsApp number (pick it in the create/edit dialog; default is All numbers). Scoped endpoints only receive events that belong to that number; tenant-level events with no number context (e.g. a manually created contact) are delivered to every subscribed endpoint. Every payload that carries a wabaId also includes a number object (wabaId, displayPhoneNumber, businessName) so multi-number consumers can attribute events without extra lookups — this field is additive and non-breaking. Existing endpoints remain All numbers.
Delivery headers
POST https://your.receiver/...
Content-Type: application/json
X-Flowgento-Event: message.received
X-Flowgento-Delivery: <uuid>
X-Flowgento-Signature: sha256=<hex hmac-sha256 of the raw body>
User-Agent: Flowgento-Webhook/1.0 Envelope
{"event":"message.received","deliveryId":"<uuid>","emittedAt":"2026-06-08T...","payload":{ ... per-event fields ..., "number": { "wabaId": "<id>", "displayPhoneNumber": "+91 79788 39679", "businessName": "..." } }} Events
| Event | Payload fields |
|---|---|
| message.received | messageId, conversationId, contactId, contactPhone, contactName, wabaId, text, contentType, hasAttachments, timestamp |
| message.status | messageId, conversationId, wabaId, status (SENT|DELIVERED|READ|FAILED), errorCode, errorMessage, updatedAt |
| conversation.created | conversationId, wabaId, contactId, contactPhone, contactName, createdAt |
| conversation.resolved | conversationId, wabaId, contactId, contactPhone, resolvedAt, resolvedByUserId |
| contact.created | contactId, phone, name, email, source, createdAt |
| contact.tagged | contactId, tagIds[], tagNames[] |
Verifying the signature (Node)
import crypto from 'node:crypto';
function verify(rawBody, signatureHeader, endpointSecret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', endpointSecret)
.update(rawBody) // raw, unparsed bytes — not JSON.parse'd
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected),
);
} Reject the request and return any non-2xx if the signature doesn't match. Flowgento will retry, so a brief receiver outage won't lose events. Each attempt is logged on the endpoint's "Recent deliveries" panel in Settings → Webhooks.
Need help?
Email partnerships@flowgento.com with the workspace name, the key prefix (the fg_live_xxxxxxxx portion, never the full key), and a short description of what you're building. We respond within one business day.