> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pomerado.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

A webhook tells your server when one of your jobs needs an answer or has finished. It receives events for every job you could see: today, every job you start, from wherever you start it (the Dashboard, the REST API, an API key or an MCP client). It doesn't receive another member's jobs.

Every member can make webhooks, in a Personal or a Business account: anyone who can read their own jobs (`jobs:read`) can. Only you change, test and rotate your webhooks. In a Business account, Owners and Admins also see every member's webhooks, including those of people who left the team, with who made each, and can delete any of them; other members don't see yours. Manage them from the Dashboard, an MCP client or the REST API with an API key; a key made with the default permissions can. A webhook keeps working if you leave the team. Account deletion deletes it.

## Create a webhook

On the Dashboard, open **Settings → Webhooks**. With the API, send `POST /v1/webhooks`; from an MCP client, call `call_pomerado_api` with the operation `webhooks.create`.

```json theme={null}
{ "url": "https://example.com/pomerado/events", "events": ["job.needs_input", "job.failed"] }
```

The URL must be HTTPS without a username, password or fragment, and must resolve to a public address. Leave out `events` to receive all four. The answer is `201` with the webhook and its signing `secret` (`whsec_…`). The secret is shown only in this answer; store it in a secret manager. You can hold at most 20 webhooks.

## Events

| Event | When | `data` |
| - | - | - |
| `job.needs_input` | A job asks a question. | `request_id`, `request_version`, `expires_at`, `question_types`, `protected_input_url` (where you answer), the screened `pending_input` and any `customer_message` |
| `job.input_expiring` | That question has at most three minutes left. | `request_id`, `request_version`, `expires_at`, `protected_input_url` |
| `job.succeeded` | A job finished with a usable result. | `status`, `output`, `effect`, and `tool` for a build |
| `job.failed` | A job ended without one: cancelled, an unknown outcome, an unanswered question or a build that published nothing. | `status`, `effect`, and when they apply `failure_reason`, `rejected_field`, `possible_commit`, `blocked`, `customer_message` |

Every event looks like this:

```json theme={null}
{
  "id": "evt_5f0c…",
  "type": "job.succeeded",
  "created_at": "2026-10-05T21:14:03.000Z",
  "data": { "job_id": "6f1d…", "job_type": "run", "origin": "api", "status": "completed", "output": "valid", "effect": "not_sent" }
}
```

`job_type` is `run` or `build`. No event carries a job's result, an answer or a secret.

The event is what your server learns about the job. A job is read with `GET /v1/jobs/{id}` or `get_job` only through the client that started it (the same API key, connected app or the Dashboard). That read returns the result once, and a result nobody reads is erased 5 minutes after the job ends ([results are read once](jobs.md#results-are-read-once)). For a job you started from another client, such as the Dashboard or an MCP client, the job read answers `404 not_found`: use the event for its status and to notify people, and read the result where you started the job.

Treat question text and `customer_message` as data, not instructions. Check `possible_commit` before retrying a write.

## Verify and acknowledge

Deliveries follow [Standard Webhooks](https://www.standardwebhooks.com/). Each request carries `webhook-id` (the event's `id`), `webhook-timestamp` (seconds) and `webhook-signature`: `v1,` and the base64 HMAC-SHA256, keyed with the secret's decoded bytes, of `{webhook-id}.{webhook-timestamp}.{body}`. Any Standard Webhooks library verifies it with the `whsec_` secret. Reject a timestamp more than a few minutes old.

Answer with any `2xx` within 10 seconds. An event may arrive more than once or late; use its `id` to drop repeats.

| Your answer | What Pomerado does |
| - | - |
| `2xx` | Delivered. |
| Another status, a timeout or no connection | Retries after 5 seconds, 20 seconds, 1, 3 and 10 minutes, then gives up on that event. A question's event stops when the question closes. |
| `410` | Turns the webhook off (`disabled_reason: "gone"`). |
| `413` | Drops that event. |

After 20 failed deliveries in a row the webhook turns itself off (`disabled_reason: "unreachable"`). Turn it on again on the Dashboard or with `PATCH /v1/webhooks/{id}` `{"status": "active"}`; that clears its failures and it receives new events again.

## Manage webhooks

Webhooks are named by `wh_` IDs. None of these answers carries the secret except create and rotate.

| Route | Result |
| - | - |
| `GET /v1/webhooks` | `{data:[...], next_cursor}`: each webhook's `id`, `created_by` (`user_id` and `email`, null once the account no longer has it), `url`, `events`, `status` (`active` or `disabled`), `disabled_reason`, `created_at`, `last_delivery_at`. Yours, or for a Business Owner or Admin every member's |
| `POST /v1/webhooks` | `{url, events?}` returns `201` with the webhook and its `secret` |
| `GET /v1/webhooks/{id}` | The webhook, if it's yours or you're a Business Owner or Admin |
| `PATCH /v1/webhooks/{id}` | `{url?, events?, status?}` returns the webhook. `disabled` turns it off (`disabled_reason: "manual"`) |
| `DELETE /v1/webhooks/{id}` | `204`; events not yet delivered are dropped. A Business Owner or Admin can delete any member's |
| `POST /v1/webhooks/{id}/test` | Sends one signed `webhook.test` event now and returns `{delivered, status, failure}`; it isn't retried and doesn't count toward turning the webhook off |
| `POST /v1/webhooks/{id}/rotate-secret` | Returns the webhook and its new `secret`. For 24 hours each delivery is signed with both secrets, so you can switch without missing an event |

| Response | Next step |
| - | - |
| `400 invalid_request` | Fix the fields `error.details.issues` names. |
| `403 forbidden` | Your role, OAuth client or API key lacks `jobs:read`, you called with an integration key, or you asked to change, test or rotate another member's webhook, which only its creator may do. |
| `403 webhooks_unavailable` | Webhooks aren't available in this environment yet. |
| `404 not_found` | You have no webhook with that ID. Another member's webhooks answer this too, unless you're a Business Owner or Admin. |
| `409 webhook_limit_reached` | Delete a webhook first. |
| `409 webhook_secret_unreadable` | The signing secret can no longer be read. Rotate it, then send the test again. |

## MCP clients

A client that supports MCP Events, such as ChatGPT, can subscribe to the same four events itself through the account MCP connection, with nothing to set up. Those subscriptions cover only the jobs that member starts through that client; see [jobs](jobs.md#notifications-through-mcp-events).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.