Developers
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.
ORCAFLO_API_KEY). Never put it in a website or app your customers download.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 🚚' }),
});Every request sends an API key as a bearer token:
Authorization: Bearer ofk_live_…403 missing_scope.| Scope | Allows |
|---|---|
contacts:readcontacts:write | Read contacts, tags, custom fields, lifecycle stages / create, update, tag and delete them |
conversations:readconversations:write | Conversations, summaries, teammates and teams / assign, close and reopen (team inbox) |
messages:readmessages:send | Message history / send messages, pause and resume the AI |
appointments:readappointments:write | Free slots and bookings / book, reschedule, cancel |
orders:read | Orders and payment status |
products:readproducts:write | Product catalogue |
broadcasts:readbroadcasts:write | Broadcasts |
workflows:readworkflows: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:managechannels:inbound | Custom channels / post customer messages into one |
https://api.orcaflo-panel.com/v1, HTTPS only. JSON in and out, UTC ISO-8601 timestamps, phone numbers in international format (+971…).limit (1–100, default 25) and cursor; each response has next_cursor (null on the last page).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.POST /contacts plus start_conversation: OrcaFlo writes an opener and sends it at a safe pace during business hours.| Limit | Applies 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 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 } }| Status | type | When |
|---|---|---|
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. |
500502 | 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. |
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": { … } }
}410 unsubscribes the endpoint."<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.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);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_addedcontact.tag_removed | A tag was added or removed |
contact.mergedcontact.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_pausedconversation.bot_resumed | Human takeover switched on or off |
conversation.summary_updated | The AI rewrote its conversation summary |
conversation.openedconversation.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_responseconversation.sla_resolvedconversation.sla_breached | Team inbox reply-time targets met or missed |
appointment.bookedappointment.rescheduledappointment.cancelledappointment.completedappointment.no_show | Booking lifecycle; Calendly and Cal.com bookings carry external with their id |
order.createdorder.paidorder.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.
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
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.
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.
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.
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.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.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.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_ids (for example { "hubspot": "1234" }), and GET /contacts?hubspot_id= or ?salesforce_id= finds the OrcaFlo contact for a CRM record.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:
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.contact.temperature_updated, conversation.assigned and message.sent.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.