Docs
API reference
Basics
- URL
https://api.webhookadmin.com/v1- Authentication
Authorization: Bearer sk_live_…orsk_test_…. A key reads and writes only its own environment- Format
- JSON. Times are Unix milliseconds; IDs are prefixed strings
- Body limit
- 1.1MB (413
too_largeabove that) - Lists
?cursor=&limit=(limit1–100, default 50). Returns{ items, next_cursor }
List cursors and message or delivery IDs are accepted only from the oldest month within your retention period to the current month (422 for an out-of-range cursor, 404 for an ID).
Errors
{ "error": "<code>", "message": "<description>" }| HTTP | error | Meaning |
|---|---|---|
| 401 | unauthorized | Missing, wrong, revoked or expired key |
| 402 | plan_limit | Plan limit reached |
| 403 | forbidden | The key lacks the scope |
| 404 | not_found | Not found |
| 409 | conflict | Conflicts with existing data |
| 409 | endpoint_disabled | The endpoint is disabled. Fix it and re-enable it, then retry |
| 413 | too_large | Body larger than 1.1MB |
| 422 | invalid | Invalid input |
| 429 | rate_limited | Rate limit exceeded |
Rate limits
Requests per minute, counted per environment across all keys.
| Plan | Per minute |
|---|---|
| Free | 600 |
| Starter | 3,000 |
| Pro | 12,000 |
| Business | 20,000 |
| Response header | Meaning |
|---|---|
x-ratelimit-limit | Requests per minute |
x-ratelimit-remaining | Remaining in the current minute |
x-ratelimit-reset | When the current minute ends (Unix seconds) |
retry-after | Only on 429. Seconds until you can retry |
Endpoints
Send a message
POST /v1/messagesScope: messages:send
| Input | Type | Description |
|---|---|---|
consumer | string | The consumer’s external_id (up to 200 characters) |
event_type | string | Event type. Letters, digits and _ . : -, up to 100 characters |
payload | JSON | The body to deliver. Up to 1MB |
transformations_params | object | Optional. Passed to the transformation as webhook.transformationsParams (up to 4KB as JSON). The spelling transformationsParams is accepted too |
Idempotency-Key (header) | string | Optional. 1–256 characters |
Returns 202 { id, deliveries }, where deliveries is the number of deliveries created.
- The same
Idempotency-Keyreturns the sameidfor 24 hours (the first message, even if the body differs). - If no consumer matches
consumer, one is created (with no endpoints, sodeliveriesis 0). - A message with
deliveriesof 0 (no endpoint) is stored with the statusno_endpointbut not sent, and it does not count toward your monthly quota. Adding an endpoint later does not send it. - The
x-usage-monthresponse header holds your organization’s production messages this month (UTC), counting only messages that had an endpoint. - Messages from test environments and test sends are free, up to 1,000 per environment per day (UTC). Beyond that you get 402
plan_limit.
List messages
GET /v1/messagesScope: logs:read
Query: status (pending / retrying / success / failed / cancelled / no_endpoint), event_type, q, since (Unix milliseconds; messages created at or after this time), cursor, limit.
no_endpoint means the message had no endpoint to go to when it was accepted (the consumer has no endpoint, or none of its endpoints takes the event type). It is stored but not sent, and it does not count toward your monthly quota. Adding an endpoint later does not send it. Before 2026-10-05 these messages were returned as pending (messages accepted earlier are now returned as no_endpoint too).
q matches part of the message ID, the event type, the consumer’s external_id or the Idempotency-Key. It does not search payloads.
With q, status or event_type and no since, only messages from the last 7 days are returned.
Returns { items: MessageRow[], next_cursor }.
Get a message
GET /v1/messages/{id}Scope: logs:read
Returns MessageDetail (the payload, and deliveries and attempts per endpoint).
Retry a delivery
POST /v1/deliveries/{id}/retryScope: messages:retry
id is a Delivery ID. Returns 202.
- Returns 409
conflictwhile the delivery is in progress (pending,retrying). - Returns 409
endpoint_disabledand does not accept the retry when the endpoint is disabled (disabled). Fix the endpoint and setstatus: activewith Update an endpoint first.deliveries[].endpoint_statusinMessageDetailtells you in advance. - For a paused endpoint (
paused) the retry is accepted and held until you resume it. Returns 404not_foundif the endpoint was deleted.
List consumers
GET /v1/consumersScope: logs:read
Query: cursor, limit. Returns { items: Consumer[], next_cursor }.
Create a consumer
POST /v1/consumersScope: consumers:write
| Input | Type | Description |
|---|---|---|
external_id | string | Your ID for this customer (up to 200 characters) |
name | string | Optional. Up to 200 characters |
Returns 201 Consumer. Returns 409 conflict if the external_id already exists.
Delete a consumer
DELETE /v1/consumers/{id}Scope: consumers:write
Deletes the consumer, its endpoints (signing secrets, transformations and polling tokens) and the sources that deliver to it (with their request logs). This cannot be undone. Returns 204. Returns 404 for a missing consumer or one in another environment.
- Portal links for the consumer stop working immediately, even before they expire.
- Deliveries not yet sent (including pending retries) are not sent. They are recorded as failed, and messages waiting on a polling endpoint are cancelled (
cancelled). - Delivery logs are deleted with the rest of the day once it is past your plan’s retention period. Monthly message counts are kept.
- Sending a message to the same
external_idcreates a new consumer with no endpoints.
Create a customer portal link
POST /v1/consumers/{id}/portalScope: endpoints:write
| Input | Type | Description |
|---|---|---|
frame_origin | string | Optional. Origin of the page that embeds the portal in an iframe (https:/, no path) |
locale | ja or en | Optional. Portal language. Defaults to the browser language |
Returns 201 { url, expires_at }. The url expires in 15 minutes and can be opened any number of times until then. Returns 402 plan_limit on the Free plan and 404 for a consumer in another environment.
Embed it in your app (src is the returned url):
<iframe
src="https://app.webhookadmin.com/portal/eyJlIjoi…?lang=ja"
title="Webhook"
allow="clipboard-write"
style="display: block; width: 100%; height: 900px; border: 0"
></iframe>- Only the
frame_originorigin can embed the portal. A link withoutframe_origindoes not render inside an iframe (Content-Security-Policy: frame-ancestors). - The portal runs on the token in the link, not on cookies, so it works in an iframe on another domain, Safari included.
- Consumers can add, edit, pause and delete endpoints, choose event types, show and rotate the signing secret, send a test message, and view and retry deliveries.
- Add the query parameter
theme=darkto theurlfor the dark theme.
List endpoints
GET /v1/endpointsScope: logs:read
Query: consumer_id (optional). Returns { items: Endpoint[] }.
Create an endpoint
POST /v1/endpointsScope: endpoints:write
| Input | Type | Description |
|---|---|---|
consumer_id | string | The consumer id (con_…) |
url | string | Endpoint URL (up to 2,000 characters). For type http (the default) |
type | string | Optional. http (default), polling (Polling endpoints; neither url nor destination) or a destination other than a webhook (Destinations). webhook and pollingEndpoint are accepted too |
destination | object | Settings and credentials of a destination other than a webhook, instead of url |
event_types | string[] | Optional. Event types to receive. All when omitted |
fixed_ip | boolean | Optional. Deliver from the static source IP (209.71.107.233) |
description | string | Optional. Up to 200 characters |
retry | object | Optional. Retry count and interval (Retry policy). The default when omitted |
compat_signature | object | Optional. Compatibility signature (Signature verification) |
ordering | string | Optional. Delivery order (Ordered delivery): none (default) or fifo |
body_format | string | Optional. Shape of the body: standard (default, { type, timestamp, data }) or raw (data as is) |
secret | string | Optional. A signing secret you already use (whsec_…). A new one is generated when omitted (Migrating from other services (webhooks)) |
Returns 201 Endpoint with secret (the signing secret, returned only here) and poller_url (the poller URL; null for http).
- URLs use https on port 443 or 8443.
- Raw IP addresses, internal host names, names that resolve to private addresses and names that do not resolve return 422.
- A consumer can have up to 20 endpoints. Beyond that you get 422.
Get an endpoint
GET /v1/endpoints/{id}Scope: logs:read
Returns Endpoint with recent: AttemptRow[], retrying_count, waiting_count (deliveries waiting their turn; only with ordering: fifo, otherwise 0) recovery (the latest bulk resend, or null) and poller_url (for a polling endpoint; null for http).
Update an endpoint
PATCH /v1/endpoints/{id}Scope: endpoints:write
Input (all optional): url, event_types, status (active / paused), description, retry, compat_signature, ordering, body_format. Returns Endpoint.
- Messages to a
pausedendpoint are held, and held messages from this month and last month are sent when it is set back toactive. - An automatically disabled endpoint (
disabled) resumes when you setstatustoactive. - Set
retrytonullto go back to the default, andcompat_signaturetonullto remove it. - Setting
orderingfromfifoback tononesends the deliveries that were waiting their turn right away. typecannot be changed after creation (422).- The signing secret (
secret) cannot be set here (422). Use Rotate the signing secret.
Delete an endpoint
DELETE /v1/endpoints/{id}Scope: endpoints:write
Returns 204.
Rotate the signing secret
POST /v1/endpoints/{id}/rotate-secretScope: endpoints:write
Returns { secret }. Deliveries carry a signature with the old secret as well for 24 hours.
Send a test message
POST /v1/endpoints/{id}/testScope: messages:send
Sends one test message to this endpoint only. The request body is optional.
| Input | Type | Description |
|---|---|---|
event_type | string | Optional. Default webhook.test |
payload | object | Optional. The data to send (a JSON object, up to 1 MB). Lets you send a sample of a real event to just this endpoint |
- Without
payload, webhook endpoints receive{ "test": true, "endpoint_id": … }. Chat endpoints (Slack, Teams, Chatwork, Google Chat) receive a message titled “Test message” saying “This is a test message from Webhook Admin. Notifications are reaching this destination.” (in Japanese when the organization’s notification language is Japanese). - Transformations apply at delivery, as for any other message.
- Test sends do not count toward the monthly limit. Like messages in the test environment, they are limited to 1,000 a day per environment.
- Returns 409
endpoint_disabledwhen the endpoint is disabled (disabled).
Returns 202 { message_id }. Check the result with Get a message.
Test a destination
POST /v1/destinations/testScope: endpoints:write
| Input | Type | Description |
|---|---|---|
endpoint_id | string | Optional. Use the saved settings and credentials of this endpoint |
type | string | Required without endpoint_id. The destination type |
destination | object | Required without endpoint_id, with credentials. Overrides the saved settings when given |
fixed_ip | boolean | Optional. Send from the static source IP. The endpoint’s setting when omitted |
Returns { ok, via, response_status, duration_ms, error, response_head, ref }. Sends one webhook.test message right away and records nothing. Chat destinations (Slack, Teams, Chatwork, Google Chat) get the same “Test message” as Send a test message. Counts toward the daily limit of test sends. Pro plan and above.
Get the transformation
GET /v1/endpoints/{id}/transformationScope: logs:read
Returns { endpoint_id, code, updated_at, enabled, variables }. 404 not_found when there is none.
Save the transformation
PUT /v1/endpoints/{id}/transformationScope: endpoints:write
| Input | Type | Description |
|---|---|---|
code | string | JavaScript that defines function handler(webhook), up to 51,200 characters (Transformations). Required when there is no transformation yet |
enabled | boolean | Optional. false keeps the code but stops using it; true turns it back on |
variables | object | Optional. Values passed as webhook.env (string values, up to 50 entries and 4KB as JSON). null clears them |
Returns { endpoint_id, code, updated_at, enabled, variables }. Omitted fields keep their current values. Pro plan and above (saving only enabled: false works on any plan). Code that does not compile, has no handler or is empty returns 422 (fields.code).
Remove the transformation
DELETE /v1/endpoints/{id}/transformationScope: endpoints:write
Returns 204. Messages are sent without it from the next attempt. Available on any plan.
Test the transformation
POST /v1/endpoints/{id}/transformation/testScope: endpoints:write
| Input | Type | Description |
|---|---|---|
code | string | Optional. Code to run. Omit to use the saved transformation |
payload | any | Optional. Sample data. Default { "test": true, "endpoint_id": … } |
event_type | string | Optional. Default webhook.test |
variables | object | Optional. Values for webhook.env. The saved ones when omitted |
transformations_params | object | Optional. Value for webhook.transformationsParams |
Returns one of { result: "send", method, url, headers, payload, changed, logs }, { result: "cancel", logs } or { result: "error", error: { kind, message }, logs }. Nothing is sent or saved. logs holds console.log output.
Poll
GET /v1/poller/{endpoint_id}Scope: Poller token (sk_poll_…), not an API key
| Query | Type | Description |
|---|---|---|
iterator | string | Optional. The iterator of the previous response. Omit it to start at the beginning of the retention period (the day the endpoint was created) |
limit | number | Optional. 1 to 250 (default 50) |
event_type | string | Optional, repeatable. Narrows the endpoint’s event_types further |
after | string | Optional. An ISO 8601 time. Without iterator, start from that time |
channel | string | Optional. Accepted, but does not filter anything, as there are no channels yet |
Returns { data: PolledMessage[], iterator, done }, in the order the messages were accepted. done: true means nothing more right now. The receiver calls it with Authorization: Bearer sk_poll_… (Polling endpoints).
The compatible routes are listed in Migrating from other services (polling endpoints).
List poller tokens
GET /v1/endpoints/{id}/poller-tokensScope: logs:read
Returns { items: PollerToken[] } in creation order. The token strings are not returned.
Create a poller token
POST /v1/endpoints/{id}/poller-tokensScope: endpoints:write
Input: name (optional, up to 100 characters). The body can be omitted.
Returns 201 PollerToken with token (sk_poll_…, returned only here). Starter plan and above. Up to 5 per endpoint (422 beyond that). 404 for an http endpoint.
Delete a poller token
DELETE /v1/endpoints/{id}/poller-tokens/{token_id}Scope: endpoints:write
Returns 204. The token stops working immediately.
Resend failed deliveries
POST /v1/endpoints/{id}/recoverScope: messages:retry
| Input | Type | Description |
|---|---|---|
since | number | Unix milliseconds. Messages created at or after this time are covered. Within your retention period, not in the future |
Returns 202 Recovery. Check progress with Get a bulk resend.
- Resends every delivery to this endpoint that ended
failed, as the same message (samewebhook-id) (Bulk resend). - Returns 409
endpoint_disabledif the endpoint is disabled and 409conflictif it is paused. Re-enable it first. - Only one can run per endpoint. Returns 409
conflictwhile one is running.
Get a bulk resend
GET /v1/endpoints/{id}/recoveries/{recovery_id}Scope: logs:read
Returns Recovery. status is running, done, or stopped (the endpoint was paused, disabled or deleted before it finished).
List event types
GET /v1/event-typesScope: None (any key of the environment)
Returns { items: EventType[] }, sorted by name (not paged).
Create an event type
POST /v1/event-typesScope: endpoints:write
| Input | Type | Description |
|---|---|---|
name | string | Event type. Letters, digits and _ . : -, up to 100 characters |
description | string | Optional. Up to 200 characters |
Returns 201 EventType. Returns 409 conflict if the name already exists.
Registered event types are offered as choices when an endpoint’s event types are set in the dashboard and the customer portal.
List sources
GET /v1/sourcesScope: logs:read
Returns { items: Source[], next_cursor } in creation order.
Create a source
POST /v1/sourcesScope: endpoints:write
| Input | Type | Description |
|---|---|---|
name | string | 1 to 100 characters |
provider | string | stripe, github, shopify, slack, meta, zoom, standard, hmac, basic, api_key or none |
consumer_id | string | ID of the consumer that receives the messages |
secret | string | Optional. The signing secret (up to 500 characters); whsec_… for standard. It can also be set later with Set the signing secret |
config | object | Optional. SourceConfig (Source settings) |
status | string | Optional. active (default) or paused |
Returns 201 Source; url is the ingress URL. Returns 402 plan_limit beyond your plan’s number of sources, and 404 if the consumer does not exist.
Get a source
GET /v1/sources/{id}Scope: logs:read
Returns Source. The signing secret is shown only as a masked hint (secret_hint).
Update a source
PATCH /v1/sources/{id}Scope: endpoints:write
Input: name, provider, consumer_id, status, config (all optional). Returns Source.
- Changing
providerremoves the signing secret. Set the new service’s secret with Set the signing secret. - When you change
providerwithoutconfig, the defaults of thatproviderapply. Passingconfigreplaces it. - Set the signing secret with Set the signing secret (passing
secrethere returns 422). status: pausedholds incoming requests, andactiveresumes and delivers the held requests (Pausing).
Delete a source
DELETE /v1/sources/{id}Scope: endpoints:write
Returns 204. The ingress URL returns 404 from then on, and the request log is deleted.
Set the signing secret
PUT /v1/sources/{id}/secretScope: endpoints:write
| Input | Type | Description |
|---|---|---|
secret | string | null | The signing secret shown by the service (up to 500 characters): whsec_… for standard, printable ASCII without spaces otherwise. null removes it |
Returns Source. After a change, requests signed with the previous secret are still accepted for 24 hours. null removes every secret, including the previous one.
List a source’s requests
GET /v1/sources/{id}/requestsScope: logs:read
Query (all optional): cursor, limit, and status to filter by accepted, duplicate, rejected or held. Returns { items: SourceRequest[], next_cursor }, newest first, within your retention period.
status | Meaning |
|---|---|
accepted | Accepted. message_id is the message that was created |
duplicate | A redelivery with the same delivery ID. message_id is the earlier message |
rejected | Rejected, with the reason in reason |
held | Received while the source was paused, and processed when it resumes |
methodis the request method (POST,PUT,PATCH,DELETEorGET).processed_atis when a held or rejected request was processed later (nulluntil then).- Requests with
reprocessable: truecan be reprocessed. headersholds the received headers, except authentication, cookies and similar.
Reprocess a rejected request
POST /v1/sources/{id}/requests/{request_id}/reprocessScope: messages:send
Verifies a rejected request again with the source’s current signing secret and settings, then processes it (Reprocessing). Returns the updated SourceRequest.
- An accepted request becomes
accepted(orduplicate) and counts toward your monthly quota. On a paused source it becomesheld. - If verification fails again, it stays
rejectedwith a newreasonandprocessed_at. - Returns 409 for requests that are not rejected or have no stored body (
reprocessable: false), and 404 for requests past your retention period.
Ingress URL
POST /in/{token}Scope: The ingress URL (no API key)
Where other services send their webhooks (Sources). Accepts POST, PUT, PATCH and DELETE (GET only for Meta’s subscription check). Bodies up to 10 MB. The signature is verified, then a message is created for the source’s consumer.
| HTTP | Meaning |
|---|---|
| 200 | Accepted: { "id": "msg_…" }, or your custom response. A redelivery with the same delivery ID returns the same ID. A paused source holds the request and returns { "status": "held", "request_id": "sreq_…" }. URL checks from Slack, Zoom and Meta get the response each service expects |
| 401 | Wrong or missing signature, timestamp more than 5 minutes off, no signing secret set, or wrong Basic credentials or API key (Basic adds WWW-Authenticate: Basic) |
| 402 | More sources than the plan allows |
| 403 | The organization is suspended, or hub.verify_token does not match in Meta’s subscription check |
| 404 | Unknown ingress URL |
| 405 | A method other than POST, PUT, PATCH or DELETE (except GET for meta) |
| 413 | Body larger than 10 MB |
| 429 | More than 1,000 requests in 10 seconds to one source, or a none source over the per-minute API rate limit (both with retry-after), the monthly quota, or the daily limit of a test environment |
Rejections return { "error": "<reason>" }, for example invalid signature, invalid credentials, invalid API key, timestamp outside the tolerance or signing secret is not set.
Retry policy
Choose the retry count and interval per endpoint, in the dashboard, the API (retry), MCP or the customer portal.
| Field | Values |
|---|---|
count | 0, 1, 2, 3, 5, 7, 12 (retries after the first send; 0 means no retry) |
interval | progressive (5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 10 h, then every 10 h), or a fixed 1m, 5m, 30m, 1h, 6h |
{ "retry": { "count": 3, "interval": "5m" } }- The default is
count: 7withinterval: progressive(up to 8 attempts including the first, over about 28 hours).retry: nullgoes back to the default. - Combinations whose last retry comes more than 3 days after the first send are rejected (422). Only
progressivewith12does. - Automatic disabling after 5 days of failures is counted separately from the retry policy.
- While an endpoint keeps failing, we hold off sending to it for a while. Deliveries due in that time count as an attempt without being sent and are retried at the next time set by the retry policy. When retries run out, the delivery ends as failed.
Ordered delivery
Choose per endpoint to deliver messages one at a time, in the order they were accepted (ordering: fifo; the default is none). Set it in the dashboard, the API, MCP or the customer portal.
{ "ordering": "fifo" }- One delivery at a time. The next one is not sent until the previous one is delivered or ends as failed.
- The order is the order in which the API returned 202. A message sent after you received the previous 202 always arrives after it.
- While the previous delivery keeps failing, the later ones wait. It is retried per the retry policy, and when retries run out it ends as failed and the next one goes out.
- While deliveries are waiting, failure alerts include how many are waiting. The endpoint details show it too (
waiting_count). - Retrying a failed delivery (one by one or in bulk) sends it after the deliveries that are waiting now.
- Messages accepted while the endpoint is paused keep their order and go out from the first one when you resume.
- Switching back to
nonesends the waiting deliveries right away, in parallel. Messages accepted before you switched tofifoare not ordered. - Ordering is per endpoint. Other endpoints of the same message are not affected.
- Because deliveries go out one at a time, throughput depends on how fast the receiver responds.
Destinations other than webhooks
Set an endpoint’s type to something other than http to deliver to a queue, an event bus, object storage or a chat instead of a webhook. The body is the same as for a webhook endpoint (the Standard Webhooks JSON by default, or data as is with body_format: raw; chats get a message built from it). Pro plan and above; set it in the dashboard, the API, MCP or the customer portal.
{
"consumer_id": "con_…",
"type": "sqs",
"destination": {
"region": "ap-northeast-1",
"account_id": "123456789012",
"queue_name": "orders",
"credentials": { "access_key_id": "AKIA…", "secret_access_key": "…" }
}
}type | destination | credentials | Signature and IDs |
|---|---|---|---|
sqs | region, account_id, queue_name | access_key_id, secret_access_key | Message attributes |
eventbridge | region, event_bus (default default), source (default webhookadmin), detail_type (default: the event type) | Same as SQS | None |
pubsub | project_id, topic | service_account (the service account key JSON) | Message attributes |
s3 | region, bucket, prefix | Same as SQS | Object metadata |
r2 | account_id, bucket, prefix | R2 API token access key | Object metadata |
gcs | bucket, prefix | HMAC key or service_account | Object metadata |
azure_blob | account, container, prefix | account_key | Blob metadata (webhook_id and so on) |
servicebus | namespace, entity (queue or topic) | key_name, key (a SAS policy with Send) | Message properties |
kafka | url (Confluent Cloud REST endpoint; requests go to /, so a standalone REST Proxy is not supported), cluster_id, topic | api_key, api_secret | Record headers |
rabbitmq | url (HTTP API base), vhost (default /), exchange, routing_key | username, password | Message headers |
slack | None | webhook_url (a Slack Incoming Webhook URL) | No signature |
teams | None | webhook_url (a Teams Workflows URL) | No signature |
chatwork | room_id (the numeric chat ID) | api_token | No signature |
google_chat | None | webhook_url (a Google Chat space webhook URL) | No signature |
- The signature and IDs go where the destination can carry them, as
webhook-id,webhook-timestamp,webhook-signatureandevent-type. Receivers verify the body with the same SDKs as webhooks (chats get no signature). - Hosts are the providers’ official endpoints built from
regionand the like. Theurlof Kafka and RabbitMQ follows the endpoint URL rules (https, port 443 or 8443, no private addresses). - Object storage gets one object per message,
<prefix><year>/<month>/<day>/<message id>.json. Retries overwrite the same object. - Credentials are stored encrypted and never returned by the API; you only get a masked hint (
destination.credentials_hint). On update, omitcredentialsto keep the current ones. Changing the host of a Kafka or RabbitMQurlrequires new credentials. - Retries, the retry policy, ordered delivery, transformations (the payload and cancel only), failure alerts and the static source IP work as for webhooks. A 2xx that the destination did not accept (a failed EventBridge entry and the like) counts as a failure.
- An attempt’s
viais the type (sqsand so on) andrefis the ID the destination returned (SQS MessageId, object name, Kafka partition and offset and so on). - Use Test a destination to send one message before saving.
typealso accepts the compatible names (eventBridge,googleCloudPubSub,amazonS3,googleCloudStorage,azureBlobStorage,rabbitMq).
Chats (Slack, Teams, Chatwork, Google Chat)
Chats get a message instead of JSON. If the payload data (or the whole body with body_format: raw, or when a transformation removed data) has a string text, the message is built from title (default: the event type), text, lines (an array of strings, up to 50) and link (https URLs only). Otherwise the title is the event type and the body is the JSON of data, truncated (about 3,000 characters on Slack, 28 KB on Teams, 60,000 characters on Chatwork, 4,000 characters in total on Google Chat). The message ID is shown in small print.
{
"consumer_id": "con_…",
"type": "slack",
"destination": {
"credentials": { "webhook_url": "https://hooks.slack.com/services/T…/B…/…" }
}
}
// The payload data (or the transformed body)
{
"title": "Invoice paid",
"text": "INV-1024 was paid ($120.00)",
"lines": ["Customer: Example Inc.", "Method: card"],
"link": "https://example.com/invoices/INV-1024"
}- Slack: put the Incoming Webhook URL (
https://hooks.slack.com/services/T…/B…/…) inwebhook_url. - Teams: in the channel’s Workflows, create a flow from the “Post to a channel when a webhook request is received” template and put its URL (
*.powerplatform.comor*.logic.azure.com) inwebhook_url. Set who can trigger the flow to Anyone. Messages are sent as Adaptive Cards. Old Office 365 connector URLs (webhook.office.com) are not accepted. - Chatwork: issue a token from your name at the top right → Integrations → API Token and put it in
api_token, with the chat ID inroom_id. Plans other than personal ones need approval from the organization admin. - Google Chat: open the menu next to the space name → Apps & integrations → Add webhooks, create a webhook and put its URL (
https://chat.googleapis.com/v1/spaces/…/messages?key=…&token=…) inwebhook_url. The title is sent in bold and the link as<URL|host name>. Not available when your Google Workspace admin restricts webhooks. - A transformation such as
webhook.payload.data = { text: … }(orwebhook.payload = { text: … }withbody_format: raw) decides the message. - No signature is sent. The URL or API token itself is the secret.
- When the URL or token no longer works (Slack 403, 404, 410; Teams 401, 403, 404, 410; Chatwork 401, 403, 404; Google Chat 401, 403, 404), the message is not retried: the endpoint is disabled right away and the auto-disabled alert goes out with the reason (
disabled_reason). Fix the credentials and setstatusback toactive. - 400 and 413 fail that one message without retries (the endpoint stays active). 429 is retried no earlier than
Retry-After(Chatwork:x-ratelimit-reset), capped at 1 hour (via the static source IP, the normal retry schedule applies). 5xx and timeouts follow the normal retry schedule. - An attempt’s
refis the Chatwork message ID (nullfor Slack, Teams and Google Chat).
Migrating from other services (destinations)
POST /api/v1/app/{app_id}/destination takes a compatible destination shape (type, config, eventTypes, description) and creates an endpoint. app_id is the consumer id or external_id; authenticate with an API key (endpoints:write). Errors come back in the { code, detail } shape, with detail in English. For webhook endpoints, see Migrating from other services (webhooks).
Compatible type | This API | How config is read |
|---|---|---|
sqs | sqs | Region, account and queue from queueUrl; accessKeyId, secretAccessKey |
eventBridge | eventbridge | region, eventBusName, detailType, access key |
googleCloudPubSub | pubsub | projectId, topicId, credentials (service account key JSON) |
amazonS3 | s3 | bucket, region, access key |
googleCloudStorage | gcs | bucket, credentials (service account key JSON) |
azureBlobStorage | azure_blob | account, container, accessKey |
rabbitMq | rabbitmq | uri is the HTTP API URL (https:/), routingKey |
- Messages are sent one at a time.
batchSizeandmaxWaitSecsare accepted and ignored. - The body is the Standard Webhooks JSON by default (
dataas is withbody_format: raw), with the signature and IDs in attributes or metadata. Verify it with the same SDKs as webhooks. - RabbitMQ
amqp://andamqps://URIs are not supported. Pass the management plugin’s HTTP API URL. roleArnandexternalId(assuming an IAM role) andendpointUrlare not supported. Pass an access key.- There are no SNS, BigQuery, ClickHouse, Snowflake, Redshift, Postgres or OpenTelemetry destinations.
uidis ignored. The endpoint ID isep_…; read and change it with the endpoints API.
Transformations
Save a JavaScript function handler(webhook) on an endpoint to change the payload, headers, URL path and query or method right before each delivery, or to skip it. Pro plan and above; set it in the dashboard, the API or MCP.
function handler(webhook) {
const d = webhook.payload.data;
webhook.payload = { text: `Invoice ${d.id} paid (${d.amount})` };
webhook.headers['x-tenant'] = 'acme';
webhook.url = webhook.url + '?source=webhookadmin';
if (d.amount === 0) webhook.cancel = true;
return webhook;
}| Field | Description |
|---|---|
payload | The body to send: { type, timestamp, data }, or data as is for an endpoint with body_format: raw. Replace it with any JSON value (up to 1 MB) |
headers | Headers to add (up to 20, 8 KB in total). Headers used for delivery, such as webhook-* and content-type, cannot be set |
method | POST, PUT or PATCH |
url | Only the path and query can change; the scheme, host and port stay the same |
cancel | true skips the delivery. It becomes cancelled (not a failure, not retried) |
eventType | The event type (read only) |
env | The variables saved with the transformation (read only; {} when there are none) |
transformationsParams | The transformations_params sent with the message (read only; absent when not sent) |
- It runs on every attempt, from the original payload. The signature covers the transformed body.
- An exception, a limit, or an invalid return value fails the delivery without sending it, and it is not retried. The attempt’s
errorreadstransformation: <kind>: <detail>. Fix the code, then resend. - Each run has CPU and memory limits, and code is limited to 51,200 characters. There is no network access.
- A transformation accepts bodies up to 1 MB. A body over 1 MB received by a source is not transformed, and the delivery fails (Forwarding the original request).
variablesholds string values, up to 50 entries and 4 KB as JSON;transformations_paramsis up to 4 KB as JSON.- Saving
enabled: falsekeeps the code andvariablesbut delivers without transforming. Turning it off works on any plan. - Attempts where the transformation changed the request have
transform: "changed". - Use Test the transformation to see the result and
console.logoutput without sending anything. - Transformations are not shown in the customer portal.
Polling endpoints
An endpoint with type: polling has no URL. The receiver fetches its messages in the order they were accepted, with the endpoint’s poller token (sk_poll_…) and Poll. It works where webhooks can’t reach or no inbound port can be opened. Starter plan and above; create one in the dashboard, the API, MCP or the customer portal.
curl -H "Authorization: Bearer sk_poll_..." \
"https://api.webhookadmin.com/v1/poller/ep_...?iterator=<previous iterator>&limit=100"{
"data": [
{
"id": "msg_2026092912ab34cd56ef7800",
"event_type": "invoice.paid",
"timestamp": "2026-09-29T03:04:05.000Z",
"payload": { "type": "invoice.paid", "timestamp": "2026-09-29T03:04:05.000Z", "data": { "id": "inv_1" } },
"headers": { "webhook-id": "msg_…", "webhook-timestamp": "1790651045", "webhook-signature": "v1,…" }
}
],
"iterator": "MjAyNi0wOS0yOS4wLjE",
"done": true
}payload,idandheaders(webhook-id,webhook-timestamp,webhook-signature) are the same as a pushed webhook. The connection is protected by the token and TLS, so verifying the signature is optional; you can still checkJSON.stringify(payload)with the endpoint’s secret.- Calling with the previous response’s
iteratoracknowledges the previous page and marks those deliveriessuccess(the fetch shows as an attempt withvia: "poll"). Save theiteratorafter processing a page and continue from it. - Delivery is at least once. Calling with an older
iteratorreturns the same messages again, and calling without one starts from the beginning of the retention period, so deduplicate byidif needed. - A token can only read its own endpoint; other endpoints return 403. Up to 5 tokens per endpoint; deleting one revokes it immediately. Up to 600 calls per minute per endpoint.
- Messages not fetched within the retention period are deleted with the logs. If no one polls for the time set in your organization’s conditions (24 hours by default), a failure alert is sent.
- Retries, bulk resend, transformations, fixed IPs, compatibility signatures and
ordering: fifodo not apply (reading in order already keeps the order). - While the endpoint is paused, or after the plan drops below Starter, new messages are not stored. Deleting the endpoint cancels messages that were not fetched.
- When you filter with
event_type, the messages it skips are acknowledged by the next call too. - The
Pollerin the SDKs (Node.js, Python, Go, PHP) reads page after page and returns theiterator.
Migrating from other services (polling endpoints)
Receivers that poll in the compatible format keep working with the same shapes: point the server URL at https://api.webhookadmin.com and use an sk_poll_… token.
| Method | Path | Description |
|---|---|---|
| GET | / | { data: [{ id, eventType, payload, timestamp }], iterator, done }. payload is the data you sent, whatever the endpoint’s body_format. app_id is the consumer id or external_id |
| GET | / | We keep the position and move it forward on every read |
| POST | …/ | { after } (ISO 8601) → { iterator }. Moves the position to that time |
| GET | / | With the sender’s API key (endpoints:write): the current token { token, id, name, createdAt, expiresAt, scopes }, created if there is none |
| POST | …/ | { expiry?, oldTokenExpiry? } (seconds; the old token stops working after 300 seconds by default, or right away with 0) |
- Errors use
{ code, detail }, withdetailin English. An invaliditeratorreturns 400invalid_iterator. - The
event_type,channelandafterquery parameters are accepted too (Poll). - Endpoint types are
httpandpolling;pollingEndpointis accepted as well. - The v2 routes (offsets and commit) are not available.
- For webhook endpoints, see Migrating from other services (webhooks).
Migrating from other services (webhooks)
Each endpoint can keep its existing signing secret (whsec_…) and body format, so receivers of a Standard Webhooks sender can migrate without changing their settings or signature verification. function handler(webhook) transformation code needs no changes unless it relies on one of the differences listed below. For other destinations, see Migrating from other services (destinations); for polling endpoints, see Migrating from other services (polling endpoints).
| Previous service | Webhook Admin |
|---|---|
Signing secret (whsec_…) | Pass it as secret when creating the endpoint |
Body (data as is) | body_format: "raw" on the endpoint |
Sending a message (POST /) | POST /, with the consumer’s external_id as consumer |
Transformations (handler(webhook)) | Same function format, including webhook.env and webhook.transformationsParams |
{
"consumer_id": "con_…",
"url": "https://example.com/webhooks",
"secret": "whsec_…",
"body_format": "raw"
}secretiswhsec_followed by standard base64 that decodes to 24 to 75 bytes. Thewhsec_prefix is optional. Asymmetric keys (whsk_,whpk_) are not supported (422).- An imported secret is encrypted and stored like a generated one. Showing it and rotating it (the old secret keeps signing for 24 hours) work the same.
secretcan be passed when creating through the API, MCP or the SDKs. The dashboard and the customer portal generate a new one.body_formatcan be set in the dashboard, the API, MCP, the SDKs and the customer portal. Withraw, the signature is still inwebhook-id,webhook-timestampandwebhook-signature, computed as the Standard Webhooks specification defines.
Transformations can also be read and written through compatible routes (Pro plan and above).
| Method | Path | Description |
|---|---|---|
| GET | / | { code, enabled, variables, updatedAt }. With no transformation, code, variables and updatedAt are null and enabled is false (logs:read) |
| PATCH | Same path | { code?, enabled?, variables? } → 204. code is 10 to 51,200 characters; null removes it. Omitted fields keep their values (endpoints:write) |
| POST | …/ | { code, eventType, payload, variables? } → { url, payload, method, cancel }, where payload is the body as a string. Nothing is sent or saved (endpoints:write) |
app_idis the consumeridorexternal_id; authenticate with an API key.enabled: falsedelivers without transforming and keeps the code andvariables.- For an endpoint with
body_format: "raw",webhook.payloadisdataas is. - Errors on the compatible routes use
{ code, detail }withdetailin English. A 422 also hasfields, with camelCase field names. - Compatible routes that are not available here (sending messages, reading message attempts and so on) return 404
not_found. Send messages and read logs with the/v1API.
Main differences from the previous service:
- A transformation can change only the URL path and query, not the host.
- A transformation cannot set headers used for delivery, such as
webhook-*andcontent-type. - Destinations other than webhooks receive one message at a time, signed.
- Customers cannot write transformations in the customer portal; the sender sets them.
- A delivery skipped by a transformation is recorded as
cancelled(not a failure).
Bulk resend
Once an endpoint is fixed, resend every delivery to it that ended failed, for messages created since a time you choose. They go out as the same messages (same webhook-id), so the receiver can drop IDs it has already processed.
- Dashboard
- Endpoint details
- API
POST /v1/endpoints/{id}/recover(input and output)- MCP
recover_endpoint,get_recovery- Customer portal
- Customers can run it for their own endpoints
- Deliveries are queued gradually (50 every 10 seconds), and wait while the endpoint’s concurrency limit is full.
- It covers deliveries that had ended
failedwhen it started. Later failures and deliveries you retried one by one are not included. - Messages created while the endpoint was disabled are not covered, because no delivery to that endpoint was recorded. Messages held while it was paused are sent when it resumes.
- Pausing or deleting the endpoint midway ends it (
stopped). - Not counted in usage.
Failure alert conditions
Choose when failure alerts (Slack, Teams, Chatwork, email, webhook) are sent, per organization: in the dashboard under Failure alerts › When to alert, or with the MCP tool update_alert_conditions. The conditions apply to every alert destination.
| Condition | Options | Default |
|---|---|---|
| Delivery failures | Right after the first failure, or after failing for 5, 15 or 60 minutes | 60 minutes |
| Auto-disabled | When an endpoint is disabled after failing for 5 days | — |
| Retry backlog | When an environment has more than 100, 1,000 or 10,000 deliveries waiting to retry (once a day) | 1,000 |
| Usage | When this month’s messages reach 50%, 80% or 100% of your limit | 80% |
| Polling endpoint not polled | When no one has polled a polling endpoint for 1, 6, 24 or 72 hours (once until it is polled again) | 24 hours |
- Delivery failures are always alerted when retries run out, whatever you choose. At most one alert per endpoint per hour.
- Failure alerts to an organization are sent right away once per hour. Other endpoints that start failing within that hour are summarized in one alert every hour.
- The retry backlog is counted per environment, not per endpoint.
- Organization owners also get an email at 80% and 100% of the limit, regardless of these conditions.
Sources (receiving webhooks from other services)
A source receives webhooks from services such as Stripe, GitHub, Shopify, Slack, Meta, Zoom and LINE, verifies their signatures, and turns them into messages for the consumer you choose. Those messages are delivered to the consumer’s endpoints exactly like messages sent with POST /v1/messages, with retries, ordered delivery, transformations, static IPs, non-webhook destinations, failure alerts and bulk resend. Set sources up in the dashboard, the API or MCP.
POST /v1/sources
{ "name": "Stripe", "provider": "stripe", "consumer_id": "con_…" }
HTTP/1.1 201 Created
{ "id": "src_…", "provider": "stripe", "status": "active",
"url": "https://api.webhookadmin.com/in/…", "secret_hint": null, … }- Ingress URL
https://api.webhookadmin.com/in/{token}, one per source. Register it as the webhook URL in the other service (responses)- Consumer
- The consumer each source delivers to (
consumer_id). A common setup is a consumer for your own company, with your internal systems as its endpoints - Requests
- POST, PUT, PATCH and DELETE (GET only for Meta’s subscription check). Bodies up to 10 MB, as JSON, form data, plain text or binary
- Signing secret
- Copy it from the other service. The dashboard and API show only a masked hint (
secret_hint). After you change it, the previous secret keeps working for 24 hours - Rate
- There is no per-source rate limit. To prevent abuse, a source that receives more than 1,000 requests in 10 seconds returns 429 with
retry-after. Sources without verification (none) also count toward the per-minute API rate limit - Number of sources
- Free 1, Starter 5, Pro 20, Business unlimited (across all environments of the organization)
Stripe
- Create a source (
provider: stripe). The signing secret can be added later. - Copy the source’s ingress URL.
- In the Stripe Dashboard, open the Webhooks tab in Workbench and click Create an event destination. Choose the events, select Webhook endpoint as the destination type, and enter the ingress URL as the Endpoint URL.
- On the endpoint’s page, click Reveal secret, copy the signing secret (
whsec_…) and set it as the source’s secret (in the dashboard or with Set the signing secret).
GitHub
- Create a source (
provider: github) and set a hard-to-guess string as its signing secret. - In the repository or organization, open Settings › Webhooks › Add webhook. Enter the ingress URL as the Payload URL, choose
application/jsonas the Content type, and enter the same string as the Secret. - Choose the events and save. The
pingGitHub sends right away is accepted as a message with the event typeping.
Shopify
- Create a source (
provider: shopify). - Subscribe to webhooks from your app (
shopify.app.tomlor the Admin API) with the ingress URL as the destination. Webhooks created in the store admin under Settings › Notifications › Webhooks work the same way; choose JSON as the format. - For the signing secret, use your app’s client secret for app webhooks, or, for webhooks created in the store admin, the signing key shown below the webhook list on the same page.
Slack
- Create a source (
provider: slack) and set the Signing Secret from your Slack app’s Basic Information › App Credentials as its secret. - Turn on Event Subscriptions and enter the ingress URL as the Request URL. Slack’s verification request (
url_verification) is answered automatically, so the URL is verified right away. - Choose the events to subscribe to and save.
Meta (Facebook, WhatsApp, Instagram)
- Create a source (
provider: meta) and set the App secret from your app’s App settings › Basic as its signing secret. Choose a string for the subscription check and set it asconfig.verify_token. - In your app’s Webhooks settings (for WhatsApp, WhatsApp › Configuration, or Use cases › Customize › Configuration for apps created with the WhatsApp use case), enter the ingress URL as the Callback URL and the same string as the Verify token.
- Meta’s subscription check (a GET with
hub.mode=subscribe) is answered withhub.challengeas text/plain whenhub.verify_tokenmatchesconfig.verify_token. Otherwise, or whenverify_tokenis not set, it returns 403. - Subscribe to the fields you need. The event type joins the body’s
objectandentry[0].changes[0].field(for examplewhatsapp_business_account.messages).
{
"provider": "meta",
"config": { "verify_token": "…" }
}Zoom
- Create a source (
provider: zoom) and set the Secret Token shown under Features › Access of your Zoom App Marketplace app as its signing secret. - On the same page, turn on Event Subscription, click Add New Event Subscription, enter the ingress URL as the Event notification endpoint URL and click Validate. Zoom’s validation request (
endpoint.url_validation) is answered automatically withplainTokenandencryptedToken. - Choose the events and save. The event type is the body’s
event(for examplemeeting.started).
LINE
- Create a source (
provider: hmac) with theconfigbelow, and set the Channel secret from the Basic settings tab of your channel in the LINE Developers Console as its signing secret. - In the channel’s Messaging API tab, enter the ingress URL as the Webhook URL and turn on Use webhook.
- The request sent when you click Verify (with empty
events) is accepted as a message with the event typeinbound.
{
"provider": "hmac",
"config": {
"header": "x-line-signature",
"encoding": "base64",
"event_type_field": "events.0.type"
}
}Standard Webhooks
Services that sign in the Standard Webhooks format (webhook-id, webhook-timestamp, webhook-signature) use provider: standard. The Svix header names (svix-id, svix-timestamp, svix-signature) are accepted in the same format. OpenAI and Replicate send webhook- headers, and Clerk and Resend send svix- headers. Set the sender’s secret (whsec_…) as is.
Custom HMAC signatures
Services that send an HMAC-SHA256 of the body in a header use provider: hmac. Set the shared string as the signing secret, and describe the header in config (Source settings).
{
"provider": "hmac",
"config": {
"header": "x-signature",
"encoding": "hex",
"prefix": "sha256=",
"event_type_field": "event",
"id_header": "x-delivery-id"
}
}Basic authentication
Services that send an HTTP Basic username and password use provider: basic. Set the username as config.username and the password as the signing secret. Wrong credentials get 401 with WWW-Authenticate: Basic.
{
"provider": "basic",
"config": { "username": "webhook" }
}API keys
Services that send an API key in a header or the query string use provider: api_key. Set the key as the signing secret, and name the header in config.header (with config.prefix for any text before the key, such as "Bearer ") or the query parameter in config.query.
{
"provider": "api_key",
"config": { "header": "authorization", "prefix": "Bearer " }
}Services that do not sign their webhooks use provider: none; the ingress URL itself is then the only protection.
Source settings
| Field | Providers | Description |
|---|---|---|
header | hmac, api_key | Name of the signature header (required for hmac), or of the key header for api_key |
query | api_key | Name of the query parameter that carries the key (use header or query) |
encoding | hmac | hex (default) or base64 |
prefix | hmac, api_key | Text before the value, such as sha256= or "Bearer ". It is removed before comparing |
username | basic | The username (required; the password is the signing secret) |
verify_token | meta | The string compared with hub.verify_token in the subscription check |
event_type_field | hmac, basic, api_key, none | Body field to read the event type from; . for nesting and numbers for array items (for example data.type, events.0.type) |
id_header | hmac, basic, api_key, none | Header used to detect duplicates (the sender’s delivery ID) |
event_type | All | Event type to use when none can be read (default inbound) |
forward_headers | All | Received headers to pass on to your endpoints (up to 20; Forwarding the original request) |
response | All | The response when a request is accepted (Custom responses) |
Signature verification
provider | Headers checked | Timestamp |
|---|---|---|
stripe | Stripe-Signature | Within 5 minutes |
github | X-Hub-Signature-256 | — |
shopify | X-Shopify-Hmac-Sha256 | — |
slack | X-Slack-Signature, X-Slack-Request-Timestamp | Within 5 minutes |
meta | X-Hub-Signature-256 (keyed with the App secret) | — |
zoom | x-zm-signature, x-zm-request-timestamp | Within 5 minutes |
standard | webhook-id, webhook-timestamp, webhook-signature (or svix-id, svix-timestamp, svix-signature) | Within 5 minutes |
hmac | The header set in config.header | — |
basic | Authorization (Basic …) | — |
api_key | The header in config.header or the query parameter in config.query | — |
Signatures are computed over the raw body bytes and compared in constant time. A mismatch is rejected with 401, and the reason is kept in the request log.
The following URL checks are verified and answered in the form each service expects. They create no message, are not counted, and are answered even while the source is paused.
- Slack
url_verification:{ "challenge": "…" } - Zoom
endpoint.url_validation:{ "plainToken": "…", "encryptedToken": "…" } - Meta subscription check (GET): the value of
hub.challengeas text/plain
Event types and duplicates
provider | Event type | Duplicate detection |
|---|---|---|
stripe | Body type (for example invoice.paid) | Body id (evt_…) |
github | X-GitHub-Event plus the body’s action (for example pull_request.opened) | X-GitHub-Delivery |
shopify | X-Shopify-Topic (orders/ becomes orders.create) | X-Shopify-Webhook-Id |
slack | Body event.type, or type | Body event_id |
meta | Body object plus entry[0].changes[0].field (just object without a field) | The body (a redelivery of the same body is a duplicate) |
zoom | Body event | The body (a redelivery of the same body is a duplicate) |
standard | Body type | webhook-id (or svix-id) |
hmac, basic, api_key, none | The field in config.event_type_field | The header in config.id_header |
- When no event type can be read,
config.event_typeis used (defaultinbound)./becomes., other characters that are not allowed become_, and the result is cut to 100 characters. - For 24 hours, a request with the same delivery ID returns the ID of the first message. Redeliveries from the other service become one message and show as
duplicatein the request log. - A JSON body becomes the message’s
dataas is. Form data and plain text become{ "content_type": "…", "body": "<the body as text>" }. - The received headers are visible in the request log. Deliveries to your endpoints are signed with the endpoint’s own secret.
Custom responses
By default, an accepted request (including duplicates and held requests) gets 200 with { "id": "msg_…" }, or { "status": "held", "request_id": "sreq_…" } when held. For services that read the response body, such as Twilio with TwiML, set the status, Content-Type and body per source in config.response.
"config": {
"response": {
"status": 200,
"content_type": "application/xml",
"body": "<Response></Response>"
}
}| Field | Description |
|---|---|
status | 200 to 299 (default 200). 204 and 205 are sent without a body |
content_type | Default text/ |
body | Up to 4 KB (empty by default) |
Responses to URL checks and to rejected requests do not change.
Forwarding the original request
For an endpoint with body_format: raw, a message from a source is delivered with the original body, byte for byte, and the original Content-Type (application/octet-stream when there was none). Whitespace and key order in JSON, form data and non-UTF-8 bodies all stay unchanged. The Webhook Admin signature (webhook-id, webhook-timestamp, webhook-signature) is computed over those bytes. Endpoints with standard keep receiving { type, timestamp, data }.
Headers listed in config.forward_headers are passed on to your endpoints with their original values (up to 20, lowercase). Combined with a raw endpoint, your receiver can also verify the original service’s signature, such as stripe-signature.
"config": {
"forward_headers": ["stripe-signature"]
}- The headers Webhook Admin sets (signature, Content-Type, User-Agent) and headers added by a transformation take precedence.
- Headers used for delivery, such as authentication, cookies, body and connection headers, and headers starting with
webhook-, cannot be listed (422). - Transformations still receive the parsed payload (
webhook.payload). If a transformation changes the body, the transformed JSON is sent; if it changes only the URL or headers, the original body is sent. - A transformation accepts bodies up to 1 MB. A larger body is neither transformed nor sent: the delivery fails without retries, and the attempt’s
errorreadstransformation: input_too_large: …. Remove or fix the transformation, then resend. - Destinations other than webhooks receive the body as text when it is valid UTF-8, and the
dataJSON otherwise. Polling endpoints receive thedataJSON as before. - If the original body is no longer kept because the retention period has passed, the
dataJSON is sent.
Pausing
A paused source still verifies and accepts requests, holds them, and returns 200 to the sender ({ "status": "held", "request_id": "sreq_…" }, or your custom response). Because the sender sees a success, webhooks sent while the source was paused are not lost, even from services that do not retry failures or that turn off subscriptions after repeated failures.
- When you resume (
status: active), the held requests become messages and are delivered in the order they were received. Duplicates are detected as usual. - Requests that arrive after resuming are processed right away, so they may be delivered before the remaining held requests. Held requests keep their order among themselves.
- The number of held requests is shown in the source’s
heldand in the dashboard’s source list and details. They show asheldin the request log. - Held requests count toward your monthly quota when they are processed. A held request processed after the quota is used up becomes rejected and can be reprocessed.
- Held requests older than your plan’s retention period are deleted.
To keep accepting webhooks but stop delivering them, pause the consumer’s endpoint instead. Held messages are sent when it resumes.
Reprocessing
After you fix the signing secret or settings, reprocess rejected requests with Reprocess a rejected request, the Reprocess button in the dashboard’s request log, or the MCP tool reprocess_source_request. The request is verified again with the current secret and settings, then processed as if it had just arrived at the ingress URL.
- Timestamps are checked against the time the request was received, so a request received within 5 minutes of its timestamp passes even if you set the secret later.
- Only rejected requests with a stored body (
reprocessable: true) can be reprocessed. Requests rejected for a body over 10 MB and GET requests return 409. - An accepted request counts toward your monthly quota. On a paused source it is held.
Usage and logs
- Each accepted webhook counts as one message toward your monthly quota. Duplicates, rejected requests, URL checks and messages with no endpoint to send to (
no_endpoint) are not counted. Sources in a test environment count toward the test environment’s daily limit. - Every request is kept in the source’s request log for your retention period (List a source’s requests). Rejected requests are logged up to 60 per minute per source.
- Messages created from a source carry the source’s ID and name, and the request’s ID (
request_id), insourceof the message details. - If a plan downgrade leaves more sources than the plan allows, the most recently created ones return 402. Delete sources or upgrade again to resume.
Types
MessageRow = { id, event_type, consumer: { id, external_id, name }, created_at,
status: 'success' | 'retrying' | 'failed' | 'pending' | 'cancelled' | 'no_endpoint',
attempts, endpoint_url }
MessageDetail = { id, event_type, consumer, created_at, idempotency_key, payload, deliveries: Delivery[],
source: { id, name, request_id } | null }
Delivery = { id, endpoint_id, url, status: 'pending' | 'success' | 'retrying' | 'failed' | 'cancelled' | 'waiting',
attempt_count, next_attempt_at, attempts: AttemptRow[],
endpoint_status: 'active' | 'paused' | 'disabled' | null }
AttemptRow = { id, at, response_status, response_head, duration_ms, error,
via: 'direct' | 'relay' | 'poll' | DestinationType, source_ip,
transform: 'changed' | 'error' | null, ref: string | null }
/* error: null | 'timeout' | 'connect_failed' | 'dns_failed' | 'blocked_ip' | 'blocked_url: …'
| 'relay_failed' (fixed IP) | 'circuit_open' | 'wait_limit' | 'transformation: <kind>[: <detail>]'
| the destination's own reason (e.g. 'kafka: …') */
Consumer = { id, external_id, name, created_at, endpoint_count }
Endpoint = { id, consumer_id, url: string | null, description, event_types: string[] | null, fixed_ip,
status: 'active' | 'paused' | 'disabled', disabled_reason, failing_since, created_at,
retry: RetryPolicy, compat_signature: CompatSignature | null,
ordering: 'none' | 'fifo', transformation_updated_at: number | null,
type: 'http' | 'polling' | DestinationType, destination: object | null,
last_polled_at: number | null, body_format: 'standard' | 'raw' }
DestinationType = 'sqs' | 'eventbridge' | 'pubsub' | 's3' | 'r2' | 'gcs' | 'azure_blob'
| 'servicebus' | 'kafka' | 'rabbitmq' | 'slack' | 'teams' | 'chatwork' | 'google_chat'
RetryPolicy = { count: 0 | 1 | 2 | 3 | 5 | 7 | 12,
interval: 'progressive' | '1m' | '5m' | '30m' | '1h' | '6h' }
CompatSignature = { header, content: 'body' | 'timestamp_body', encoding: 'hex' | 'base64', prefix }
Recovery = { id, endpoint_id, since, status: 'running' | 'done' | 'stopped',
total, queued, created_at, finished_at }
EventType = { name, description, archived_at }
PollerToken = { id, endpoint_id, name, prefix, created_at, last_used_at }
Transformation = { endpoint_id, code, updated_at, enabled: boolean,
variables: Record<string, string> | null }
PolledMessage = { id, event_type, timestamp, payload /* body_format: raw なら data */,
headers: { 'webhook-id', 'webhook-timestamp', 'webhook-signature' } }
Source = { id, name, provider: SourceProvider, consumer_id, consumer: { id, external_id, name },
status: 'active' | 'paused', url, secret_hint: string | null, config: SourceConfig,
held, created_at, updated_at }
SourceProvider = 'stripe' | 'github' | 'shopify' | 'slack' | 'meta' | 'zoom' | 'standard'
| 'hmac' | 'basic' | 'api_key' | 'none'
SourceConfig = { event_type?, event_type_field?, id_header?, header?, query?, algorithm?: 'sha256',
encoding?: 'hex' | 'base64', prefix?, verify_token?, username?,
forward_headers?: string[], response?: { status, content_type, body } }
SourceRequest = { id, at, method: 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'GET',
status: 'accepted' | 'duplicate' | 'rejected' | 'held', response_status, reason,
message_id, event_type, content_type, size, headers: Record<string, string>,
processed_at: number | null, reprocessable: boolean }