Fliok Developer API
Build custom integrations with Fliok using our REST API. Send messages and media, read conversations, contacts and templates, and receive signed real-time webhooks — all programmatically.
Quick Start in 3 Steps
Get API Key
Create an account and 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
Fliok API uses Bearer Token authentication. Include your API key in the Authorization header for every request. Keys are scoped to your workspace, carry a role, and can be revoked — or given an expiry date — from the dashboard. 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.
Base URL
https://fliok.com/api/v1Current Version
v1 (stable)# Include in every request header
Authorization: Bearer wa_xxxxxxxxxxxxxxxxxxxxxxxx
Never 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)
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": "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" }
]
}
]
}
}
}'
# 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 wamid)
curl https://fliok.com/api/v1/messages/gBEGkYiEB1VXAglK8ZaH2Kq4A \
-H "Authorization: Bearer wa_YOUR_API_KEY"# Successful response from POST /send (200 OK)
{
"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',
},
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"
HEAD = {"Authorization": f"Bearer {API_KEY}"}
# 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",
],
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.
API Endpoints
/sendSend a text, template, media or interactive message Auth/mediaUpload a document/image to Meta and get a reusable media id Auth/messagesList messages, cursor-paginated. Filter by conversationId and status Auth/messages/{id}Get one message and its delivery status — by our id or by wamid Auth/conversationsList conversations with contact info and window expiry Auth/contactsList contacts, cursor-paginated. Filter with ?search= Auth/templatesList your WhatsApp templates and their Meta approval status Auth/templatesSubmit a new template to Meta for approval Auth/contactsCreate a contact Auth/contacts/{id}Get one contact with its custom fields Auth/contacts/{id}Update a contact. Custom attributes merge rather than replace Auth/contacts/{id}Delete a contact Auth/broadcastsList campaigns with live sent/delivered/failed counts Auth/broadcastsCreate a campaign by tags, contact ids or a number list. Returns how many will actually send after opt-out and the marketing cap Auth/broadcasts/{id}One campaign with its delivery progress Auth/broadcasts/{id}Cancel a campaign. Messages already handed to WhatsApp cannot be recalled Auth/conversations/{id}One conversation, including whether the 24-hour free-form window is open Auth/conversations/{id}Assign, prioritise, resolve or archive a conversation Auth/conversations/{id}/notesInternal notes on a conversation — never sent to the customer Auth/conversations/{id}/notesAdd an internal note Auth/analyticsMessage and conversation counts for a window (?days=, max 90). Reports skipped separately from failed Auth/invoicesList issued GST tax invoices, cursor-paginated Auth/invoices/{id}One invoice as JSON, or ?format=html for a printable tax invoice Auth/flowsList automation flows and whether each is published (read only) Auth/quick-repliesList saved canned replies Auth/webhooksList your registered webhook endpoints Auth/webhooksRegister an endpoint. Returns the signing secret once Auth/webhooks/{id}Pause/resume an endpoint or change its subscribed events Auth/webhooks/{id}Remove a webhook endpoint Auth/leadsList leads, page-numbered. Filter by view, a filter JSON or ?q=, sort by any sortable field Auth/leadsAdd a lead. Idempotent by phone: a known number is recaptured, never duplicated. Stage and owner by name or email Auth/leads/{id}One lead with stage, owner, deal value and custom fields — by our id or by phone number Auth/leads/{id}Move stage (with lost reason), reassign, rate, tag or edit fields. Lands on the lead’s timeline and fires automations Auth/leads/{id}/notesAdd a note to a lead’s timeline — never sent to the customer Auth/tasksList follow-up tasks with counts per status (open, overdue, today, upcoming, done, cancelled) Auth/tasksCreate a follow-up on a lead; the assignee defaults to the lead’s owner Auth/tasks/{id}Complete (with an outcome), cancel, reschedule or reassign a task Auth/pipelineYour pipeline stages (kind initial, active, won or lost) and lost reasons AuthSales 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)
# Request
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"
}'# Response
// 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
# Request
# 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"# Response
// 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
# Request
# :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" }'# Response
// 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
# Request
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" }'# Response
// 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
# Request
curl https://fliok.com/api/v1/pipeline -H "Authorization: Bearer wa_YOUR_API_KEY"
# Response
// 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…"
}Webhook Events
Register a webhook URL and Fliok POSTs to it whenever an event occurs. 20 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 }. Non-2xx responses are retried, and an endpoint that keeps failing is disabled automatically.
Choose what you receive with the events array when you register or PATCH an endpoint — for example ["message.received","message.failed"]. Leave it empty (the default) to receive every event, including ones added later. Thex-webhook-event header always names the event.
message.receivedA contact sent a message to your number. Deduplicated — Meta redelivers webhooks, you do not get doubles.
Payload: messageId, wamid, conversationId, contact, type, content
message.statusAn outbound message changed state: sent, delivered, read or failed. Each state fires at most once; statuses never move backwards.
Payload: messageId, wamid, conversationId, status, errors
message.failedAn 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.
Payload: messageId, wamid, conversationId, contact, type, category, errorCode, errorTitle, errorMessage, permanent, retryable, failedAt
template.approvedMeta approved a message template. Fired on the transition into approved, so a redelivered Meta webhook does not re-fire it.
Payload: templateId, metaTemplateId, name, language, category, status
template.rejectedMeta rejected a message template. `reason` carries Meta's rejection reason when it supplies one.
Payload: templateId, metaTemplateId, name, language, category, status, reason
template.category_changedMeta 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.
Payload: templateId, metaTemplateId, name, language, previousCategory, category
contact.createdA phone number became a contact for the first time. `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.
Payload: contactId, phone, name, source, createdAt
conversation.openedThe 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.
Payload: conversationId, contact, windowExpiresAt, freeEntryExpiresAt, openedAt
conversation.closedThe 24-hour customer-service window expired. Only approved templates can be sent until the customer messages again. Detected within ~5 minutes of expiry.
Payload: conversationId, contact, windowExpiredAt, closedAt
conversation.ratedA customer answered the satisfaction survey on a resolved conversation. `score` is 1-5. Fired once per conversation; later taps are ignored.
Payload: conversationId, score
flow.completedA 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.
Payload: messageId, contactId, fields
call.missedA customer called the business number on WhatsApp and nobody answered. Fires once per call, on the transition to missed.
Payload: callId, contactId, conversationId, phone, startedAt
order.cod_responseA customer answered the cash-on-delivery confirmation for an order. `confirmed` false means they disowned it — hold the parcel.
Payload: conversationId, contactId, reference, confirmed
broadcast.completedA broadcast finished fanning out — every recipient is queued, skipped or excluded. Per-recipient delivery continues and is reported by message.status.
Payload: broadcastId, name, templateName, totals, recipients, startedAt, completedAt
lead.createdA 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.
Payload: lead, source, via, form, createdAt
lead.recapturedA 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.
Payload: lead, source, via, form, captureCount, capturedAt
lead.stage_changedA 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.
Payload: lead, from, to, lostReason, changedAt
lead.assignedA lead's owner changed — by a person, a routing rule, an automation or inbox auto-assign. `to` is null when the lead was unassigned.
Payload: lead, from, to, assignedAt
task.createdA follow-up task was created on a lead. `task` carries id, title, type, dueAt, priority, status and assignee.
Payload: task, lead, createdAt
task.completedA task on a lead was marked done, with the outcome the person recorded.
Payload: task, lead, outcome, completedAt
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=".
// 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'];
const expected = 'sha256=' + crypto
.createHmac('sha256', secret) // the whsec_… shown once on creation
.update(ts + '.' + rawBody)
.digest('hex');
const got = headers['x-webhook-signature'] || '';
return expected.length === got.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got));
}
// 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());
// 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, broadcast.completed, lead.created, lead.recaptured, lead.stage_changed, lead.assigned, task.created, task.completed
// 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)
When 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)
To 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 | 600 | 60 / sec |
| Send | /send | 200 | 50 / sec |
| Media upload | /media | 200 | 50 / sec |
| Webhook admin | POST /webhooks | 60 | 30 / sec |
| 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 |
| 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. Wait for it to refill at the sustained rate above and retry with exponential backoff — a burst of immediate retries will simply drain it again.
Error Codes
Why Developers Love the Fliok API
RESTful & Predictable
Standard HTTP verbs, JSON request and response bodies, and ISO-8601 timestamps throughout.
Scoped, Expiring Keys
Each key carries a role, 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?
Create your free account to get an API key. No credit card required, and the free plan can call the API.