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.

38
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
POST/contactsCreate a contact Auth
GET/contacts/{id}Get one contact with its custom fields Auth
PATCH/contacts/{id}Update a contact. Custom attributes merge rather than replace Auth
DELETE/contacts/{id}Delete a contact Auth
GET/broadcastsList campaigns with live sent/delivered/failed counts Auth
POST/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
GET/broadcasts/{id}One campaign with its delivery progress Auth
DELETE/broadcasts/{id}Cancel a campaign. Messages already handed to WhatsApp cannot be recalled Auth
GET/conversations/{id}One conversation, including whether the 24-hour free-form window is open Auth
PATCH/conversations/{id}Assign, prioritise, resolve or archive a conversation Auth
GET/conversations/{id}/notesInternal notes on a conversation — never sent to the customer Auth
POST/conversations/{id}/notesAdd an internal note Auth
GET/analyticsMessage and conversation counts for a window (?days=, max 90). Reports skipped separately from failed Auth
GET/invoicesList issued GST tax invoices, cursor-paginated Auth
GET/invoices/{id}One invoice as JSON, or ?format=html for a printable tax invoice Auth
GET/flowsList automation flows and whether each is published (read only) Auth
GET/quick-repliesList saved canned replies 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
GET/leadsList leads, page-numbered. Filter by view, a filter JSON or ?q=, sort by any sortable field Auth
POST/leadsAdd a lead. Idempotent by phone: a known number is recaptured, never duplicated. Stage and owner by name or email Auth
GET/leads/{id}One lead with stage, owner, deal value and custom fields — by our id or by phone number Auth
PATCH/leads/{id}Move stage (with lost reason), reassign, rate, tag or edit fields. Lands on the lead’s timeline and fires automations Auth
POST/leads/{id}/notesAdd a note to a lead’s timeline — never sent to the customer Auth
GET/tasksList follow-up tasks with counts per status (open, overdue, today, upcoming, done, cancelled) Auth
POST/tasksCreate a follow-up on a lead; the assignee defaults to the lead’s owner Auth
PATCH/tasks/{id}Complete (with an outcome), cancel, reschedule or reassign a task Auth
GET/pipelineYour pipeline stages (kind initial, active, won or lost) and lost reasons Auth

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)

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

conversation.rated

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

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

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

Payload: conversationId, contactId, reference, confirmed

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

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.

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

Payload: lead, source, via, form, captureCount, capturedAt

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.

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

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

Payload: task, lead, createdAt

task.completed

A 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 /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
Broadcast createPOST /broadcasts1010 / hour
CRM writesPOST /leads · PATCH /leads/{id} · notes · POST /tasks · PATCH /tasks/{id}12010 / sec
Lead capture URLPOST /api/leads/capture/{token} (per sending address)3030 / 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

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
402Plan LimitYour plan’s contact limit is full, so a NEW lead cannot be added (recapturing an existing lead still works)
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