Developers

Connect OrcaFlo to anything

Sync contacts with your CRM, react to new leads, bookings and payments, send WhatsApp messages from your own systems, plug in another messaging app, or let Claude and ChatGPT work in your inbox.

Quickstart

  1. In the OrcaFlo panel open Manage → Developers → API keys and create a key. Pick only the permissions it needs; you can revoke it any time.
  2. Keep it in an environment variable (ORCAFLO_API_KEY). Never put it in a website or app your customers download.
  3. Check it works:
const res = await fetch('https://api.orcaflo-panel.com/v1/me', {
  headers: { Authorization: `Bearer ${process.env.ORCAFLO_API_KEY}` },
});
console.log(await res.json()); // { workspace_id, workspace_name, scopes, … }

Then do something useful: add a lead from your website and reply to a customer.

const api = (path, init = {}) => fetch('https://api.orcaflo-panel.com/v1' + path, {
  ...init,
  headers: { Authorization: `Bearer ${process.env.ORCAFLO_API_KEY}`, 'Content-Type': 'application/json', ...init.headers },
}).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body.error.message); return body; });

// Create or update a contact from your website form
const contact = await api('/contacts/by-phone/' + encodeURIComponent('+971501234567'), {
  method: 'PUT',
  body: JSON.stringify({ name: 'Sara Ahmed', email: 'sara@example.com', tags: ['website'] }), // + lifecycle: a name from GET /lifecycles
});

// Reply in an existing conversation (safe to retry with the same Idempotency-Key)
await api('/messages', {
  method: 'POST',
  headers: { 'Idempotency-Key': 'order-1042-shipped' },
  body: JSON.stringify({ contact_id: contact.id, text: 'Your order has shipped 🚚' }),
});

Authentication

Every request sends an API key as a bearer token:

Authorization: Bearer ofk_live_…
  • Admins create keys under Manage → Developers → API keys. A key is shown once and belongs to one business; the panel shows when each key was last used. Revoking a key stops it at once, including the webhooks it created.
  • Each key has permissions (scopes). Presets: Read only, Zapier / Make / n8n (everything except catalogue and broadcast writes) and Full access. A missing permission gives 403 missing_scope.
  • Apps that act for a user (like the Claude and ChatGPT connectors) use OAuth 2.1 with PKCE instead of a key; see Connect Claude or ChatGPT.
  • Keys are secrets for servers. Never ship one in a website, mobile app or public repository; if one leaks, revoke it and create another.
ScopeAllows
contacts:read
contacts:write
Read contacts, tags, custom fields, lifecycle stages / create, update, tag and delete them
conversations:read
conversations:write
Conversations, summaries, teammates and teams / assign, close and reopen (team inbox)
messages:read
messages:send
Message history / send messages, pause and resume the AI
appointments:read
appointments:write
Free slots and bookings / book, reschedule, cancel
orders:read
Orders and payment status
products:read
products:write
Product catalogue
broadcasts:read
broadcasts:write
Broadcasts
workflows:read
workflows:run
List workflows and their runs / start a workflow for a contact
webhooks:manage
Create and delete webhook endpoints (Zapier, Make and n8n use this)
channels:manage
channels:inbound
Custom channels / post customer messages into one

Requests and paging

  • Base URL https://api.orcaflo-panel.com/v1, HTTPS only. JSON in and out, UTC ISO-8601 timestamps, phone numbers in international format (+971…).
  • Lists are paged with limit (1–100, default 25) and cursor; each response has next_cursor (null on the last page).
  • Send an Idempotency-Key header on POSTs: a retry with the same key within 24 hours returns the first result instead of doing it twice. Reusing a key for a different request is a 409.
  • To protect your WhatsApp number, messages go only into conversations the customer started. Reach new leads with POST /contacts plus start_conversation: OrcaFlo writes an opener and sends it at a safe pace during business hours.

Rate limits

LimitApplies to
300 per minute
Reads (GET), per key
60 per minute
Writes (POST, PUT, PATCH, DELETE), per key
30 per minute
Messages sent, per key
20 per hour
New conversations started (start_conversation), per business, to keep your number safe
60 per minute
Requests to each incoming webhook URL

Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds); a replayed Idempotency-Key response doesn't count against the limit and carries Idempotent-Replayed: true instead. Over a limit you get 429 with Retry-After; wait that long and retry.

Errors

Errors use the HTTP status plus a JSON body. message is written for people and is safe to show to your users; branch on code.

HTTP/1.1 422 Unprocessable Entity
{ "error": { "type": "unprocessable", "code": "no_prior_conversation",
    "message": "This contact has never messaged you. Start a conversation with POST /v1/contacts and "start_conversation".",
    "param": null } }
StatustypeWhen
400
invalid_request
A parameter is missing or has the wrong format. param names it.
401
authentication
No key, a mistyped key, or a revoked or expired one.
403
permission
missing_scope: the key lacks a permission. feature_disabled: the feature isn't on for this business.
403
workspace_paused
not_live, billing_suspended, workspace_paused or whatsapp_disconnected: nothing can be sent until it's fixed in the panel.
404
not_found
The contact, conversation or appointment doesn't exist in this business.
409
conflict
contact_exists (use the upsert), already_booked, slot_full, idempotency_mismatch, idempotency_in_progress.
422
unprocessable
no_prior_conversation: the customer has never messaged you; window_closed: outside the channel's reply window.
429
rate_limited
Over a rate limit. Wait Retry-After seconds.
500
502
server_error
Our side, or WhatsApp refused the message (send_failed). Safe to retry with the same Idempotency-Key.
503
server_error
not_configured: the API isn't switched on yet.

Webhooks

Add an HTTPS endpoint in Manage → Developers → Webhooks (or with POST /webhooks), pick the events, and copy the signing secret. Every event is a POST like this:

POST /your-endpoint
OrcaFlo-Event-Id: 0b4c…            (dedupe on this: delivery is at-least-once)
OrcaFlo-Event-Type: message.received
OrcaFlo-Signature: t=1790600000,v1=5d41…

{
  "id": "0b4c…", "type": "message.received", "api_version": "2026-10-01",
  "created_at": "2026-10-02T09:14:03.120Z", "workspace_id": "…",
  "origin": "contact",               // contact, bot, agent:…, api:…, system
  "data": { "contact": { … }, "message": { … } }
}
  • Answer with any 2xx within 10 seconds, then do slow work in the background.
  • Failures are retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h. 410 unsubscribes the endpoint.
  • The signature is HMAC-SHA256 of "<t>.<raw body>" with your secret. Reject anything older than 5 minutes. During a secret rotation two v1= values are sent for 24 hours; accept either.
  • An endpoint that keeps failing for 24 hours (and at least 100 attempts in a row) is switched off, and your admins are told. Fix it and switch it back on in the panel; Recent deliveries shows every attempt and can Resend one, and Test sends a sample event.
  • Endpoints must be public HTTPS addresses; private and local addresses are refused, and redirects aren't followed. A business can have up to 20 active endpoints.
  • Manage endpoints from code with GET/POST /webhooks and DELETE /webhooks/{id} (scope webhooks:manage). Endpoints that the Zapier, Make and n8n apps create don't receive the message, conversation and appointment events their own connection caused, so an automation can't loop on its own messages; contact events still arrive, so one automation can react to another's new contact.

A complete receiver:

// npm i express
import express from 'express';
import crypto from 'node:crypto';

const app = express();
const SECRET = process.env.ORCAFLO_WEBHOOK_SECRET; // whsec_… from OrcaFlo
const seen = new Set();                              // use your DB in production

function verify(raw, header, secret) {
  const parts = String(header || '').split(',');
  const t = Number((parts.find((p) => p.startsWith('t=')) || '').slice(2));
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;   // replay window
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${raw}`).digest('hex');
  return parts.filter((p) => p.startsWith('v1=')).some((p) => {
    const got = Buffer.from(p.slice(3)); const exp = Buffer.from(expected);
    return got.length === exp.length && crypto.timingSafeEqual(got, exp);
  });
}

// Use the RAW body: re-serialised JSON won't match the signature.
app.post('/orcaflo', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verify(req.body.toString('utf8'), req.get('OrcaFlo-Signature'), SECRET)) return res.sendStatus(401);
  const event = JSON.parse(req.body);
  if (seen.has(event.id)) return res.sendStatus(200);  // at-least-once: dedupe on event.id
  seen.add(event.id);
  if (event.type === 'contact.lifecycle_updated') console.log(event.data.contact.name, '→', event.data.to);
  res.sendStatus(200);                                 // answer fast; do slow work in a queue
});
app.listen(3000);

Event types

contact.created
A new contact (first message, lead form, API, panel)
contact.updated
Contact details changed; changed + previous values
contact.lifecycle_updated
Moved to another lifecycle stage (from, to)
contact.temperature_updated
The AI rated the lead hot, warm or cold
contact.tag_added
contact.tag_removed
A tag was added or removed
contact.merged
contact.unmerged
Contacts were merged into one, or a merge was undone
contact.deleted
A contact was deleted
message.received
A customer sent a message
message.sent
A message went out (AI, team, follow-up, broadcast, API)
message.failed
A message could not be delivered
conversation.escalated
The AI handed the chat to a human
conversation.resolved
Your team resolved the escalation
conversation.bot_paused
conversation.bot_resumed
Human takeover switched on or off
conversation.summary_updated
The AI rewrote its conversation summary
conversation.opened
conversation.closed
Team inbox: a conversation opened (new or returning customer) or was closed
conversation.assigned
Team inbox: assigned to a teammate or team (or unassigned)
conversation.sla_first_response
conversation.sla_resolved
conversation.sla_breached
Team inbox reply-time targets met or missed
appointment.booked
appointment.rescheduled
appointment.cancelled
appointment.completed
appointment.no_show
Booking lifecycle; Calendly and Cal.com bookings carry external with their id
order.created
order.paid
order.cancelled
Payment links, cash on delivery, manual sales
broadcast.completed
A broadcast finished sending
ad_conversion.sent
A conversion was reported to Meta for ad optimisation (only once a business connects Meta Conversions API)
incoming_webhook.received
One of your incoming webhook URLs got data

Full payloads with samples: https://api.orcaflo-panel.com/v1/event-types.

Connect Claude or ChatGPT

OrcaFlo runs a hosted MCP server, so an AI assistant can work with your conversations: find customers waiting for a reply, read a chat and its summary, update a contact, check free slots, assign or close conversations (team inbox) and, if you allow it, send a reply.

Server URL: https://api.orcaflo-panel.com/mcp

  • Claude: Settings → Connectors → Add custom connector, paste the URL and click Connect.
  • ChatGPT: turn on developer mode for connectors, create a new connector and paste the URL.
  • Other MCP clients: add a remote (Streamable HTTP) server with the URL. Clients that can't do OAuth can send an API key as Authorization: Bearer ….

You'll sign in with your OrcaFlo login and see exactly which permissions the assistant asks for; untick any you don't want (for example sending messages). Only an admin can approve, and you can disconnect it any time under Manage → Developers → AI assistants.

Custom channels

Use a messaging app OrcaFlo doesn't support yet. Your bridge forwards each customer message to us; the AI answers through your callback URL, with the same inbox, CRM, follow-ups and handover to your team as WhatsApp.

# Create the channel (scope channels:manage). The secret is shown once.
curl -X POST https://api.orcaflo-panel.com/v1/channels -H "Authorization: Bearer $ORCAFLO_API_KEY" -H "Content-Type: application/json" \
  -d '{"name":"My app","callback_url":"https://example.com/orcaflo-callback"}'
# → { "id": "…", "status": "active", "secret": "chsec_…", "inbound_url": "…/v1/channels/…/inbound" }

# Each reply arrives at your callback, signed with OrcaFlo-Signature:
{ "type": "message.outbound", "id": "…", "channel_id": "…",
  "to": { "id": "user-42" }, "message": { "type": "text", "text": "Yes, we're open until 8pm!" } }

A complete bridge:

// A minimal bridge: your messaging app ⇄ OrcaFlo's AI.  npm i express
import express from 'express';
import crypto from 'node:crypto';

const API = 'https://api.orcaflo-panel.com/v1';
const KEY = process.env.ORCAFLO_API_KEY;           // scope channels:inbound
const CHANNEL = process.env.ORCAFLO_CHANNEL_ID;    // from POST /channels
const SECRET = process.env.ORCAFLO_CHANNEL_SECRET; // chsec_… from POST /channels
const app = express();

// 1. Your app → OrcaFlo: forward every customer message.
app.post('/from-my-app', express.json(), async (req, res) => {
  const { userId, userName, messageId, text } = req.body;
  const r = await fetch(`${API}/channels/${CHANNEL}/inbound`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ from: { id: userId, name: userName }, message: { id: messageId, text } }),
  });
  res.sendStatus(r.status === 202 ? 200 : 502);    // 503 = retry later; message ids dedupe
});

// 2. OrcaFlo → your app: the AI's replies arrive here (your callback_url).
app.post('/orcaflo-callback', express.raw({ type: 'application/json' }), async (req, res) => {
  const raw = req.body.toString('utf8');
  const parts = String(req.get('OrcaFlo-Signature')).split(',');
  const t = Number((parts.find((p) => p.startsWith('t=')) || 't=0').slice(2));
  const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${raw}`).digest('hex');
  if (Math.abs(Date.now() / 1000 - t) > 300 || !parts.includes('v1=' + expected)) return res.sendStatus(401);
  const { to, message } = JSON.parse(raw);
  const sentId = await sendInMyApp(to.id, message.text ?? message.url); // your own send function
  res.json({ message_id: sentId });
});

app.listen(3000);

Custom channels are in early access: ask us to switch them on for your business.

Incoming webhooks

Create a private URL under Manage → Developers → Incoming webhooks. Anything POSTed to it (JSON, form or text, up to 64 KB) becomes an incoming_webhook.received event you can react to with webhooks or workflows.

Team inbox, templates and workflows

  • Assign and close (team inbox): GET /users and GET /teams for the ids, then POST /conversations/{id}/assign with user_id and/or team_id (null clears it), /close and /reopen. Conversations carry status (open, closed, snoozed), assignee_user_id and team_id.
  • WhatsApp templates: GET /whatsapp/templates lists approved templates; POST /messages/template sends one, also outside the 24-hour window. Only for businesses on the official WhatsApp Business API; others get 422 templates_unavailable.
  • Workflows: GET /workflows, then POST /workflows/{id}/runs with contact_id for workflows whose trigger is "Inbox shortcut" or "Started by another workflow" (can_start_from_api). GET /workflows/{id}/runs shows how they went.
  • Bookings with staff: if GET /services says staff_mode: true, the business books per service and staff member. Pass service_id (and optionally staff_id) to GET /appointments/availability; each slot lists the free staff_ids. POST /appointments takes service_id (or the service name as service) and picks a free staff member if you don't. Appointments carry service_id and staff_id; bookings made in Calendly or Cal.com are moved and cancelled there too.
  • CRM links: contacts include crm_ids (for example { "hubspot": "1234" }), and GET /contacts?hubspot_id= or ?salesforce_id= finds the OrcaFlo contact for a CRM record.

Zapier, Make and n8n

OrcaFlo has its own apps with the same events as triggers, plus actions to send messages and templates, create or update contacts, tag, set lifecycle stages, assign and close conversations, start workflows and book appointments. They aren't in the stores' directories yet, so add them directly:

  • Zapier: accept the OrcaFlo invite while logged in to Zapier, then connect with an API key (Zapier / Make / n8n preset).
  • Make: install the OrcaFlo app while logged in to Make, then create a connection with an API key (Zapier / Make / n8n preset).
  • n8n (self-hosted): Settings → Community Nodes → Install n8n-nodes-orcaflo (npm). n8n Cloud needs n8n's verification, which is in progress.

Step by step: Connect OrcaFlo to Zapier, Make or n8n.

OrcaFlo's own workflow builder uses the same names as the API and webhooks, so a value means the same thing everywhere:

  • {{contact.phone}} = the API's contact.phone: + and digits for phone channels (e.g. +971501234567), empty for others. {{contact.phone_e164}} is the same value; {{contact.phone_key}} is OrcaFlo's internal conversation key (e.g. 971501234567 or ig:…). API paths take the contact's id ({{contact.id}}).
  • {{contact.custom_fields.<key>}} = the API's custom_fields.
  • Workflow triggers Lead temperature changed, Conversation assigned and Message sent fire on the same changes as the events contact.temperature_updated, conversation.assigned and message.sent.

API reference

The full reference is an OpenAPI 3.1 document: https://api.orcaflo-panel.com/v1/openapi.json. Import it into Postman or your SDK generator.

Your AI sales assistant, free for 14 days. Then from $14.99 a month.Start now