Skip to content
Flowgento

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).

ScopeGrants
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":[...]}}
CodeHTTPWhen
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

EventPayload 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.