Webhook management lives under Webhooks in the workspace sidebar, at /webhooks. Doing it from the dashboard keeps the audit trail tied to a real human, and every write there asks for a verification code: creating, editing, deleting, rotating the secret and revealing it are all guarded, and one code covers the lot for an hour. The dashboard is not the only door, though: the same endpoints are exposed on the REST API and in the SDK to any API key carrying the webhooks scope, which is none until you grant it. The one thing an API key can never do is read an existing secret back.
Permissions
| Permission | Owner | Admin | Viewer |
|---|---|---|---|
webhooks:read | ✅ | ✅ | ✅ |
webhooks:create | ✅ | ✅ | ❌ |
webhooks:update | ✅ | ✅ | ❌ |
webhooks:delete | ✅ | ✅ | ❌ |
webhooks:rotate | ✅ | ✅ | ❌ |
webhooks:secret_read | ✅ | ✅ | ❌ |
Custom roles can mix and match all six via the Roles page. API keys are narrower: the webhooks scope grants webhooks:read at read level and create, update, delete and rotate at write level. webhooks:secret_read is not in that map, so reading a secret back stays a dashboard action.
Creating an endpoint
- Open Webhooks, then click Create webhook. Pressing
canywhere on that page opens the same modal. - Fill in name, endpoint URL, optional description, and at least one event type.
- Save. Unless you verified in the last hour, the code dialog opens here, and the create is rerun for you once you clear it.
- The endpoint is enabled the moment it is created and starts collecting matching deliveries.
The URL is validated on the server and must be https. Plain http, a URL carrying a username or password, and hosts that resolve to loopback, private, link-local or shared address space are all refused, as are localhost, .internal, .local, .localdomain, .home.arpa and the cloud metadata hosts.
The dashboard event picker offers 47 of the 48 event types the API accepts. subscription.duplicate_detected is missing from it, so subscribe to that one over the REST API or an SDK. GET /webhooks/event-types always returns the full catalogue.
The signing secret
Creating an endpoint mints an ev_whsec_ secret from 24 random bytes. It is stored encrypted rather than hashed, so it stays recoverable:
- Nothing shows the secret at creation time. The modal closes on a "Webhook created" toast, and the secret waits in the table.
- The Secret column renders a masked
ev_whsec_chip. Clicking it asks for a verification code, then copies the plaintext secret straight to your clipboard. The value is never printed on screen. - Losing your copy is not a reason to rotate. Copy it again from that column.
- The REST API is stricter. It returns the secret only in the create and rotate responses, and has no endpoint that reads it back.
Editing
The edit modal covers name, URL, description and subscribed events. Enable and disable is a separate item in the same dropdown, not a field in the modal. Both routes go through the same guarded update endpoint, so an edit and a disable each ask for a code when your hour has run out.
Edits apply per attempt, not per event. The delivery worker re-reads the endpoint row on every attempt, so a retry of a delivery that was queued before your edit goes to the new URL and is signed with the current secret.
Rotating the signing secret
- Open the dropdown on the webhook row and click Rotate secret.
- Type the endpoint name to confirm.
- Enter the verification code. It is a 6 character code emailed to you, or, with two factor authentication on, the 6 digits from your authenticator app and no email at all. One code elevates you for an hour and covers the other guarded actions in the workspace inside that window.
- The old secret stops signing immediately. Copy the new one from the Secret column and update your receiver.
Disabling vs deleting
- Disabling stops the fan-out: new events no longer create deliveries for that endpoint. It does not park what is already queued. Those rows are marked failed on their next attempt with the error
Endpoint disabled. - Deleting is soft. The row is flagged deleted and forced disabled, disappears from the list, and any queued deliveries fail the same way. It asks for the endpoint name plus a verification code, and there is no undo.
Viewing deliveries
The dropdown on any webhook row has View deliveries, which opens the Recent deliveries drawer, newest first. Each row shows:
- Event type and status badge (Succeeded, Failed or Pending; queued and in-flight attempts both render as Pending)
- HTTP response code, when the receiver answered at all
- Attempt number and the time of the last attempt
- Error message, when there is one
- Resend, which needs
webhooks:update
Resend does not retry the row in place. It inserts a fresh delivery for the same event with the attempt count back at zero, so the full five-attempt schedule starts over. It is refused when the endpoint is disabled or deleted, and once the workspace already holds 500 deliveries that are pending or processing.
Limits
| Limit | Value |
|---|---|
| Max endpoints per workspace | 20 |
| Webhook name length | 1 to 64 characters |
| URL length | up to 2048 characters, https only |
| Description length | up to 200 characters |
| Request timeout | 10 seconds |
| Max delivery attempts | 5 |
| Backoff after a failed attempt | 30 s, 2 min, 10 min, 30 min |
| Max pending or processing deliveries per workspace | 500 |
| Deliveries per page | 15 by default, selectable up to 100 |
Errors the dashboard surfaces
| Condition | Status |
|---|---|
Acting without the matching webhooks:* permission | 401 |
Create, edit, delete, rotate or secret read without a live elevation. error.code is auth.step_up_required | 403 |
| Invalid name, URL, description or event type | 400 |
| Editing an endpoint that was already deleted | 400 |
| Creating the 21st endpoint | 409 |
| Rotate, delete, secret read or deliveries on an unknown endpoint | 404 |
| Resending onto a disabled or deleted endpoint | 404 |
| Resending past 500 queued deliveries | 409 |