# Webhooks

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

Source: https://mockflow.com/developers/concepts/webhooks

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](/developers/console/?tab=webhooks), 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.

```bash
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.created` (Board): A board was created, in the app or through the API.
- `board.updated` (Board): A board was renamed, locked or unlocked, or moved to another space.
- `board.deleted` (Board): A board was moved to the trash.
- `board.restored` (Board): 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.succeeded` (Job): A job started through the API finished: an AI generation, an edit, a skill or an export.
- `job.failed` (Job): 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

```json
{
  "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-Signature` (string, required): `t=<unix seconds>,v1=<hex>`. See below.
- `MockFlow-Event-Id` (string, required): The event. The same on every retry and replay: use it to ignore one you have seen.
- `MockFlow-Event-Type` (string, required): The event's `type`, for routing before you parse the body.
- `MockFlow-Delivery-Id` (string, required): This attempt's delivery, as the log names it.
- `MockFlow-Webhook-Id` (string): 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.

```javascript
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 || ''));
}
```

```python
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.

> **Note** 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](/developers/reference) 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}`.

```json
{
  "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.

```bash
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`.

> **Note** 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.
