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/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 AuthWebhook Events
Register a webhook URL and Fliok POSTs to it whenever an event occurs. 10 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
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
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, broadcast.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 |
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.