Webhooks
Signed POSTs to your endpoint when boards change, retried for a day, with a seven day log. And callbackUrl for jobs.
A webhook tells your server when something happens, instead of your server asking. MockFlow sends a signed POST with a JSON body to your endpoint: a board was created, renamed, trashed or restored, someone joined or left a space, a job you started finished. Each delivery is retried for a day if your server is down, and kept in a log for seven days.
Add a webhook
In the Webhooks tab of the developer console, or with POST /webhooks. Give an https address on a public host, the events you want, and optionally one board; leave the board out for every board in the workspace. The answer carries the signing secret, once; the console can reveal it again.
curl -X POST https://api.mockflow.com/v1/webhooks \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://yourapp.com/mockflow/webhook", "events": ["board.created", "member.*"]}'
Managing webhooks needs boards:read. A webhook only hears about boards its creator can open, and whoever edits a webhook becomes its creator, so a webhook never reports more than the person behind it could read.
Events
Subscribe to types one by one, to a family such as board.*, or to * for everything, including events added later. GET /webhooks/events returns the same list, for a picker.
Event types
board.createdBoardA board was created, in the app or through the API.
board.updatedBoardA board was renamed, locked or unlocked, or moved to another space.
board.deletedBoardA board was moved to the trash.
board.restoredBoardA board came back from the trash.
member.added{ email, role, spaceId, boardId }Someone was given access to a space or a board.
member.removed{ email, spaceId, boardId }Someone lost access to a space or a board.
job.succeededJobA job started through the API finished: an AI generation, an edit, a skill or an export.
job.failedJobA job started through the API failed.
webhook.paused{ webhookId, url, reason }Another webhook in the workspace was paused after its deliveries kept failing.
What arrives
{
"id": "evt_51ac",
"type": "board.created",
"createdAt": "2026-10-07T09:14:03.000Z",
"boardId": "brd_7f3e",
"spaceId": null,
"actor": {
"email": "ann@acme.com"
},
"data": {
"id": "brd_7f3e",
"title": "Q4 launch plan",
"spaceId": "spc_2a1d",
"url": "https://app.mockflow.com/board/7f3e",
"locked": false,
"createdBy": "ann@acme.com",
"createdAt": "2026-10-07T09:14:03.000Z"
}
}
data is a Board or an Item exactly as the REST endpoints return it, a Job, or the event's own shape from the table above. It is read a few seconds after the change, when the event is delivered, so it shows the object as it is then; the event's type says what happened.
Headers
MockFlow-Signaturestringrequiredt=<unix seconds>,v1=<hex>. See below.
MockFlow-Event-IdstringrequiredThe event. The same on every retry and replay: use it to ignore one you have seen.
MockFlow-Event-TypestringrequiredThe event's type, for routing before you parse the body.
MockFlow-Delivery-IdstringrequiredThis attempt's delivery, as the log names it.
MockFlow-Webhook-IdstringThe webhook it was sent for. Absent on a callback.
Check the signature
Compute an HMAC-SHA256 of the timestamp, a dot and the raw request body, with your signing secret, and compare it with v1 in constant time. Refuse a timestamp more than five minutes old, so a captured request cannot be replayed to you later. Use the raw body: parsing and re-serialising the JSON changes the bytes.
const crypto = require('crypto');
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = crypto.createHmac('sha256', secret).update(parts.t + '.' + rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ''));
}
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split('=', 1) for p in header.split(','))
if abs(time.time() - int(parts['t'])) > 300:
return False
expected = hmac.new(secret.encode(), parts['t'].encode() + b'.' + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get('v1', ''))
Answering, retries and pauses
Answer 2xx within 10 seconds, then do the work. Anything else, or no answer, is a failure, and MockFlow tries again after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours and then every 10 hours, until a day has passed since the first try. If a delivery still fails then, the webhook is paused, its creator is emailed, and webhook.paused goes to the workspace's other webhooks. Fix the endpoint, send a test, and turn it back on.
Answer 410 Gone to say you no longer want a webhook: it is paused at once, without retries. Redirects are not followed.
Events about one board usually arrive in the order they happened, but a retry can overtake a later event. Order by createdAt when it matters, and use the event id to ignore a duplicate.
The delivery log
The console keeps seven days of deliveries per webhook, each with its payload, what your endpoint answered and how long it took, and a Replay to send one again. The same is in the API: GET /webhooks/{webhookId}/deliveries, POST .../deliveries/{deliveryId}/replay, and POST /webhooks/{webhookId}/test for a ping event.
callbackUrl: hear when a job finishes
Every request that answers with a job also takes an optional callbackUrl: generating a board with Mida, POST /boards with a prompt, creating, changing or deleting items, converting, applying a skill and exporting. When the job finishes or fails, a short body is POSTed there once, with the same retries: the event id, the job's status (succeeded or failed), the board's id and URL, and imageUrl, a picture of what the job drew (the generated section, the created item, the export), or null when there is none. It is signed with the workspace's callback secret, from the console or GET /webhooks/callback-secret. A DELETE takes it as ?callbackUrl=. For the full job, read GET /jobs/{jobId}.
{
"eventId": "evt_9c2d",
"status": "succeeded",
"boardId": "brd_7f3e",
"boardUrl": "https://app.mockflow.com/board/7f3e",
"imageUrl": "https://assets.mockflow.com/app/wireframepro/fileexport/c41a.png"
}
Whether the callback got through is on the job itself: GET /jobs/{jobId} carries a callback with the delivery's status, attempts and the endpoint's answer. The console's Webhooks tab lists every callback sent under Callbacks sent, with a Replay, and GET /webhooks/callbacks is the same list.
curl -X POST https://api.mockflow.com/v1/boards/brd_7f3e/ai/generate \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "A launch plan", "callbackUrl": "https://yourapp.com/mockflow/done"}'
Building a Zapier integration
Triggers are REST hooks: on subscribe, POST /webhooks with Zapier's target URL and the trigger's event; on unsubscribe, DELETE /webhooks/{webhookId}; for sample data, the trigger's event as it appears in the delivery log. If a Zap is deleted while its webhook still exists, Zapier answers 410 and the webhook pauses itself.
A long action, such as generating a board, uses Zapier's callback: pass z.generateCallbackUrl() as the request's callbackUrl, and the Zap continues when the job is done, with the board's URL and imageUrl.
Changes to the items on a board (a sticky note added, a card moved, a comment) are not events yet. Read a board's items with GET /boards/{boardId}/items when a board event or a job tells you it changed.