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, and receive signed real-time webhooks — all programmatically.

12
API Endpoints
Bearer
Token Auth, Scoped Keys
HMAC
Signed Webhooks
Cursor
Paginated Reads

Quick Start in 3 Steps

01

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.

02

Make Your First Call

Use our REST API with any language. Full cURL, Python, Node.js, PHP examples included.

03

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/v1

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

Node.js
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);
Python
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
<?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

POST/sendSend a text, template, media or interactive message Auth
POST/mediaUpload a document/image to Meta and get a reusable media id Auth
GET/messagesList messages, cursor-paginated. Filter by conversationId and status Auth
GET/messages/{id}Get one message and its delivery status — by our id or by wamid Auth
GET/conversationsList conversations with contact info and window expiry Auth
GET/contactsList contacts, cursor-paginated. Filter with ?search= Auth
GET/templatesList your WhatsApp templates and their Meta approval status Auth
POST/templatesSubmit a new template to Meta for approval Auth
GET/webhooksList your registered webhook endpoints Auth
POST/webhooksRegister an endpoint. Returns the signing secret once Auth
PATCH/webhooks/{id}Pause/resume an endpoint or change its subscribed events Auth
DELETE/webhooks/{id}Remove a webhook endpoint Auth

Webhook 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.received

A contact sent a message to your number. Deduplicated — Meta redelivers webhooks, you do not get doubles.

Payload: 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.

Payload: 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.

Payload: 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.

Payload: templateId, metaTemplateId, name, language, category, status

template.rejected

Meta rejected a message template. `reason` carries Meta's rejection reason when it supplies one.

Payload: 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.

Payload: templateId, metaTemplateId, name, language, previousCategory, category

contact.created

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

Payload: 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.

Payload: conversationId, contact, windowExpiredAt, closedAt

broadcast.completed

A 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 /send using type: "template"
  • Poll GET /templates for 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.

BucketEndpointsBurstSustained
Reads/messages · /conversations · /contacts · /templates60060 / sec
Send/send20050 / sec
Media upload/media20050 / sec
Webhook adminPOST /webhooks6030 / sec
Template createPOST /templates5050 / 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

400Bad RequestMissing or invalid parameters — e.g. invalid_template_parameters names the offending variable
401UnauthorizedNo credentials, or api_key_invalid / api_key_revoked / api_key_expired for a rejected key
403ForbiddenThe API key’s role is too low for this endpoint (sending needs agent, writes need admin)
404Not FoundThe requested message, template or webhook endpoint does not exist in your workspace
409Conflictcontact_opted_out, a duplicate webhook URL, or the 10-endpoint cap
413Payload Too LargeMedia upload above the 16 MB limit
422Unprocessableoutside_24h_window — a free-form send needs an open customer service window; use a template
429Rate LimitedYou have exceeded the token bucket for this endpoint — back off and retry
500Server ErrorAn 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.

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.

API Documentation - REST API v1 | Fliok Developer Docs | Fliok