Developers

The CompanyFlowHQ API

Send leads in from any website or app, post to social media from your own tools, and get told the moment something happens with webhooks. Included with the Growth, Pro and Social Agency plans.

Getting started

  1. In CompanyFlowHQ, go to Settings → Developers and create an API key. Tick only the permissions it needs.
  2. Copy the key straight away — it starts with cfhq_live_ and is shown once.
  3. Call the API from your server (never from a web page or app a customer can see).

Base address: https://companyflowhq.com/api/v1. Requests and responses are JSON (UTF-8). Times are ISO 8601 in UTC. Money is in the smallest unit (pence for GBP).

Authentication

Send the key in the Authorization header:

curl https://companyflowhq.com/api/v1/leads -H "Authorization: Bearer cfhq_live_YOUR_KEY"

Permissions (scopes):

ScopeLets the key…
leads:readRead leads
leads:writeAdd and update leads, add notes
social:readRead social accounts and posts
social:writeCreate social posts and upload pictures
bookings:readRead bookings
invoices:readRead invoices
ads:readRead Facebook & Instagram ad campaigns and results
webhooks:manageManage webhooks
hooks:manageZapier / Make instant triggers
deals:readRead deals
deals:writeAdd deals
invoices:writeCreate draft invoices
messages:sendSend emails, texts and WhatsApp messages
tickets:writeOpen support requests
kitchen:readRead kitchen orders
kitchen:writeSend in kitchen orders and change their status

Keys can be revoked at any time and can be set to expire. We store only a fingerprint (SHA-256) of each key.

Endpoints

MethodPathScopeWhat it does
GET/api/v1/leadsleads:readList leads, newest first. Filters: stage, email, updated_since.
POST/api/v1/leadsleads:writeAdd a lead (same duplicate check and auto-replies as your website form).
GET/api/v1/leads/{id}leads:readOne lead.
PATCH/api/v1/leads/{id}leads:writeChange details, stage or priority.
POST/api/v1/leads/{id}/notesleads:writeAdd a note to the lead's timeline.
GET/api/v1/social/accountssocial:readYour connected social accounts.
POST/api/v1/social/postssocial:writeCreate a post for one or more accounts: now, at a time, next queue slot or as a draft.
GET/api/v1/social/posts/{id}social:readA post's status and the result on each network.
POST/api/v1/mediasocial:writeUpload a picture (JPEG, PNG, WebP or GIF, up to 20 MB). Returns its id.
GET/api/v1/bookingsbookings:readBookings. Filters: from, to, status.
GET/api/v1/invoicesinvoices:readSent invoices, quotes and credit notes. Filters: status, kind.
POST/api/v1/leads/upsertleads:writeFind a lead by email or phone and update it, or add it if it's new.
GET/api/v1/leads/searchleads:readFind a lead by ?email= or ?phone=.
POST/api/v1/leads/{id}/tasksleads:writeAdd a task (title, due_at, assign_to_email).
POST/api/v1/leads/{id}/sequencesleads:writeStart an automatic follow-up sequence ({ sequence_id }).
GET/api/v1/sequencesleads:readYour follow-up sequences.
POST/api/v1/messagesmessages:sendEmail, text or WhatsApp a lead. Consent, opt-outs and quiet hours are respected.
POST/api/v1/invoicesinvoices:writeCreate a draft invoice or quote (it is never sent automatically).
GET/api/v1/invoices/searchinvoices:readFind an invoice by ?number=.
GET / POST/api/v1/dealsdeals:read / deals:writeList or add deals.
GET/api/v1/deals/pipelinesdeals:readDeal pipelines and their stages.
POST/api/v1/ticketstickets:writeOpen a support request.
GET/api/v1/meanyYour workspace and the key's owner (connection test).
POST / DELETE/api/v1/hooks, /api/v1/hooks/{id}hooks:manageREST hooks: subscribe a URL to one event, or unsubscribe (used by Zapier and Make).
GET/api/v1/triggers/{event}hooks:manageThe latest items for an event, in the same shape as a delivery (?limit=3).
GET / POST/api/v1/webhookswebhooks:manageList or add webhooks.
PATCH / DELETE/api/v1/webhooks/{id}webhooks:manageChange, switch off or delete a webhook.

Leads example

Add a lead (curl)

curl -X POST https://companyflowhq.com/api/v1/leads \
  -H "Authorization: Bearer cfhq_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: form-2026-09-27-000123" \
  -d '{
    "first_name": "Amara",
    "last_name": "Perera",
    "email": "amara@example.com",
    "phone": "+44 7700 900123",
    "requirement": "MSc Data Science, January intake",
    "consent_marketing": true
  }'

Returns 201 with { "data": { …lead } }. If the person is already in your workspace (same email or phone), you get 200 with "duplicate": true and the existing lead. Sending the same Idempotency-Key twice never creates two leads.

Move a lead on (JavaScript)

const res = await fetch("https://companyflowhq.com/api/v1/leads/LEAD_ID", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.CFHQ_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ stage: "qualified", priority: "hot" }),
});
if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message}`);
}
const { data: lead } = await res.json();

Social posts example

Upload a picture first (optional), then create the post. Each account can have its own text.

# 1. Upload a picture
curl -X POST https://companyflowhq.com/api/v1/media \
  -H "Authorization: Bearer cfhq_live_YOUR_KEY" \
  -F "file=@open-day.jpg"
# → { "data": { "id": "MEDIA_ID", ... } }

# 2. Schedule the post
curl -X POST https://companyflowhq.com/api/v1/social/posts \
  -H "Authorization: Bearer cfhq_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Open day this Saturday, 10am to 2pm. Everyone welcome!",
    "media_ids": ["MEDIA_ID"],
    "accounts": [
      { "account_id": "FACEBOOK_ACCOUNT_ID" },
      { "account_id": "LINKEDIN_ACCOUNT_ID", "text": "Join our open day this Saturday (10:00–14:00)." }
    ],
    "schedule": { "mode": "at", "at": "2026-10-03T08:00:00Z" }
  }'

schedule.mode is now, at, queue (next free slot) or draft. Add "require_approval": true to send it for approval in CompanyFlowHQ instead. Posts follow the same rules as in the app, including client approval for agencies. Check progress with GET /api/v1/social/posts/{id} — results lists each network's status, link and any error.

Pagination

Lists return up to limit items (default 25, most 100), newest first:

{ "data": [ … ], "has_more": true, "next_cursor": "WyIyMDI2LTA5…" }

Pass ?cursor= with next_cursor to get the next page. Stop when has_more is false.

Webhooks

Add a webhook in Settings → Developers (or with the API). We POST JSON to your HTTPS address when something happens:

EventWhen
lead.createdA new lead arrives
lead.updatedA lead's details or stage change
booking.createdA booking is made
invoice.paidAn invoice is paid in full
social.post.publishedA social post is published
social.post.failedA social post fails
social.post.milestoneA social post passes a views milestone (1,000, 2,500, 5,000…)
chat.lead_createdA website chat becomes a lead
lead.stage_changedA lead moves to another stage
lead.wonA lead is marked as won
booking.cancelledA booking is cancelled
quote.acceptedA customer accepts a quote
deal.wonA deal is won
deal.stage_changedA deal moves to another stage
form.responseSomeone fills in a form or survey
ticket.createdA new support request arrives
review.feedbackA customer leaves private feedback
message.receivedA new message arrives in the Inbox
kitchen.order.createdA new kitchen order arrives
kitchen.order.updatedA kitchen order is accepted, ready, collected or cancelled
POST /your-webhook
Content-Type: application/json
X-CompanyFlow-Event: lead.created
X-CompanyFlow-Delivery: 5f0c…
X-CompanyFlow-Signature: t=1790000000,v1=6b1f…

{ "id": "evt_…", "type": "lead.created", "created": "2026-09-27T10:00:00.000Z",
  "data": { "lead": { "id": "…", "first_name": "Amara", … }, "source": "Website" } }

Answer with any 2xx status within 10 seconds. Anything else counts as a failure and we try again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. After 20 failures in a row the webhook is switched off and the owner is emailed. Redirects are not followed, and addresses must be public (no private or local networks). The same event can occasionally arrive twice — use id to ignore repeats.

Zapier & Make

Connect CompanyFlowHQ to 7,000+ apps — Xero, QuickBooks, Slack, Google Sheets and more — without code.

  1. In Settings → Developers, press Create Zapier key (or Create Make key) and copy the key.
  2. In Zapier, choose the CompanyFlowHQ app and paste the key when asked. In Make, add the CompanyFlowHQ app (or use its HTTP module with the header Authorization: Bearer cfhq_live_…).
  3. Pick a trigger such as New lead, Invoice paid or Deal won, then an action in the other app.

Triggers are instant: when a Zap is turned on, Zapier subscribes with POST /api/v1/hooks { "event": "lead.created", "target_url": "https://hooks.zapier.com/…" } and unsubscribes with DELETE /api/v1/hooks/{id} when it's turned off. Deliveries use the same format, signing and retries as webhooks; answering 410 Gone removes the subscription. You can see and remove active triggers in Settings → Developers.

Checking signatures

Every message is signed with your webhook's secret (starts with whsec_). The signature is an HMAC-SHA256 of <t>.<raw body>. Always check it, and reject messages older than 5 minutes.

Node.js (Express)

import crypto from "node:crypto";
import express from "express";

const app = express();
app.post("/companyflow-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("X-CompanyFlow-Signature") ?? "";
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto
    .createHmac("sha256", process.env.CFHQ_WEBHOOK_SECRET)
    .update(`${t}.${req.body}`)
    .digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  const ok =
    fresh && v1 && v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
  if (!ok) return res.status(400).send("Bad signature");
  const event = JSON.parse(req.body);
  // … handle event.type / event.data …
  res.sendStatus(200);
});

Cloudflare Workers / Deno / browsers (Web Crypto)

async function verify(secret, header, rawBody) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const key = await crypto.subtle.importKey(
    "raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
  const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${t}.${rawBody}`));
  const hex = [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
  return hex === v1; // use a constant-time compare in production
}

Rate limits

Each key can make 120 requests a minute. Every response has X-RateLimit-Limit and X-Request-Id. Over the limit you get 429 with a Retry-After header — wait that many seconds and try again.

Errors

Errors always look the same:

{ "error": { "code": "insufficient_scope", "message": "This API key doesn't have the “leads:write” permission." } }
StatusCodeMeaning
400invalid_request / invalid_json / invalid_cursorSomething in the request needs fixing — the message says what.
401unauthorized / revoked / expiredThe key is missing, wrong, revoked or expired.
402plan_requiredThe workspace's plan doesn't include the API.
403insufficient_scope / key_owner_inactiveThe key lacks the permission, or the person who made it has left.
404not_foundNothing with that id in your workspace.
409conflictIt can't be changed right now (for example a post that has already gone out).
413payload_too_largeJSON bodies must be under 256 KB; pictures under 20 MB.
415unsupported_media_typeUpload a JPEG, PNG, WebP or GIF.
429rate_limitedMore than 120 requests a minute. Wait for the Retry-After seconds.
500server_errorOur fault. Try again; quote the X-Request-Id if it keeps happening.

Questions? Email support@companyflowhq.com.