Skip to content

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

58
API Endpoints
Bearer
Token Auth, Role-Based Keys
HMAC
Signed Webhooks
Cursor
Paginated Reads

Quick Start in 3 Steps

01

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.

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

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.

Base URLhttps://fliok.com/api/v1
Current Versionv1stable
# 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

cURL
# 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"
# 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',
    '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);

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 message
  • POST/mediaUpload a document/image to Meta and get a reusable media id

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 Pabbly
  • GET/messagesList messages, cursor-paginated. Filter by conversationId and status
  • GET/messages/{id}Get one message and its delivery status — by our id or by wamid
  • GET/conversationsList WhatsApp conversations with contact info and window expiry
  • GET/contactsList contacts, cursor-paginated. Filter with ?search= (any part of a name or number), ?phone= (one exact number); ?channel=instagram for Instagram customers
  • GET/phone-numbersYour WhatsApp numbers — the ids phoneNumberId refers to
  • GET/templatesList your WhatsApp templates and their Meta approval status
  • POST/templatesSubmit a new template to Meta for approval

Contacts

4
  • POST/contactsCreate a contact
  • GET/contacts/{id}Get one contact with its custom fields — by our id or by phone number
  • PATCH/contacts/{id}Update a contact. Custom attributes merge rather than replace
  • DELETE/contacts/{id}Delete a contact

Broadcasts

4
  • GET/broadcastsList campaigns with live sent/delivered/failed counts
  • 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
  • GET/broadcasts/{id}One campaign with its delivery progress
  • DELETE/broadcasts/{id}Cancel a campaign. Messages already handed to WhatsApp cannot be recalled

Inbox

4
  • GET/conversations/{id}One conversation, including whether the 24-hour free-form window is open
  • PATCH/conversations/{id}Assign, prioritise, resolve or archive a conversation
  • GET/conversations/{id}/notesInternal notes on a conversation — never sent to the customer
  • POST/conversations/{id}/notesAdd an internal note

Reporting

1
  • GET/analyticsMessage and conversation counts for a window (?days=, max 90). Reports skipped separately from failed

Invoices

2
  • GET/invoicesList issued GST tax invoices, cursor-paginated
  • GET/invoices/{id}One invoice as JSON, or ?format=html for a printable tax invoice

Read-only config

2
  • GET/flowsList automation flows and whether each is published (read only)
  • GET/quick-repliesList saved canned replies

Webhooks

7
  • GET/webhooksList your registered webhook endpoints
  • POST/webhooksRegister an endpoint for every event, a list (events) or just one (event). Returns its id and, once, the signing secret
  • GET/webhooks/eventsEvery event you can subscribe to, with its meaning and payload fields
  • GET/webhooks/samples/{event}Your latest real deliveries of one event (admin keys), or a documented example when there are none yet
  • PATCH/webhooks/{id}Pause/resume an endpoint or change its subscribed events
  • POST/webhooks/{id}/rotateReplace the signing secret; the old one keeps signing alongside it for 24 hours
  • DELETE/webhooks/{id}Remove a webhook endpoint. Safe to repeat: one already gone answers ok with deleted: false

Sales CRM

9
  • GET/leadsList leads, page-numbered. Filter by view, a filter JSON or ?q=, sort by any sortable field
  • POST/leadsAdd a lead. Idempotent by phone: a known number is recaptured, never duplicated. Stage and owner by name or email
  • GET/leads/{id}One lead with stage, owner, deal value and custom fields — by our id or by phone number
  • PATCH/leads/{id}Move stage (with lost reason), reassign, rate, tag or edit fields. Lands on the lead’s timeline and fires automations
  • POST/leads/{id}/notesAdd a note to a lead’s timeline — never sent to the customer
  • GET/tasksList follow-up tasks with counts per status (open, overdue, today, upcoming, done, cancelled)
  • POST/tasksCreate a follow-up on a lead; the assignee defaults to the lead’s owner
  • PATCH/tasks/{id}Complete (with an outcome), cancel, reschedule or reassign a task
  • GET/pipelineYour pipeline stages (kind initial, active, won or lost) and lost reasons

Instagram

13
  • GET/instagram/conversationsInstagram Direct chats, cursor-paginated, with whether the 24-hour reply window is open
  • GET/instagram/conversations/{id}/messagesOne Instagram chat and its messages, newest first
  • POST/instagram/sendReply in an Instagram chat — only within 24 hours of the customer’s last message
  • POST/instagram/mediaGive Fliok a JPEG by public https:// URL for a post; returns a mediaId
  • GET/instagram/postsPosts written in Fliok: drafts, scheduled, published, failed, cancelled
  • POST/instagram/postsSave a draft, schedule a post, or publish now (admin keys) from your mediaIds
  • GET/instagram/posts/recentThe account’s own published posts on Instagram, live, with likes and comments
  • GET/instagram/posts/{id}One Fliok post — poll it for published or failed
  • POST/instagram/posts/{id}/cancelCancel a scheduled post before it goes out (admin keys)
  • GET/instagram/commentsComments and @mentions, filtered by needs_reply, all, mentions or hidden
  • POST/instagram/comments/{id}/replyReply in public under a comment
  • POST/instagram/comments/{id}/hideHide a comment from everyone but its author, or show it again
  • GET/instagram/insightsAccount summary for 7, 28 or 90 days: followers, views, reach, engagement, top posts

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 safe
  • GET/payment-requests/{id}Where a payment request stands — by our id or your referenceId

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

FieldTypeWhat it is
torequiredstringThe 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`.
recipientstringInstead 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.
typerequiredstringtext, template, image, video, audio, document, interactive, reaction, location, sticker or contacts.
textstringThe message, for type "text" (required then, never empty). Needs an open 24-hour window.
payloadobjectFor every other type: Meta’s message object, { "type": "<type>", "<type>": { … } } — e.g. { "type": "document", "document": { "id": "<media id>", "filename": "invoice.pdf" } }.
payload.template.variablesarrayTemplate 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.headerMediaobjectTemplate shortcut: { "id": "<media id>" } or { "link": "https://…" } (plus "filename" for a document) for an image, video or document header.
payload.template.headerText · couponCode · offerExpiresAtvariousTemplate shortcut: a text header’s variables, a coupon-code button’s code, a limited-time offer’s end (ISO time).
phoneNumberIduuidWhich of your numbers sends (GET /phone-numbers). Default: your first active number.
replyTostringA 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.

FieldTypeWhat it is
fileBase64requiredstringThe file’s bytes, base64-encoded, without a "data:" prefix.
mimeTypestringFor example application/pdf, image/jpeg, video/mp4. Default application/pdf.
filenamestringUp to 200 characters. Default document.pdf.
phoneNumberIduuidThe 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.

FieldTypeWhat it is
limitnumber1–100, default 50.
cursoruuidThe nextCursor of the page before. Anything else is a 400.
conversationIduuid/messages only: one chat’s messages.
statusstring/messages only: queued, sent, delivered, read, failed or skipped.
searchstring/contacts only: part of a number, a name or a WhatsApp username.
channelstring/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.

FieldTypeWhat it is
phonerequiredstringPOST only (or "e164"). E.164, with or without “+”.
displayNamestringUp to 200 characters.
tagsstring[]Up to 50 tags, replacing the current ones.
attributesobjectCustom fields. Merged into what is stored, never replacing it.
optInStatusstringunknown, opted_in or opted_out. A contact who sent STOP can’t be opted back in by the API (409).
optInSourcestringWhere 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.

FieldTypeWhat it is
namerequiredstringLowercase letters, digits and underscores.
languagerequiredstringA plain code such as "en" or "en_US" (only a send uses { "code": … }).
categoryrequiredstringMARKETING, UTILITY or AUTHENTICATION. Meta may change it.
componentsrequiredobject[]Meta’s component array (HEADER, BODY, FOOTER, BUTTONS). Every variable needs an example.
wabaConnectionIduuidOnly 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.

FieldTypeWhat it is
namerequiredstringUp to 200 characters.
templateIdrequireduuidAn approved template’s id (GET /templates).
phoneNumberIdrequireduuidThe number that sends (GET /phone-numbers).
segmentrequiredobjectWho receives it: { "tags": [ … ] } and/or "excludeTags", "optInStatus", "lastSeenWithinDays" — or { "contactIds": [ … ] } for hand-picked contacts (an empty list means nobody).
variablesobjectThe template’s parameters as { "components": [ … ] }, the same shape a template send uses.
sendNowbooleanStart sending at once.
scheduledAtISO timeSend later instead. With neither, it is saved as a draft.
confirmNewRecipientsbooleanYour confirmation that the recipients who have never messaged you agreed to get WhatsApp messages from your business. Recorded in the activity log.
knownOnlybooleanSend only to recipients who have messaged you or have an opt-in recorded; the others are listed as skipped.
followUpOfuuidThe 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.

FieldTypeWhat it is
statestringPATCH: open, pending or resolved.
prioritystringPATCH: low, normal, high or urgent.
isArchivedbooleanPATCH: archive or unarchive.
assignedAgentIduuid | nullPATCH: a team member’s user id, or null to unassign.
bodyrequiredstringNotes: 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"
  }'

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"

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" }'

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" }'

Read the pipeline

curl https://fliok.com/api/v1/pipeline -H "Authorization: Bearer wa_YOUR_API_KEY"

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/send answers 422 outside_24h_window after 24 hours.
  • Posts take photos by URL. POST /instagram/media downloads 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"

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." }'

Schedule 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"

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 }'

Read insights

curl "https://fliok.com/api/v1/instagram/insights?days=28" -H "Authorization: Bearer wa_YOUR_API_KEY"

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"

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 body id). 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 phone is wa:<user id> instead of digits, and events that name a contact also carry userId (their WhatsApp user id) and username whenever WhatsApp sent them — for customers with a number too. To answer, pass that phone to POST /send as to exactly as you received it, or send recipient: userId. Don’t treat phone as 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 hours x-webhook-signature carries 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.

EventDescriptionPayload
message.receivedA contact sent a message to your number. Deduplicated — Meta redelivers webhooks, you do not get doubles.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.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.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.templateId, metaTemplateId, name, language, category, status
template.rejectedMeta rejected a message template. `reason` carries Meta's rejection reason when it supplies one.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.templateId, metaTemplateId, name, language, previousCategory, category
contact.createdA 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.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.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.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.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.messageId, contactId, fields
call.missedA 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_responseA customer answered the cash-on-delivery confirmation for an order. `confirmed` false means they disowned it — hold the parcel.conversationId, contactId, reference, confirmed
order.eventA 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.completedA 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.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.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.lead, source, via, form, captureCount, capturedAt
form.submittedSomebody 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_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.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.lead, from, to, assignedAt
task.createdA follow-up task was created on a lead. `task` carries id, title, type, dueAt, priority, status and assignee.task, lead, createdAt
task.completedA task on a lead was marked done, with the outcome the person recorded.task, lead, outcome, completedAt
instagram.message.receivedA 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.receivedA 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.publishedA 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.paidA 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)

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 window

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)

Any time

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 · /templates · /phone-numbers · GET /webhooks · Instagram reads · GET /payment-requests/{id}60060 / sec
Send/send20050 / sec
Media upload/media20050 / sec
Webhook adminPOST /webhooks6030 / sec
Webhook secret rotationPOST /webhooks/{id}/rotate55 / hour
Template createPOST /templates5050 / hour
Broadcast createPOST /broadcasts1010 / hour
CRM writesPOST /leads · PATCH /leads/{id} · notes · POST /tasks · PATCH /tasks/{id}12010 / sec
Instagram replyPOST /instagram/send12030 / sec
Instagram commentsPOST /instagram/comments/{id}/reply · /hide (each its own bucket)601 / sec
Instagram postsPOST /instagram/posts · POST /instagram/posts/{id}/cancel601 / sec
Instagram mediaPOST /instagram/media301 / 2 sec
Instagram live postsGET /instagram/posts/recent301 / 2 sec
WhatsApp PayPOST /payment-requests301 / 2 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. 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

StatusMeaningWhen you see it
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), or instagram_disabled — Instagram isn’t on for your workspace yet
404Not FoundThe requested message, template or webhook endpoint does not exist in your workspace
402Plan LimitA 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)
409Conflictcontact_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)
413Payload Too LargeMedia upload above the 16 MB limit (8 MB for an Instagram photo)
422Unprocessableoutside_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
429Rate LimitedYou have exceeded the token bucket for this endpoint — wait the Retry-After seconds, then 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.

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.