Skip to content
MockFlow DevelopersGet an API key

Signed POSTs to your endpoint when boards change, retried for a day, with a seven day log. And callbackUrl for jobs.

Updated 8 Oct 20266 min read

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.createdBoard

A board was created, in the app or through the API.

board.updatedBoard

A board was renamed, locked or unlocked, or moved to another space.

board.deletedBoard

A board was moved to the trash.

board.restoredBoard

A 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.succeededJob

A job started through the API finished: an AI generation, an edit, a skill or an export.

job.failedJob

A 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-Signaturestringrequired

t=<unix seconds>,v1=<hex>. See below.

MockFlow-Event-Idstringrequired

The event. The same on every retry and replay: use it to ignore one you have seen.

MockFlow-Event-Typestringrequired

The event's type, for routing before you parse the body.

MockFlow-Delivery-Idstringrequired

This attempt's delivery, as the log names it.

MockFlow-Webhook-Idstring

The 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.

Was this page helpful?

Sign in to MockFlow

The same account you use in the app and on the API.

Forgot your password?
or
Continue with GoogleContinue with MicrosoftContinue with Apple