The REST API lets your own systems run flows, read results, start and stop campaigns, and read or add table rows. Outgoing webhooks call your URL when something happens in a workspace. Both come with Pro and up (feature api). Keys and webhooks live in Settings › API keys and Settings › Webhooks (owners and admins).
API keys
| Format | moo_ + 43 random characters, shown once when made; stored as its SHA-256 |
| Role | Member (runs flows, changes only what its creator made) or Admin (everything but billing), from the permission table |
| Expiry | 30, 90 or 365 days, or never |
| Lives as long as | its creator is in the workspace (a frozen seat can only read) |
| Rate limit | per key a minute, by plan: Pro 60, Team 300, Business 1,000, Enterprise unlimited (limit.api.minute); over it answers 429 with Retry-After: 60 |
| Limit | 50 keys per workspace |
Send it as Authorization: Bearer moo_…. Creating and deleting keys is in the audit log; calls made with a key appear there as API key <name>.
Endpoints
Base https://<your site>/api/v1. Every answer is { "data": … } or { "error": "…" }. The OpenAPI 3.1 document is at /api/v1/openapi.json (no key needed): import it into Postman or Insomnia.
| Method | Path | Permission | Does |
|---|---|---|---|
| GET | /flow | flow view | Flows, newest first (?limit= ≤ 100, ?before=<id>) |
| POST | /flow | flow create | A flow from { name, graph? } (the graph Studio saves; none: a Manual trigger), within the plan's flow limit |
| GET | /flow/<id> | flow view | One flow with its draft and published graph |
| POST | /flow/<id>/publish | flow publish | Publishes the current draft (a version in its history, audit flow.publish) |
| GET | /flow/<id>/run | run view | The flow's runs, newest first (?status=, `?dry=0 |
| POST | /flow/<id>/run | flow run | Runs the published version for real from { triggerId? } (default: its first trigger) with { input } as the trigger's output. 202 { runId } |
| GET | /run | run view | The workspace's runs on every engine it uses, newest first (?status=FAILED,CANCELLED, `?dry=0 |
| GET | /campaign/<id>/run | run view | The campaign's runs, newest first (same filters) |
| GET | /run/<id> | run view | Status, error, each step's output (node.<id>.output); ?log=1 adds the first 200 logs |
| GET | /campaign | campaign view | Campaigns (on/off, the run going now) |
| GET | /campaign/<id> | campaign view | One campaign with its options and schedule |
| PATCH | /campaign/<id> | campaign run | { isEnabled, isBrowserClose? }: on (runs in its schedule window) or off (stops what's going) |
| GET | /table/<id>/row | dataset view | Rows, newest first (?limit= ≤ 1000, ?where={"column":"value"}) |
| POST | /table/<id>/row | dataset edit | Adds { rows: [{ column: value }] } (1 to 500) |
| GET | /variable | dataset view | Variables the key may use (secret ones without their value) |
| GET | /member | member view | Members: name, email, role, state |
Errors: 400 invalid body, 401 missing/unknown/expired key or its creator left, 402 plan without the API (or a plan limit), 403 the role can't, 404 not found (also another workspace's ids), 409 not runnable (unpublished, no trigger), 423 suspended, 429 rate limit, 502 the engine, 503 maintenance.
curl -X POST https://mooflair.com/api/v1/flow/6ae1…/run -H "Authorization: Bearer moo_…" -H "Content-Type: application/json" -d '{"input":{"url":"https://example.com"}}'
The code: src/controllers/api/access.ts (apiAccess, the key's workspaceAccess), routes in src/app/api/v1. Creating, publishing and starting go through the same controllers as the app (flowCreate, flowPublish, flowStart, campaignEnable).
Webhooks
A webhook is a URL plus the events it hears. We POST JSON:
{ "id": "<delivery id>", "event": "run.finished", "createdAt": "2026-10-29T09:52:11.000Z", "workspace": { "id": "…", "slug": "acme" }, "data": { "run": { "id": "…", "status": "SUCCEEDED", "error": null, "isPriority": false }, "flow": { "id": "…", "name": "…" }, "campaign": null } }
| Event | When | data |
|---|---|---|
run.finished | A real run succeeded | run, flow, campaign |
run.failed | A real run failed or was cancelled | run (with error), flow, campaign |
intervention.waiting | A real run waits for a person | run, flow, campaign, intervention.title |
campaign.started | A campaign started a real run | campaign, run.id, isResume |
campaign.stopped | A campaign was turned off or stopped | campaign, reason |
invoice.paid | An invoice was paid (gateway or manual) | invoice (id, number, currency, total in minor units, reason) |
member.joined | Someone accepted an invite | member (userId, name, email, role) |
ping | Send a test | message |
Test runs never send events. Run events come from the engine (POST /api/internal/event, see ).
Checking the signature
Headers: X-Mooflair-Event, X-Mooflair-Delivery (the delivery id, the same on every retry: use it to skip duplicates) and X-Mooflair-Signature: t=<unix seconds>,v1=<hex>, where v1 = HMAC-SHA256(secret, "<t>.<raw body>"). The secret (whsec_…) is shown once when the webhook is made or rolled (New signing secret).
import { createHmac, timingSafeEqual } from "node:crypto";
const [t, v1] = header.split(",").map((part) => part.split("=")[1]);
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const isValid = timingSafeEqual(Buffer.from(expected), Buffer.from(v1)) && Math.abs(Date.now() / 1000 - Number(t)) < 300;
Delivery and retries
Answer 2xx within 10 seconds. Anything else is retried by the job queue (webhook/deliver, 6 tries with doubling backoff from 30 seconds, about 15 minutes). Deliveries shows the last 30 (status, tries, the last answer); they're kept 30 days. Redirects aren't followed. In production a URL must be https on a public address.
The code: src/controllers/webhook/queue.ts (webhookQueue, the one way to send an event), src/lib/job/webhook/deliver, schemas webhook and webhook/delivery.
FAQ
Do I need a paid plan for the API?
Yes. API keys and webhooks come with Pro, Team, Business and Enterprise. On Free, the Settings tabs show an Upgrade.
Can a key see another workspace?
No. A key belongs to one workspace; ids from another workspace answer 404.
What happens to a key when its creator leaves?
It stops working at once (401). Make keys for systems from an account that stays, or make a new one.
Can I run a draft through the API?
No. The API runs the published version, like triggers and campaigns. Publish with POST /flow/<id>/publish first.
How do I avoid handling an event twice?
Store X-Mooflair-Delivery (or id in the body) and skip ids you've seen: a retry sends the same id.