REST API v1 — Documentation
Fliok Developer API
Build custom integrations with Fliok using our REST API. Send messages and media, read conversations, contacts and templates, answer Instagram messages and comments, schedule Instagram posts, ask for payments with WhatsApp Pay, and receive signed real-time webhooks — all programmatically.
Base URLhttps://fliok.com/api/v1
Quick Start in 3 Steps
Get an API key
On the Pro plan, generate a key from the API keys page. Give it an expiry if you want it to lapse.
Make Your First Call
Use our REST API with any language. Full cURL, Python, Node.js, PHP examples included.
Set Up Webhooks
Register an endpoint and receive signed events for incoming messages and delivery status changes.
Authentication
The REST API and webhooks are included on the Pro and Enterprise plans only. There is no free API tier and no sandbox: a key on a workspace that is not on Pro or Enterprise answers 402 plan_feature_unavailable. Your first upgrade starts with a 7-day trial (a payment method is needed), so you can try the API before the first payment.
Fliok API uses Bearer Token authentication. Include your API key in the Authorization header for every request. A key belongs to one workspace and carries one role for all of it — admin, agent or viewer, the same as a teammate — and can be revoked or given an expiry date from the dashboard. Keys are not limited to particular resources, so give each integration its own key with the lowest role that works. A rejected key returns 401 with a specific reason: api_key_expired, api_key_revoked or api_key_invalid, so you can tell a lapsed key from a missing one.
https://fliok.com/api/v1v1stableAuthorization: Bearer wa_xxxxxxxxxxxxxxxxxxxxxxxxNever expose your API key in client-side code or public repositories. Use environment variables and rotate keys if compromised.
Send Your First Message
# Send a text message (needs an open 24h window). The Idempotency-Key makes a retry
# safe: the same key within 24 hours answers with the first result instead of sending twice.
curl -X POST https://fliok.com/api/v1/send \
-H "Authorization: Bearer wa_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-12345-shipped" \
-d '{
"to": "+919876543210",
"type": "text",
"text": "Hello! Your order #12345 has been shipped."
}'
# Send a template (the only way to open a NEW conversation)
curl -X POST https://fliok.com/api/v1/send \
-H "Authorization: Bearer wa_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+919876543210",
"type": "template",
"payload": {
"type": "template",
"template": {
"name": "order_confirmation",
"language": { "code": "en" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Rahul" },
{ "type": "text", "text": "ORD-12345" }
]
}
]
}
}
}'
# The same template, letting Fliok build the components from the approved
# template: variables in order, and the header's media when it has one
curl -X POST https://fliok.com/api/v1/send \
-H "Authorization: Bearer wa_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+919876543210",
"type": "template",
"payload": {
"type": "template",
"template": {
"name": "order_confirmation",
"language": { "code": "en" },
"variables": ["Rahul", "ORD-12345"]
}
}
}'
# Submit a template to Meta for approval (language is a plain code here —
# only a send uses { "code": … }). Every variable needs an example.
curl -X POST https://fliok.com/api/v1/templates \
-H "Authorization: Bearer wa_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "order_update",
"language": "en",
"category": "UTILITY",
"components": [
{ "type": "BODY", "text": "Hi {{1}}, your order {{2}} has shipped.",
"example": { "body_text": [["Rahul", "ORD-12345"]] } },
{ "type": "BUTTONS", "buttons": [
{ "type": "URL", "text": "Track order", "url": "https://example.com/track" } ] }
]
}'
# Upload a document, then send it by media id
curl -X POST https://fliok.com/api/v1/media \
-H "Authorization: Bearer wa_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"fileBase64": "JVBERi0xLjQK...",
"mimeType": "application/pdf",
"filename": "invoice.pdf"
}'
# -> { "id": "1234567890" } (reusable for ~30 days)
# Check what happened to a message (by our id or by the wamid webhooks carry)
curl https://fliok.com/api/v1/messages/wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgS \
-H "Authorization: Bearer wa_YOUR_API_KEY"{
"ok": true,
"messageId": "4f6c1e2a-91d3-4a77-b0c2-5f1e8d9a3b44"
}
// The send is queued, not yet delivered. Poll GET /messages/{id}
// or subscribe to the message.status webhook for the outcome.Code Examples
Use any HTTP client in your language. Here are ready-to-use examples for the most popular languages.
const API_KEY = 'wa_your_api_key';
const BASE = 'https://fliok.com/api/v1';
// Send a text message (requires an open 24h window)
const res = await fetch(BASE + '/send', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + API_KEY,
'Content-Type': 'application/json',
'User-Agent': 'my-app/1.0',
// Safe to retry: the same key never sends twice (24 hours)
'Idempotency-Key': 'order-12345-shipped',
},
body: JSON.stringify({
to: '+919876543210',
type: 'text',
text: 'Hello from Fliok!',
}),
});
const data = await res.json();
console.log('Message ID:', data.messageId);import base64, requests
API_KEY = "wa_your_api_key"
BASE = "https://fliok.com/api/v1"
# Name your client: some default library agents (Python's urllib) are refused at the edge.
HEAD = {"Authorization": f"Bearer {API_KEY}", "User-Agent": "my-app/1.0"}
# 1. Upload a PDF -> Meta media id
with open("invoice.pdf", "rb") as f:
up = requests.post(f"{BASE}/media", headers=HEAD, json={
"fileBase64": base64.b64encode(f.read()).decode(),
"mimeType": "application/pdf",
"filename": "invoice.pdf",
}).json()
# 2. Send it
res = requests.post(f"{BASE}/send", headers=HEAD, json={
"to": "+919876543210",
"type": "document",
"payload": {"type": "document",
"document": {"id": up["id"], "filename": "invoice.pdf"}},
})
print("Message ID:", res.json()["messageId"])<?php
$apiKey = 'wa_your_api_key';
$base = 'https://fliok.com/api/v1';
// Send a template (works outside the 24h window)
$ch = curl_init("$base/send");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $apiKey",
"Content-Type: application/json",
"User-Agent: my-app/1.0",
],
CURLOPT_POSTFIELDS => json_encode([
'to' => '+919876543210',
'type' => 'template',
'payload' => [
'type' => 'template',
'template' => [
'name' => 'order_confirmation',
'language' => ['code' => 'en'],
'components' => [[
'type' => 'body',
'parameters' => [
['type' => 'text', 'text' => 'Rahul'],
['type' => 'text', 'text' => 'ORD-12345'],
],
]],
],
],
]),
]);
$response = json_decode(curl_exec($ch));
echo "Message ID: " . $response->messageId;Works with any language that supports HTTP requests — Ruby, Go, Java, .NET, Dart, and more.
Send a User-Agent header that names your app, such as my-app/1.0. fliok.com is behind Cloudflare, which refuses some default library agents before the request reaches the API — Python’s built-in urllib gets “error 1010”. cURL and Python’s requests work as they are; with any other client, set one.
API Endpoints
Paths are relative to https://fliok.com/api/v1. marks an endpoint that needs your API key. The 13 Instagram endpoints and the WhatsApp Pay ones answer for a workspace once the feature is set up for it.
Sending
2- POST
/sendSend a text, template, media or interactive messageNeeds an API key - POST
/mediaUpload a document/image to Meta and get a reusable media idNeeds an API key
Reading
8- GET
/meWho is calling: your workspace, and the key’s label, role and expiry (never the key itself). The connection test for Zapier, Make and PabblyNeeds an API key - GET
/messagesList messages, cursor-paginated. Filter by conversationId and statusNeeds an API key - GET
/messages/{id}Get one message and its delivery status — by our id or by wamidNeeds an API key - GET
/conversationsList WhatsApp conversations with contact info and window expiryNeeds an API key - GET
/contactsList contacts, cursor-paginated. Filter with ?search= (any part of a name or number), ?phone= (one exact number); ?channel=instagram for Instagram customersNeeds an API key - GET
/phone-numbersYour WhatsApp numbers — the ids phoneNumberId refers toNeeds an API key - GET
/templatesList your WhatsApp templates and their Meta approval statusNeeds an API key - POST
/templatesSubmit a new template to Meta for approvalNeeds an API key
Contacts
4- POST
/contactsCreate a contactNeeds an API key - GET
/contacts/{id}Get one contact with its custom fields — by our id or by phone numberNeeds an API key - PATCH
/contacts/{id}Update a contact. Custom attributes merge rather than replaceNeeds an API key - DELETE
/contacts/{id}Delete a contactNeeds an API key
Broadcasts
4- GET
/broadcastsList campaigns with live sent/delivered/failed countsNeeds an API key - POST
/broadcastsCreate a campaign by tags, contact ids or a number list. Returns how many will actually send after opt-out and the marketing capNeeds an API key - GET
/broadcasts/{id}One campaign with its delivery progressNeeds an API key - DELETE
/broadcasts/{id}Cancel a campaign. Messages already handed to WhatsApp cannot be recalledNeeds an API key
Inbox
4- GET
/conversations/{id}One conversation, including whether the 24-hour free-form window is openNeeds an API key - PATCH
/conversations/{id}Assign, prioritise, resolve or archive a conversationNeeds an API key - GET
/conversations/{id}/notesInternal notes on a conversation — never sent to the customerNeeds an API key - POST
/conversations/{id}/notesAdd an internal noteNeeds an API key
Reporting
1- GET
/analyticsMessage and conversation counts for a window (?days=, max 90). Reports skipped separately from failedNeeds an API key
Invoices
2- GET
/invoicesList issued GST tax invoices, cursor-paginatedNeeds an API key - GET
/invoices/{id}One invoice as JSON, or ?format=html for a printable tax invoiceNeeds an API key
Read-only config
2- GET
/flowsList automation flows and whether each is published (read only)Needs an API key - GET
/quick-repliesList saved canned repliesNeeds an API key
Webhooks
7- GET
/webhooksList your registered webhook endpointsNeeds an API key - POST
/webhooksRegister an endpoint for every event, a list (events) or just one (event). Returns its id and, once, the signing secretNeeds an API key - GET
/webhooks/eventsEvery event you can subscribe to, with its meaning and payload fieldsNeeds an API key - GET
/webhooks/samples/{event}Your latest real deliveries of one event (admin keys), or a documented example when there are none yetNeeds an API key - PATCH
/webhooks/{id}Pause/resume an endpoint or change its subscribed eventsNeeds an API key - POST
/webhooks/{id}/rotateReplace the signing secret; the old one keeps signing alongside it for 24 hoursNeeds an API key - DELETE
/webhooks/{id}Remove a webhook endpoint. Safe to repeat: one already gone answers ok with deleted: falseNeeds an API key
Sales CRM
9- GET
/leadsList leads, page-numbered. Filter by view, a filter JSON or ?q=, sort by any sortable fieldNeeds an API key - POST
/leadsAdd a lead. Idempotent by phone: a known number is recaptured, never duplicated. Stage and owner by name or emailNeeds an API key - GET
/leads/{id}One lead with stage, owner, deal value and custom fields — by our id or by phone numberNeeds an API key - PATCH
/leads/{id}Move stage (with lost reason), reassign, rate, tag or edit fields. Lands on the lead’s timeline and fires automationsNeeds an API key - POST
/leads/{id}/notesAdd a note to a lead’s timeline — never sent to the customerNeeds an API key - GET
/tasksList follow-up tasks with counts per status (open, overdue, today, upcoming, done, cancelled)Needs an API key - POST
/tasksCreate a follow-up on a lead; the assignee defaults to the lead’s ownerNeeds an API key - PATCH
/tasks/{id}Complete (with an outcome), cancel, reschedule or reassign a taskNeeds an API key - GET
/pipelineYour pipeline stages (kind initial, active, won or lost) and lost reasonsNeeds an API key
- GET
/instagram/conversationsInstagram Direct chats, cursor-paginated, with whether the 24-hour reply window is openNeeds an API key - GET
/instagram/conversations/{id}/messagesOne Instagram chat and its messages, newest firstNeeds an API key - POST
/instagram/sendReply in an Instagram chat — only within 24 hours of the customer’s last messageNeeds an API key - POST
/instagram/mediaGive Fliok a JPEG by public https:// URL for a post; returns a mediaIdNeeds an API key - GET
/instagram/postsPosts written in Fliok: drafts, scheduled, published, failed, cancelledNeeds an API key - POST
/instagram/postsSave a draft, schedule a post, or publish now (admin keys) from your mediaIdsNeeds an API key - GET
/instagram/posts/recentThe account’s own published posts on Instagram, live, with likes and commentsNeeds an API key - GET
/instagram/posts/{id}One Fliok post — poll it for published or failedNeeds an API key - POST
/instagram/posts/{id}/cancelCancel a scheduled post before it goes out (admin keys)Needs an API key - GET
/instagram/commentsComments and @mentions, filtered by needs_reply, all, mentions or hiddenNeeds an API key - POST
/instagram/comments/{id}/replyReply in public under a commentNeeds an API key - POST
/instagram/comments/{id}/hideHide a comment from everyone but its author, or show it againNeeds an API key - GET
/instagram/insightsAccount summary for 7, 28 or 90 days: followers, views, reach, engagement, top postsNeeds an API key
WhatsApp Pay
2- POST
/payment-requestsAsk for a payment inside a WhatsApp chat — by conversationId or the customer’s number (to). INR, in paise. Your referenceId makes retries safeNeeds an API key - GET
/payment-requests/{id}Where a payment request stands — by our id or your referenceIdNeeds an API key
Request Bodies
What the most-used endpoints take, field by field. A body that doesn’t match answers 400 naming the field.
POST /send
JSON body. Send an `Idempotency-Key` header (1–255 characters, remembered for 24 hours) and a retry of the same request returns the first answer instead of sending twice; the same key with a different body is refused (422 idempotency_key_reused).
| Field | Type | What it is |
|---|---|---|
torequired | string | The customer’s number in E.164, with or without “+”. A number without a country code takes your sending number’s. For a customer WhatsApp shares no number for, pass the phone we report for them ("wa:<user id>") as it is — or leave `to` out and use `recipient`. |
recipient | string | Instead of `to`: the customer’s WhatsApp user id (the `userId` on webhooks and contacts, like "IN.21483648909127364650"), for a customer WhatsApp shares no phone number for. `to` wins when both are sent. |
typerequired | string | text, template, image, video, audio, document, interactive, reaction, location, sticker or contacts. |
text | string | The message, for type "text" (required then, never empty). Needs an open 24-hour window. |
payload | object | For every other type: Meta’s message object, { "type": "<type>", "<type>": { … } } — e.g. { "type": "document", "document": { "id": "<media id>", "filename": "invoice.pdf" } }. |
payload.template.variables | array | Template shortcut: the body’s {{1}}, {{2}}… in order. Fliok builds Meta’s components from the approved template, so you can leave components out. |
payload.template.headerMedia | object | Template shortcut: { "id": "<media id>" } or { "link": "https://…" } (plus "filename" for a document) for an image, video or document header. |
payload.template.headerText · couponCode · offerExpiresAt | various | Template shortcut: a text header’s variables, a coupon-code button’s code, a limited-time offer’s end (ISO time). |
phoneNumberId | uuid | Which of your numbers sends (GET /phone-numbers). Default: your first active number. |
replyTo | string | A wamid to reply to: the message shows as a reply to it. |
Returns 200 { "ok": true, "messageId": "<uuid>" } — queued, not yet delivered. Refusals: 422 outside_24h_window, 422 needs_phone_number (a verification-code template to a customer with no phone number — WhatsApp only delivers those to a number), 409 contact_opted_out, 402 plan_limit_reached (a new number past your contact limit) or quota_exceeded, 400 invalid_template_parameters / template_header_media_required. A refused send writes nothing: no contact, no conversation, no contact.created webhook.
POST /media
JSON body, up to 16 MB of file. Upload once, send the id as many times as you like for about 30 days.
| Field | Type | What it is |
|---|---|---|
fileBase64required | string | The file’s bytes, base64-encoded, without a "data:" prefix. |
mimeType | string | For example application/pdf, image/jpeg, video/mp4. Default application/pdf. |
filename | string | Up to 200 characters. Default document.pdf. |
phoneNumberId | uuid | The number that will send it. Default: your first active number. |
Returns 200 { "id": "<Meta media id>" }
GET /messages · GET /contacts · GET /conversations
Query parameters. Newest first; pass the previous page’s nextCursor as cursor until it comes back null. /messages and /contacts page by creation, so a sync never skips a row; /conversations orders by latest activity, so a chat that gets a message while you page can move — sync with /messages instead.
| Field | Type | What it is |
|---|---|---|
limit | number | 1–100, default 50. |
cursor | uuid | The nextCursor of the page before. Anything else is a 400. |
conversationId | uuid | /messages only: one chat’s messages. |
status | string | /messages only: queued, sent, delivered, read, failed or skipped. |
search | string | /contacts only: part of a number, a name or a WhatsApp username. |
channel | string | /contacts only: "instagram" lists Instagram customers (their e164 is "ig:<id>", not a phone). |
Returns 200 { "messages" | "contacts" | "conversations": [ … ], "nextCursor": "<uuid>" | null }. Every contact carries `userId` (their WhatsApp user id) and `username` (their WhatsApp username), null when WhatsApp has sent us neither. A WhatsApp customer whose number is not shared has "wa:<user id>" as e164.
POST /contacts · PATCH /contacts/{id}
JSON body. POST is keyed by phone: a number you already have is updated (200), a new one is created (201) and counts toward your contact limit. {id} is our id, the phone number, or — for a customer WhatsApp shares no number for — the "wa:<user id>" we report as their e164. Such a contact is created when they write to you, never by POST.
| Field | Type | What it is |
|---|---|---|
phonerequired | string | POST only (or "e164"). E.164, with or without “+”. |
displayName | string | Up to 200 characters. |
tags | string[] | Up to 50 tags, replacing the current ones. |
attributes | object | Custom fields. Merged into what is stored, never replacing it. |
optInStatus | string | unknown, opted_in or opted_out. A contact who sent STOP can’t be opted back in by the API (409). |
optInSource | string | Where the consent came from, up to 120 characters. |
Returns POST → 201 { "contact": { … }, "created": true } (200 with false for a known number). PATCH → 200 { "contact": { … } }
POST /templates
JSON body, submitted straight to Meta. Poll GET /templates for the approval.
| Field | Type | What it is |
|---|---|---|
namerequired | string | Lowercase letters, digits and underscores. |
languagerequired | string | A plain code such as "en" or "en_US" (only a send uses { "code": … }). |
categoryrequired | string | MARKETING, UTILITY or AUTHENTICATION. Meta may change it. |
componentsrequired | object[] | Meta’s component array (HEADER, BODY, FOOTER, BUTTONS). Every variable needs an example. |
wabaConnectionId | uuid | Only when the workspace has more than one WhatsApp Business Account. |
Returns 200 with the stored template, status "pending" until Meta decides.
POST /broadcasts
JSON body, admin keys. Refused before anything is written when this month’s allowance is used up or a template variable is blank. Recipients who have never messaged you (and have no opt-in recorded) have their own rules, the same as in the dashboard: 422 "notice_to_new_recipients" for a utility or authentication template, 409 "new_recipients_unconfirmed" without confirmNewRecipients, 409 "new_recipient_cool_off" after Meta restricted the account, and 409 "follow_up_too_soon" when they were sent a broadcast in the last few days and have not replied. Each answer carries a "message", "newRecipients" and "knownRecipients"; knownOnly is always accepted. A 409 "number_banned" or "sending_blocked" means Meta is refusing messages from the number.
| Field | Type | What it is |
|---|---|---|
namerequired | string | Up to 200 characters. |
templateIdrequired | uuid | An approved template’s id (GET /templates). |
phoneNumberIdrequired | uuid | The number that sends (GET /phone-numbers). |
segmentrequired | object | Who receives it: { "tags": [ … ] } and/or "excludeTags", "optInStatus", "lastSeenWithinDays" — or { "contactIds": [ … ] } for hand-picked contacts (an empty list means nobody). |
variables | object | The template’s parameters as { "components": [ … ] }, the same shape a template send uses. |
sendNow | boolean | Start sending at once. |
scheduledAt | ISO time | Send later instead. With neither, it is saved as a draft. |
confirmNewRecipients | boolean | Your confirmation that the recipients who have never messaged you agreed to get WhatsApp messages from your business. Recorded in the activity log. |
knownOnly | boolean | Send only to recipients who have messaged you or have an opt-in recorded; the others are listed as skipped. |
followUpOf | uuid | The broadcast this one follows up. A follow-up to a marketing broadcast must use a marketing template. |
Returns 201 { "id", "status": "running" | "scheduled" | "draft", "audience": { "matched", "willSend", "suppressed" }, "newRecipients" } — willSend is how many will actually be messaged after opt-outs and Meta’s marketing cap. newRecipients is how many of them have never messaged you; they are sent gradually, a daily allowance per number, so the broadcast can stay "running" for several days.
PATCH /conversations/{id} · POST /conversations/{id}/notes
JSON body, agent keys.
| Field | Type | What it is |
|---|---|---|
state | string | PATCH: open, pending or resolved. |
priority | string | PATCH: low, normal, high or urgent. |
isArchived | boolean | PATCH: archive or unarchive. |
assignedAgentId | uuid | null | PATCH: a team member’s user id, or null to unassign. |
bodyrequired | string | Notes: the note, 1–5,000 characters. Never sent to the customer. |
Returns PATCH → 200 with the conversation. Notes → 201 with the note.
Sales CRM: Leads, Tasks & Pipeline
Every WhatsApp contact is a lead with a stage, an owner, a rating, a deal value and a timeline. The phone number is the lead’s identity, so POST /leads is safe to retry: a number you already have is recaptured (its timeline shows it came in again) instead of duplicated. Anywhere you pass a stage, lost reason or team member you can use its name or email instead of an id, and a lead can be addressed by its phone number. Reads need any key; writes need the agent role. Changes made through the API show on the lead’s timeline and fire the same automations and lead.* / task.* webhooks as changes made in the dashboard.
No code at all? Turn on Lead capture in Settings → Sales pipeline for a private URL that website forms, Zapier, Make, Pabbly and Google Sheets can POST leads to without an API key (/api/leads/capture/{token} — JSON, form or multipart).
Add a lead (or recapture one you already have)
curl -X POST https://fliok.com/api/v1/leads \
-H "Authorization: Bearer wa_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+91 98765 43210",
"name": "Asha Rao",
"email": "asha@example.com",
"source": "Partner portal",
"stage": "Contacted",
"owner": "urvi@yourcompany.com",
"rating": 3,
"dealValue": 250000,
"tags": ["december"],
"attributes": { "city": "Udaipur" },
"note": "300 guests, wants a lake view"
}'// 201 Created (200 with "created": false when the number was already a lead)
{
"created": true,
"lead": {
"id": "4ca441d8-6a95-4b45-9a60-4d5c3ec0f67a",
"name": "Asha Rao",
"phone": "919876543210",
"email": "asha@example.com",
"stage": { "id": "1e43…", "name": "Contacted", "color": "#a78bfa", "kind": "active" },
"lostReason": null,
"rating": 3,
"source": "Partner portal",
"owner": { "id": "c4f6…", "name": "Urvi", "email": "urvi@yourcompany.com" },
"dealValue": 250000,
"paidTotal": 0,
"tags": ["december"],
"attributes": { "city": "Udaipur" },
"nextFollowUpAt": null,
"captureCount": 1,
"createdAt": "2026-09-14T10:00:00.000Z",
"conversationId": null
}
}List leads — a view, a filter, a search
# Won leads, biggest deals first, 25 per page
curl "https://fliok.com/api/v1/leads?view=sys:won&sort=dealValue&dir=desc&pageSize=25" \
-H "Authorization: Bearer wa_YOUR_API_KEY"
# Any filter the dashboard can build (URL-encode it)
curl -G "https://fliok.com/api/v1/leads" \
--data-urlencode 'filter={"match":"all","conditions":[{"field":"source","op":"in","value":["Website"]},{"field":"rating","op":"gte","value":4}]}' \
--data-urlencode "q=asha" \
-H "Authorization: Bearer wa_YOUR_API_KEY"// 200 OK — views: sys:open, sys:all, sys:unassigned, sys:new_today,
// sys:followups, sys:won, sys:lost, or the id of a view shared with your team
{
"leads": [ { "id": "…", "name": "Asha Rao", "stage": { "name": "Won", "kind": "won" }, … } ],
"total": 15,
"page": 1,
"pageSize": 25
}Move a lead, and log what happened
# :id is our lead id OR the phone number
curl -X PATCH https://fliok.com/api/v1/leads/+919876543210 \
-H "Authorization: Bearer wa_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "stage": "Lost", "lostReason": "Too expensive", "owner": "urvi@yourcompany.com", "addTags": ["priced-out"] }'
curl -X POST https://fliok.com/api/v1/leads/+919876543210/notes \
-H "Authorization: Bearer wa_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "body": "Asked for a quote under 2L" }'// PATCH → 200 OK. "changed" lists what actually moved; each is on the lead's timeline
{
"lead": { "id": "…", "stage": { "name": "Lost", "kind": "lost" }, "lostReason": { "id": "…", "label": "Too expensive" }, … },
"changed": ["stage_changed", "owner_changed", "tags_changed"]
}
// POST notes → 201 Created
{ "note": { "id": "510a…", "leadId": "4ca4…", "body": "Asked for a quote under 2L", "createdAt": "2026-09-14T10:05:00.000Z" } }
// An unknown name is refused, never guessed:
// 400 { "error": "invalid", "message": "There is no stage called \"Wonn\". GET /api/v1/pipeline lists the stages." }Follow-up tasks
curl -X POST https://fliok.com/api/v1/tasks \
-H "Authorization: Bearer wa_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "leadId": "+919876543210", "type": "call", "title": "Call back with revised quote",
"dueAt": "2026-09-16T11:00:00+05:30", "priority": "high", "assignee": "tushar@yourcompany.com" }'
curl "https://fliok.com/api/v1/tasks?status=overdue&pageSize=50" -H "Authorization: Bearer wa_YOUR_API_KEY"
curl -X PATCH https://fliok.com/api/v1/tasks/31089580-1422-48cc-becd-85c4e6689352 \
-H "Authorization: Bearer wa_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "done", "outcome": "Sent revised quote" }'// POST → 201 Created; PATCH → 200 OK with the same shape
{
"task": {
"id": "31089580-1422-48cc-becd-85c4e6689352",
"lead": { "id": "4ca4…", "name": "Asha Rao", "phone": "919876543210" },
"type": "call",
"title": "Call back with revised quote",
"dueAt": "2026-09-16T05:30:00.000Z",
"priority": "high",
"status": "pending",
"state": "pending",
"assignee": { "id": "c4f6…", "name": "Tushar" },
"completedAt": null,
"outcome": null
}
}
// GET → { "tasks": [ … ], "total": 3, "page": 1, "pageSize": 50,
// "counts": { "overdue": 3, "today": 1, "upcoming": 7, "done": 40, "cancelled": 2 } }Read the pipeline
curl https://fliok.com/api/v1/pipeline -H "Authorization: Bearer wa_YOUR_API_KEY"// 200 OK — switch on "kind", not on your team's stage names
{
"stages": [
{ "id": "2167…", "name": "New lead", "color": "#53bdeb", "kind": "initial", "position": 0 },
{ "id": "1e43…", "name": "Contacted", "color": "#a78bfa", "kind": "active", "position": 1 },
{ "id": "9b75…", "name": "Won", "color": "#00a884", "kind": "won", "position": 5 },
{ "id": "9502…", "name": "Lost", "color": "#f15c6d", "kind": "lost", "position": 6 }
],
"lostReasons": [ { "id": "97e8…", "label": "Too expensive", "position": 1 } ],
"initialStageId": "2167…"
}Instagram: Messages, Posts, Comments & Insights
Until Instagram is on for your workspace (it is an add-on on the Basic plan and up), every /instagram endpoint answers 403 instagram_disabled. Once it is set up and your Instagram professional account is connected in Integrations, these endpoints run the same code as the dashboard — the same roles, rules and limits. Reads need any key; replying, hiding and drafts need the agent role; scheduling, publishing and cancelling need admin.
- Replies only inside 24 hours. Instagram lets a business answer within 24 hours of the customer’s last message. Instagram’s longer 7-day window is for a person replying in the inbox, never for an integration —
POST /instagram/sendanswers422 outside_24h_windowafter 24 hours. - Posts take photos by URL.
POST /instagram/mediadownloads a JPEG (up to 8 MB) from a public https:// address into your workspace and checks its shape for the post type; private and internal addresses are refused. Reels need a video, which you upload in the dashboard. - Nothing permanent from the API. Comments can be answered in public and hidden (reversibly); deleting a comment, and Instagram ads, stay in the dashboard.
Read your Instagram inbox
curl "https://fliok.com/api/v1/instagram/conversations?limit=20" \
-H "Authorization: Bearer wa_YOUR_API_KEY"
curl "https://fliok.com/api/v1/instagram/conversations/9f1c2b7e-5d1a-4c3e-9b0a-2f6d8e7c1a44/messages?limit=50" \
-H "Authorization: Bearer wa_YOUR_API_KEY"// 200 OK — Instagram has no phone number: the contact is a name and an @username
{
"conversations": [
{
"id": "9f1c2b7e-5d1a-4c3e-9b0a-2f6d8e7c1a44",
"channel": "instagram",
"contact": { "id": "c4f6…", "name": "Riya Sen", "username": "riya.makes" },
"state": "open",
"unreadCount": 2,
"isArchived": false,
"assignedAgentId": null,
"lastMessageAt": "2026-10-05T09:58:12.000Z",
"lastCustomerMessageAt": "2026-10-05T09:58:12.000Z",
"windowOpen": true,
"windowExpiresAt": "2026-10-06T09:58:12.000Z",
"createdAt": "2026-10-01T11:00:00.000Z"
}
],
"nextCursor": null
}
// …/messages → the chat, then its messages (newest first). sender: contact | agent | ai | smb_echo
{
"conversation": { "id": "9f1c…", "windowOpen": true, … },
"messages": [
{ "id": "5d0e…", "mid": "aWdfZAG1…", "conversationId": "9f1c…", "sender": "contact", "type": "text",
"status": "delivered", "payload": { "type": "text", "text": { "body": "Is the blue one in stock?" } },
"createdAt": "2026-10-05T09:58:12.000Z", "sentAt": null, "failedAt": null, "errors": null }
],
"nextCursor": null
}Reply to an Instagram message
curl -X POST https://fliok.com/api/v1/instagram/send \
-H "Authorization: Bearer wa_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: dm-riya-1042" \
-d '{ "conversationId": "9f1c2b7e-5d1a-4c3e-9b0a-2f6d8e7c1a44", "text": "Yes! Size M and L are in stock." }'// 200 OK — sent straight to Instagram, not queued
{ "ok": true, "messageIds": ["7a3b…"], "humanAgentTag": false }
// More than 24 hours since the customer last wrote. A teammate can still answer from the
// Instagram inbox; an integration only ever gets Instagram's 24-hour window.
// 422 { "error": "outside_24h_window", "message": "It's been more than 24 hours since they last wrote. …" }
// 422 { "error": "never_wrote", … } — they have never messaged you
// 502 { "error": "send_unconfirmed", … } — Instagram didn't answer; check the chat before sending againSchedule a post
# 1. Hand Fliok the photo: a JPEG at a public https:// address, up to 8 MB
curl -X POST https://fliok.com/api/v1/instagram/media \
-H "Authorization: Bearer wa_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://cdn.example.com/diwali-lamps.jpg", "for": "image" }'
# -> 201 { "media": { "mediaId": "instagram-posts/…/1b2c….jpg", "size": 412233, "mime": "image/jpeg",
# "kind": "image", "width": 1080, "height": 1350 }, "hints": [] }
# 2. Schedule it (admin key). action: draft | schedule | publish
curl -X POST https://fliok.com/api/v1/instagram/posts \
-H "Authorization: Bearer wa_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "image",
"caption": "Diwali lamps are back in stock ✨",
"media": ["instagram-posts/…/1b2c….jpg"],
"action": "schedule",
"scheduledAt": "2026-10-20T18:30:00+05:30"
}'
# 3. Changed your mind?
curl -X POST https://fliok.com/api/v1/instagram/posts/3e7a…/cancel -H "Authorization: Bearer wa_YOUR_API_KEY"// 201 Created (cancel → 200 with "status": "cancelled")
{
"post": {
"id": "3e7a…",
"kind": "image",
"caption": "Diwali lamps are back in stock ✨",
"media": [ { "mediaId": "instagram-posts/…/1b2c….jpg", "size": 412233, "mime": "image/jpeg", "kind": "image", "width": 1080, "height": 1350 } ],
"cover": null,
"shareToFeed": true,
"status": "scheduled",
"scheduledAt": "2026-10-20T13:00:00.000Z",
"publishedAt": null,
"igMediaId": null,
"permalink": null,
"errorMessage": null,
"attempts": 0,
"outcomeUnknown": false,
"otherAccount": false,
"createdById": null,
"createdAt": "2026-10-05T10:00:00.000Z",
"updatedAt": "2026-10-05T10:00:00.000Z"
}
}
// Instagram's rules are checked before anything is saved for later:
// 422 { "error": "invalid_post", "message": "…", "issues": ["…"] }
// 422 { "error": "invalid_schedule", "message": "…" } — 2 minutes to 75 days ahead
// The instagram.post.published webhook fires when it goes out.Answer comments
curl "https://fliok.com/api/v1/instagram/comments?filter=needs_reply&limit=20" \
-H "Authorization: Bearer wa_YOUR_API_KEY"
curl -X POST https://fliok.com/api/v1/instagram/comments/6c2d…/reply \
-H "Authorization: Bearer wa_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "text": "₹1,499 — DM us to order!" }'
curl -X POST https://fliok.com/api/v1/instagram/comments/8e1f…/hide \
-H "Authorization: Bearer wa_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "hidden": true }'// GET → 200 OK
{
"comments": [
{
"id": "6c2d…",
"kind": "comment",
"mediaId": "17800000000000001",
"inThread": false,
"username": "riya.makes",
"text": "Price?",
"commentedAt": "2026-10-05T09:40:00.000Z",
"hidden": false,
"reply": null,
"privateReply": null,
"privateReplyBlocked": null,
"ai": null,
"canHide": true,
"canDelete": true,
"media": { "permalink": "https://www.instagram.com/p/abc123/", "thumbnailUrl": "https://…", "caption": "New arrivals", "type": "IMAGE", "owner": null }
}
],
"nextCursor": null,
"counts": { "needs_reply": 1, "all": 14, "mentions": 2, "hidden": 0 },
"connection": { "connected": true, "canManage": true, "igUserId": "17841400000000001", "username": "yourshop" }
}
// reply → 200 { "ok": true } hide → 200 { "ok": true, "hidden": true }
// Replies are PUBLIC. Deleting a comment is not offered by the API — it can't be undone.Read insights
curl "https://fliok.com/api/v1/instagram/insights?days=28" -H "Authorization: Bearer wa_YOUR_API_KEY"// 200 OK — cached for three hours per period, so polling costs nothing at Instagram
{
"days": 28,
"fetchedAt": "2026-10-05T08:00:00.000Z",
"account": { "username": "yourshop", "name": "Your Shop", "followers": 12840, "follows": 310, "posts": 412, "pictureUrl": "https://…" },
"totals": { "views": 98213, "reach": 40122, … },
"approximate": [],
"followerChange": { "follows": 420, "unfollows": 96, "net": 324 },
"daily": [ { "day": "2026-09-08", "reach": 1210 }, … ],
"unavailable": [],
"posts": [ { "id": "1780…", "caption": "…", "permalink": "https://www.instagram.com/p/…/", "likeCount": 412, "commentsCount": 38, "insights": { … }, … } ],
"postsInPeriod": true,
"cached": true
}WhatsApp Pay
Send an order the customer pays without leaving WhatsApp, through the gateway you set up in Settings → Integrations. The same rules as the inbox apply: a WhatsApp chat (not Instagram), inside its 24-hour window, a customer who hasn’t opted out. Amounts are whole numbers in paise and in Indian rupees only. Send your own order number as referenceId: a retry with the same one answers with the request already sent instead of asking twice. When the customer pays, the payment.paid webhook fires and the payment is recorded on their lead.
Request a payment in a chat, then check it
# Inside the customer's 24-hour window, in a WhatsApp chat. Amounts in paise (₹1,499 = 149900).
curl -X POST https://fliok.com/api/v1/payment-requests \
-H "Authorization: Bearer wa_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"conversationId": "2b7e9f1c-1a44-4c3e-9b0a-5d1a2f6d8e7c",
"referenceId": "ORD-1042",
"items": [ { "name": "Brass diya set", "quantity": 1, "unitPaise": 149900 } ],
"shippingPaise": 9900,
"body": "Here is your order. Tap Review and pay to pay on WhatsApp.",
"expiresInMinutes": 1440
}'
# :id is our id or your referenceId
curl https://fliok.com/api/v1/payment-requests/ORD-1042 -H "Authorization: Bearer wa_YOUR_API_KEY"// 201 Created (200 with "duplicate": true when ORD-1042 was already sent in this chat)
{
"paymentRequest": {
"id": "a91c…",
"referenceId": "ORD-1042",
"conversationId": "2b7e…",
"messageId": "c0de…",
"status": "pending",
"displayStatus": "pending",
"statusLabel": "Awaiting payment",
"items": [ { "name": "Brass diya set", "quantity": 1, "unitPaise": 149900, "linePaise": 149900, "retailerId": null } ],
"subtotalPaise": 149900,
"taxPaise": 0,
"shippingPaise": 9900,
"discountPaise": 0,
"totalPaise": 159800,
"paidPaise": null,
"currency": "INR",
"paymentMethod": null,
"failureReason": null,
"expiresAt": "2026-10-06T10:00:00.000Z",
"paidAt": null,
"orderStatus": null,
"createdAt": "2026-10-05T10:00:00.000Z"
},
"messageId": "c0de…"
}
// Refusals name the cause:
// 409 whatsapp_pay_not_set_up · whatsapp_pay_paused · instagram_conversation · contact_opted_out
// 422 outside_24h_window — send a template first; once they reply you can ask for payment
// Subscribe to payment.paid instead of polling.Webhook Events
Register a webhook URL and Fliok POSTs to it whenever an event occurs. 26 events are emitted today. Every payload is signed with HMAC-SHA256 using your endpoint secret, and every body has the same shape: { id, event, createdAt, data } — createdAt is when the event happened, the same on every retry. Non-2xx responses are retried with backoff (12 attempts over about 17 hours), and an endpoint that keeps failing is disabled automatically.
- De-duplicate on the event id. Delivery is at least once: a retry, or a redelivery from the dashboard, carries the same
x-webhook-id(and bodyid). Store the ids you have handled and skip a repeat. - A customer may have no phone number. WhatsApp lets people use a username, and then shares no number for them. Their
phoneiswa:<user id>instead of digits, and events that name a contact also carryuserId(their WhatsApp user id) andusernamewhenever WhatsApp sent them — for customers with a number too. To answer, pass thatphonetoPOST /sendastoexactly as you received it, or sendrecipient: userId. Don’t treatphoneas digits. - Reject stale requests. Refuse a delivery whose
x-webhook-timestamp(unix seconds, when this attempt was signed) is more than five minutes from your clock, so a captured request can’t be replayed later. - Rotate without downtime.
POST /webhooks/{id}/rotate(or Rotate secret on the API keys page) returns a new secret once. For the next 24 hoursx-webhook-signaturecarries two signatures,sha256=<new>,sha256=<old>: accept the delivery when any one matches, switch your server to the new secret, and the old one stops after the 24 hours. - See what was sent. The API keys page keeps every attempt for 7 days — status, timing, error and the payload — and can send any of them again.
Choose what you receive with the events array when you register or PATCH an endpoint — for example ["message.received","message.failed"], or a single event — what a no-code tool subscribing one URL per automation sends. Leave both out (the default) to receive every event, including ones added later. The x-webhook-event header always names the event. GET /webhooks/samples/{event} shows what a delivery of an event looks like before one has happened. Using Zapier, Make or Pabbly? See Zapier, Make and Pabbly Connect.
| Event | Description | Payload |
|---|---|---|
message.received | A contact sent a message to your number. Deduplicated — Meta redelivers webhooks, you do not get doubles. | messageId, wamid, conversationId, contact, type, content |
message.status | An outbound message changed state: sent, delivered, read or failed. Each state fires at most once; statuses never move backwards. | messageId, wamid, conversationId, status, errors |
message.failed | An outbound message failed for good, with the Meta error code explained. Fires once per message. Messages we deliberately did not send (status "skipped") never appear here. | messageId, wamid, conversationId, contact, type, category, errorCode, errorTitle, errorMessage, permanent, retryable, failedAt |
template.approved | Meta approved a message template. Fired on the transition into approved, so a redelivered Meta webhook does not re-fire it. | templateId, metaTemplateId, name, language, category, status |
template.rejected | Meta rejected a message template. `reason` carries Meta's rejection reason when it supplies one. | templateId, metaTemplateId, name, language, category, status, reason |
template.category_changed | Meta re-categorised a template by itself (e.g. UTILITY → MARKETING). Fired on the transition, not on webhook redelivery. Worth acting on: the category decides the per-message price and whether a marketing opt-out suppresses the send. | templateId, metaTemplateId, name, language, previousCategory, category |
contact.created | A customer became a contact for the first time. `phone` is their number — or `wa:<user id>` for a WhatsApp customer whose number Meta has not shared. `userId` and `username` (their WhatsApp user id and username) are present only when known. `source` is inbound_message, smb_echo, user_preferences or api_send. Bulk paths — CSV import, dashboard-created contacts, coexistence address-book sync and Coexistence history backfill — do NOT emit this. | contactId, phone, name, userId, username, source, createdAt |
conversation.opened | The 24-hour customer-service window opened: a customer message arrived while the window was closed. Free-form messages are allowed until windowExpiresAt. Later messages inside the same window do not re-fire it. | conversationId, contact, windowExpiresAt, freeEntryExpiresAt, openedAt |
conversation.closed | The 24-hour customer-service window expired. Only approved templates can be sent until the customer messages again. Detected within ~5 minutes of expiry. | conversationId, contact, windowExpiredAt, closedAt |
conversation.rated | A customer answered the satisfaction survey on a resolved conversation. `score` is 1-5. Fired once per conversation; later taps are ignored. | conversationId, score |
flow.completed | A customer submitted a WhatsApp Flow (an in-chat form). `fields` carries their answers keyed by the Flow's own field names; Meta's flow_token is removed. Fires however the form was sent — by hand, by the AI, by a bot flow or from the API. | messageId, contactId, fields |
call.missed | A customer called the business number on WhatsApp and nobody answered. Fires once per call, on the transition to missed. | callId, contactId, conversationId, phone, startedAt |
order.cod_response | A customer answered the cash-on-delivery confirmation for an order. `confirmed` false means they disowned it — hold the parcel. | conversationId, contactId, reference, confirmed |
order.event | A connected Shopify or WooCommerce store reported an order event. `type` is order_placed, order_paid, order_shipped or order_cancelled. Fires once per store, event and order, whether or not a WhatsApp template is set for that event; the store's retries do not fire it again. | type, provider, shopId, reference, contactId, phone, customerName, total, currency, url, itemCount, cashOnDelivery, occurredAt |
broadcast.completed | A broadcast finished fanning out — every recipient is queued, skipped or excluded. Per-recipient delivery continues and is reported by message.status. | broadcastId, name, templateName, totals, recipients, startedAt, completedAt |
lead.created | A new lead entered the CRM. `via` is form, api, import, manual, integration or whatsapp. `lead` carries id, phone, name, email, stage {id,name,kind}, owner, source, rating, dealValue and tags. | lead, source, via, form, createdAt |
lead.recaptured | A number already in the CRM arrived again (a second form fill, a re-import). The lead is not duplicated; `captureCount` says how many times it has come in. | lead, source, via, form, captureCount, capturedAt |
form.submitted | Somebody submitted a form to your lead-capture link (a website form, or a marketplace push such as IndiaMART or JustDial). Every submission fires, including a repeat from a number you already have (`created` false); a portal re-sending the same enquiry does not. `note` is what they wrote in the message box, `fields` holds answers matched to your custom fields, `extra` everything else the form sent. | leadId, created, phone, name, email, form, source, via, note, fields, extra, submittedAt |
lead.stage_changed | A lead moved stage. `from`/`to` are {id,name,kind} with kind initial, active, won or lost; `lostReason` is set on a move to a lost stage. Restructuring the pipeline in settings does not fire this. | lead, from, to, lostReason, changedAt |
lead.assigned | A lead's owner changed — by a person, a routing rule, an automation or inbox auto-assign. `to` is null when the lead was unassigned. | lead, from, to, assignedAt |
task.created | A follow-up task was created on a lead. `task` carries id, title, type, dueAt, priority, status and assignee. | task, lead, createdAt |
task.completed | A task on a lead was marked done, with the outcome the person recorded. | task, lead, outcome, completedAt |
instagram.message.received | A customer sent an Instagram Direct message. `contact` is {id, name, username} — Instagram has no phone number. A message with several photos arrives as one event per photo (its words are the first one's caption). Deduplicated like message.received. | messageId, mid, conversationId, contact, type, content, receivedAt |
instagram.comment.received | A new comment on one of your Instagram posts, a reply in a comment thread, or an @mention (`kind` is comment, reply or mention). `commentId` is the id the /instagram/comments endpoints take. Your own replies never fire it. | commentId, kind, igCommentId, igMediaId, username, text, permalink, commentedAt |
instagram.post.published | A post written in Fliok went out on Instagram — on its schedule or posted now. Fires once per post; a failure fires nothing (check the post's status). | postId, kind, caption, igMediaId, permalink, scheduledAt, publishedAt |
payment.paid | A customer paid a WhatsApp Pay request. Amounts are in paise (INR); `paymentMethod` is Meta's method type (upi, card, netbanking, wallet…) when it says. If `paidPaise` differs from `totalPaise`, Meta reported a different amount than was asked: check the payment before you fulfil the order. Fired on the transition into paid, so Meta's repeated payment updates never fire it twice. | paymentRequestId, referenceId, conversationId, contactId, totalPaise, paidPaise, currency, paymentMethod, paidAt |
Verifying Webhook Signatures (Node.js)
const crypto = require('crypto');
// Headers on every delivery:
// x-webhook-id, x-webhook-event, x-webhook-timestamp, x-webhook-signature
// The signature is HMAC-SHA256 over `${timestamp}.${rawBody}`, hex, prefixed "sha256=".
// For 24 hours after a secret is rotated the header carries two, comma-separated:
// "sha256=<new>,sha256=<old>" — accept the delivery when any one matches.
// You MUST hash the RAW body — re-serialising the parsed JSON will not match.
function verifyWebhook(rawBody, headers, secret) {
const ts = headers['x-webhook-timestamp'];
// Refuse anything signed more than 5 minutes away from now (a replayed request).
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret) // the whsec_… shown once on creation
.update(ts + '.' + rawBody)
.digest('hex');
return (headers['x-webhook-signature'] || '').split(',').some((got) =>
expected.length === got.trim().length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got.trim())));
}
// In your Express route (note express.raw, not express.json):
app.post('/webhooks/fliok', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyWebhook(req.body.toString(), req.headers, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { id, event, createdAt, data } = JSON.parse(req.body.toString());
// Delivered at least once: skip an id you have already handled.
// event is one of message.received, message.status, message.failed, template.approved, template.rejected, template.category_changed, contact.created, conversation.opened, conversation.closed, conversation.rated, flow.completed, call.missed, order.cod_response, order.event, broadcast.completed, lead.created, lead.recaptured, form.submitted, lead.stage_changed, lead.assigned, task.created, task.completed, instagram.message.received, instagram.comment.received, instagram.post.published, payment.paid
// Always switch on `event` and ignore ones you do not handle — new events may be added.
res.json({ received: true });
});Message Types & Templates
Session Messages (Free-form)
24h windowWhen a customer messages you first, a 24-hour window opens. During this window you can send:
- Text (
type: "text") - Images, video, audio, documents, stickers — upload via
/media - Interactive buttons and lists (
type: "interactive") - Location, contacts and reactions
No template approval needed. Outside the window, /send returns 422 outside_24h_window.
Template Messages (Pre-approved)
Any timeTo message a customer outside the 24h window (notifications, broadcasts, alerts), you must use Meta-approved templates:
- Create it in the dashboard, or POST
/templates - Submitted straight to Meta on the Cloud API — no reseller in between
- Once approved, send it with
/sendusingtype: "template" - Poll
GET /templatesfor the live approval status
3 categories: Marketing, Utility, Authentication. Meta prices and reviews each differently, and may recategorise on submission.
Marketing templates must include opt-out text (e.g., "Reply STOP to unsubscribe"). Templates with spammy language, excessive caps, or URLs in body text will be rejected by Meta.
Rate Limits
Limits are per-endpoint token buckets scoped to your workspace — not to your plan. Burst is how many requests you can fire back-to-back from a full bucket; sustained is how fast it refills. Separately, how fast WhatsApp actually delivers is governed by the messaging tier Meta assigns your phone number.
| Bucket | Endpoints | Burst | Sustained |
|---|---|---|---|
| Reads | /messages · /conversations · /contacts · /templates · /phone-numbers · GET /webhooks · Instagram reads · GET /payment-requests/{id} | 600 | 60 / sec |
| Send | /send | 200 | 50 / sec |
| Media upload | /media | 200 | 50 / sec |
| Webhook admin | POST /webhooks | 60 | 30 / sec |
| Webhook secret rotation | POST /webhooks/{id}/rotate | 5 | 5 / hour |
| Template create | POST /templates | 50 | 50 / hour |
| Broadcast create | POST /broadcasts | 10 | 10 / hour |
| CRM writes | POST /leads · PATCH /leads/{id} · notes · POST /tasks · PATCH /tasks/{id} | 120 | 10 / sec |
| Instagram reply | POST /instagram/send | 120 | 30 / sec |
| Instagram comments | POST /instagram/comments/{id}/reply · /hide (each its own bucket) | 60 | 1 / sec |
| Instagram posts | POST /instagram/posts · POST /instagram/posts/{id}/cancel | 60 | 1 / sec |
| Instagram media | POST /instagram/media | 30 | 1 / 2 sec |
| Instagram live posts | GET /instagram/posts/recent | 30 | 1 / 2 sec |
| WhatsApp Pay | POST /payment-requests | 30 | 1 / 2 sec |
| Lead capture URL | POST /api/leads/capture/{token} (per sending address) | 30 | 30 / min |
Handling Rate Limit Errors
A 429 Too Many Requests means the bucket is empty. It carries a Retry-After header in seconds: wait at least that long, then retry with exponential backoff — a burst of immediate retries will simply drain the bucket again.
Error Codes
| Status | Meaning | When you see it |
|---|---|---|
| 400 | Bad Request | Missing or invalid parameters — e.g. invalid_template_parameters names the offending variable |
| 401 | Unauthorized | No credentials, or api_key_invalid / api_key_revoked / api_key_expired for a rejected key |
| 403 | Forbidden | The API key’s role is too low for this endpoint (sending needs agent, writes need admin), or instagram_disabled — Instagram isn’t on for your workspace yet |
| 404 | Not Found | The requested message, template or webhook endpoint does not exist in your workspace |
| 402 | Plan Limit | A plan limit is reached: this month’s outgoing-message allowance is used up (quota_exceeded), or the contact limit is full, so a NEW lead cannot be added (recapturing an existing lead still works) |
| 409 | Conflict | contact_opted_out, a duplicate webhook URL (the body names the existing endpoint’s id), the 25-endpoint cap, Instagram not connected (not_connected, reconnect_required), WhatsApp Pay not set up, number_banned / sending_blocked (Meta is refusing messages from the number, so nothing was queued), or a broadcast to people who have never messaged you that needs confirmNewRecipients or has to wait (new_recipients_unconfirmed, new_recipient_cool_off, follow_up_too_soon) |
| 413 | Payload Too Large | Media upload above the 16 MB limit (8 MB for an Instagram photo) |
| 422 | Unprocessable | outside_24h_window — a free-form send (WhatsApp or Instagram) needs an open customer service window; on WhatsApp use a template. needs_phone_number — a verification-code template to a customer WhatsApp shares no number for. Also invalid_post / invalid_schedule for an Instagram post, and notice_to_new_recipients / follow_up_notice_template for a broadcast of a utility or authentication template to people who have never messaged you |
| 429 | Rate Limited | You have exceeded the token bucket for this endpoint — wait the Retry-After seconds, then retry |
| 500 | Server Error | An internal error occurred — retry with exponential backoff |
Why Developers Love the Fliok API
RESTful & Predictable
Standard HTTP verbs, JSON request and response bodies, and ISO-8601 timestamps throughout.
Role-Based, Expiring Keys
Each key carries a role (admin, agent or viewer) for the whole workspace, can be given an expiry date, records when it was last used, and revokes instantly.
Queued, Not Dropped
Sends are queued and retried on transient failures, so a blip at Meta does not silently lose your message.
Signed Webhooks
HMAC-SHA256 over timestamp and raw body, with retries and automatic disabling of dead endpoints.
No SDK Required
Plain HTTP and JSON — use fetch, requests, cURL or any HTTP client. Nothing to install or keep in step.
Honest Errors
Failures name the cause — outside_24h_window, contact_opted_out, api_key_expired — instead of a generic 400.
Ready to Build?
API access is part of the Pro plan. Your first upgrade starts with a 7-day trial (a payment method is needed), and you create keys from the API keys page.
