Docs

API reference

Basics

URL
https://api.webhookadmin.com/v1
Authentication
Authorization: Bearer sk_live_… or sk_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_large above that)
Lists
?cursor=&limit= (limit 1–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>" }
HTTPerrorMeaning
401unauthorizedMissing, wrong, revoked or expired key
402plan_limitPlan limit reached
403forbiddenThe key lacks the scope
404not_foundNot found
409conflictConflicts with existing data
409endpoint_disabledThe endpoint is disabled. Fix it and re-enable it, then retry
413too_largeBody larger than 1.1MB
422invalidInvalid input
429rate_limitedRate limit exceeded

Rate limits

Requests per minute, counted per environment across all keys.

PlanPer minute
Free600
Starter3,000
Pro12,000
Business20,000
Response headerMeaning
x-ratelimit-limitRequests per minute
x-ratelimit-remainingRemaining in the current minute
x-ratelimit-resetWhen the current minute ends (Unix seconds)
retry-afterOnly on 429. Seconds until you can retry

Endpoints

MethodPathScope
POST/v1/messagesmessages:send
GET/v1/messageslogs:read
GET/v1/messages/{id}logs:read
POST/v1/deliveries/{id}/retrymessages:retry
GET/v1/consumerslogs:read
POST/v1/consumersconsumers:write
DELETE/v1/consumers/{id}consumers:write
POST/v1/consumers/{id}/portalendpoints:write
GET/v1/endpointslogs:read
POST/v1/endpointsendpoints:write
GET/v1/endpoints/{id}logs:read
PATCH/v1/endpoints/{id}endpoints:write
DELETE/v1/endpoints/{id}endpoints:write
POST/v1/endpoints/{id}/rotate-secretendpoints:write
POST/v1/endpoints/{id}/testmessages:send
POST/v1/destinations/testendpoints:write
GET/v1/endpoints/{id}/transformationlogs:read
PUT/v1/endpoints/{id}/transformationendpoints:write
DELETE/v1/endpoints/{id}/transformationendpoints:write
POST/v1/endpoints/{id}/transformation/testendpoints:write
GET/v1/poller/{endpoint_id}Poller token (sk_poll_…), not an API key
GET/v1/endpoints/{id}/poller-tokenslogs:read
POST/v1/endpoints/{id}/poller-tokensendpoints:write
DELETE/v1/endpoints/{id}/poller-tokens/{token_id}endpoints:write
POST/v1/endpoints/{id}/recovermessages:retry
GET/v1/endpoints/{id}/recoveries/{recovery_id}logs:read
GET/v1/event-typesNone (any key of the environment)
POST/v1/event-typesendpoints:write
GET/v1/sourceslogs:read
POST/v1/sourcesendpoints:write
GET/v1/sources/{id}logs:read
PATCH/v1/sources/{id}endpoints:write
DELETE/v1/sources/{id}endpoints:write
PUT/v1/sources/{id}/secretendpoints:write
GET/v1/sources/{id}/requestslogs:read
POST/v1/sources/{id}/requests/{request_id}/reprocessmessages:send
POST/in/{token}The ingress URL (no API key)

Send a message

POST /v1/messages

Scope: messages:send

InputTypeDescription
consumerstringThe consumer’s external_id (up to 200 characters)
event_typestringEvent type. Letters, digits and _ . : -, up to 100 characters
payloadJSONThe body to deliver. Up to 1MB
transformations_paramsobjectOptional. Passed to the transformation as webhook.transformationsParams (up to 4KB as JSON). The spelling transformationsParams is accepted too
Idempotency-Key (header)stringOptional. 1–256 characters

Returns 202 { id, deliveries }, where deliveries is the number of deliveries created.

  • The same Idempotency-Key returns the same id for 24 hours (the first message, even if the body differs).
  • If no consumer matches consumer, one is created (with no endpoints, so deliveries is 0).
  • A message with deliveries of 0 (no endpoint) is stored with the status no_endpoint but not sent, and it does not count toward your monthly quota. Adding an endpoint later does not send it.
  • The x-usage-month response 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/messages

Scope: 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}/retry

Scope: messages:retry

id is a Delivery ID. Returns 202.

  • Returns 409 conflict while the delivery is in progress (pending, retrying).
  • Returns 409 endpoint_disabled and does not accept the retry when the endpoint is disabled (disabled). Fix the endpoint and set status: active with Update an endpoint first. deliveries[].endpoint_status in MessageDetail tells you in advance.
  • For a paused endpoint (paused) the retry is accepted and held until you resume it. Returns 404 not_found if the endpoint was deleted.

List consumers

GET /v1/consumers

Scope: logs:read

Query: cursor, limit. Returns { items: Consumer[], next_cursor }.

Create a consumer

POST /v1/consumers

Scope: consumers:write

InputTypeDescription
external_idstringYour ID for this customer (up to 200 characters)
namestringOptional. 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_id creates a new consumer with no endpoints.

List endpoints

GET /v1/endpoints

Scope: logs:read

Query: consumer_id (optional). Returns { items: Endpoint[] }.

Create an endpoint

POST /v1/endpoints

Scope: endpoints:write

InputTypeDescription
consumer_idstringThe consumer id (con_…)
urlstringEndpoint URL (up to 2,000 characters). For type http (the default)
typestringOptional. http (default), polling (Polling endpoints; neither url nor destination) or a destination other than a webhook (Destinations). webhook and pollingEndpoint are accepted too
destinationobjectSettings and credentials of a destination other than a webhook, instead of url
event_typesstring[]Optional. Event types to receive. All when omitted
fixed_ipbooleanOptional. Deliver from the static source IP (209.71.107.233)
descriptionstringOptional. Up to 200 characters
retryobjectOptional. Retry count and interval (Retry policy). The default when omitted
compat_signatureobjectOptional. Compatibility signature (Signature verification)
orderingstringOptional. Delivery order (Ordered delivery): none (default) or fifo
body_formatstringOptional. Shape of the body: standard (default, { type, timestamp, data }) or raw (data as is)
secretstringOptional. 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 paused endpoint are held, and held messages from this month and last month are sent when it is set back to active.
  • An automatically disabled endpoint (disabled) resumes when you set status to active.
  • Set retry to null to go back to the default, and compat_signature to null to remove it.
  • Setting ordering from fifo back to none sends the deliveries that were waiting their turn right away.
  • type cannot 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-secret

Scope: 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}/test

Scope: messages:send

Sends one test message to this endpoint only. The request body is optional.

InputTypeDescription
event_typestringOptional. Default webhook.test
payloadobjectOptional. 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_disabled when the endpoint is disabled (disabled).

Returns 202 { message_id }. Check the result with Get a message.

Test a destination

POST /v1/destinations/test

Scope: endpoints:write

InputTypeDescription
endpoint_idstringOptional. Use the saved settings and credentials of this endpoint
typestringRequired without endpoint_id. The destination type
destinationobjectRequired without endpoint_id, with credentials. Overrides the saved settings when given
fixed_ipbooleanOptional. 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}/transformation

Scope: logs:read

Returns { endpoint_id, code, updated_at, enabled, variables }. 404 not_found when there is none.

Save the transformation

PUT /v1/endpoints/{id}/transformation

Scope: endpoints:write

InputTypeDescription
codestringJavaScript that defines function handler(webhook), up to 51,200 characters (Transformations). Required when there is no transformation yet
enabledbooleanOptional. false keeps the code but stops using it; true turns it back on
variablesobjectOptional. 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}/transformation

Scope: 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/test

Scope: endpoints:write

InputTypeDescription
codestringOptional. Code to run. Omit to use the saved transformation
payloadanyOptional. Sample data. Default { "test": true, "endpoint_id": … }
event_typestringOptional. Default webhook.test
variablesobjectOptional. Values for webhook.env. The saved ones when omitted
transformations_paramsobjectOptional. 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

QueryTypeDescription
iteratorstringOptional. The iterator of the previous response. Omit it to start at the beginning of the retention period (the day the endpoint was created)
limitnumberOptional. 1 to 250 (default 50)
event_typestringOptional, repeatable. Narrows the endpoint’s event_types further
afterstringOptional. An ISO 8601 time. Without iterator, start from that time
channelstringOptional. 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-tokens

Scope: logs:read

Returns { items: PollerToken[] } in creation order. The token strings are not returned.

Create a poller token

POST /v1/endpoints/{id}/poller-tokens

Scope: 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}/recover

Scope: messages:retry

InputTypeDescription
sincenumberUnix 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 (same webhook-id) (Bulk resend).
  • Returns 409 endpoint_disabled if the endpoint is disabled and 409 conflict if it is paused. Re-enable it first.
  • Only one can run per endpoint. Returns 409 conflict while 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-types

Scope: None (any key of the environment)

Returns { items: EventType[] }, sorted by name (not paged).

Create an event type

POST /v1/event-types

Scope: endpoints:write

InputTypeDescription
namestringEvent type. Letters, digits and _ . : -, up to 100 characters
descriptionstringOptional. 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/sources

Scope: logs:read

Returns { items: Source[], next_cursor } in creation order.

Create a source

POST /v1/sources

Scope: endpoints:write

InputTypeDescription
namestring1 to 100 characters
providerstringstripe, github, shopify, slack, meta, zoom, standard, hmac, basic, api_key or none
consumer_idstringID of the consumer that receives the messages
secretstringOptional. The signing secret (up to 500 characters); whsec_… for standard. It can also be set later with Set the signing secret
configobjectOptional. SourceConfig (Source settings)
statusstringOptional. 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 provider removes the signing secret. Set the new service’s secret with Set the signing secret.
  • When you change provider without config, the defaults of that provider apply. Passing config replaces it.
  • Set the signing secret with Set the signing secret (passing secret here returns 422).
  • status: paused holds incoming requests, and active resumes 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}/secret

Scope: endpoints:write

InputTypeDescription
secretstring | nullThe 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}/requests

Scope: 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.

statusMeaning
acceptedAccepted. message_id is the message that was created
duplicateA redelivery with the same delivery ID. message_id is the earlier message
rejectedRejected, with the reason in reason
heldReceived while the source was paused, and processed when it resumes
  • method is the request method (POST, PUT, PATCH, DELETE or GET).
  • processed_at is when a held or rejected request was processed later (null until then).
  • Requests with reprocessable: true can be reprocessed.
  • headers holds the received headers, except authentication, cookies and similar.

Reprocess a rejected request

POST /v1/sources/{id}/requests/{request_id}/reprocess

Scope: 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 (or duplicate) and counts toward your monthly quota. On a paused source it becomes held.
  • If verification fails again, it stays rejected with a new reason and processed_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.

HTTPMeaning
200Accepted: { "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
401Wrong 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)
402More sources than the plan allows
403The organization is suspended, or hub.verify_token does not match in Meta’s subscription check
404Unknown ingress URL
405A method other than POST, PUT, PATCH or DELETE (except GET for meta)
413Body larger than 10 MB
429More 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.

FieldValues
count0, 1, 2, 3, 5, 7, 12 (retries after the first send; 0 means no retry)
intervalprogressive (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: 7 with interval: progressive (up to 8 attempts including the first, over about 28 hours). retry: null goes back to the default.
  • Combinations whose last retry comes more than 3 days after the first send are rejected (422). Only progressive with 12 does.
  • 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 none sends the waiting deliveries right away, in parallel. Messages accepted before you switched to fifo are 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": "…" }
  }
}
typedestinationcredentialsSignature and IDs
sqsregion, account_id, queue_nameaccess_key_id, secret_access_keyMessage attributes
eventbridgeregion, event_bus (default default), source (default webhookadmin), detail_type (default: the event type)Same as SQSNone
pubsubproject_id, topicservice_account (the service account key JSON)Message attributes
s3region, bucket, prefixSame as SQSObject metadata
r2account_id, bucket, prefixR2 API token access keyObject metadata
gcsbucket, prefixHMAC key or service_accountObject metadata
azure_blobaccount, container, prefixaccount_keyBlob metadata (webhook_id and so on)
servicebusnamespace, entity (queue or topic)key_name, key (a SAS policy with Send)Message properties
kafkaurl (Confluent Cloud REST endpoint; requests go to /kafka/v3/…, so a standalone REST Proxy is not supported), cluster_id, topicapi_key, api_secretRecord headers
rabbitmqurl (HTTP API base), vhost (default /), exchange, routing_keyusername, passwordMessage headers
slackNonewebhook_url (a Slack Incoming Webhook URL)No signature
teamsNonewebhook_url (a Teams Workflows URL)No signature
chatworkroom_id (the numeric chat ID)api_tokenNo signature
google_chatNonewebhook_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-signature and event-type. Receivers verify the body with the same SDKs as webhooks (chats get no signature).
  • Hosts are the providers’ official endpoints built from region and the like. The url of 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, omit credentials to keep the current ones. Changing the host of a Kafka or RabbitMQ url requires 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 via is the type (sqs and so on) and ref is 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.
  • type also 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…/…) in webhook_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.com or *.logic.azure.com) in webhook_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 in room_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=…) in webhook_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: … } (or webhook.payload = { text: … } with body_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 set status back to active.
  • 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 ref is the Chatwork message ID (null for 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 typeThis APIHow config is read
sqssqsRegion, account and queue from queueUrl; accessKeyId, secretAccessKey
eventBridgeeventbridgeregion, eventBusName, detailType, access key
googleCloudPubSubpubsubprojectId, topicId, credentials (service account key JSON)
amazonS3s3bucket, region, access key
googleCloudStoragegcsbucket, credentials (service account key JSON)
azureBlobStorageazure_blobaccount, container, accessKey
rabbitMqrabbitmquri is the HTTP API URL (https://user:password@host/), routingKey
  • Messages are sent one at a time. batchSize and maxWaitSecs are accepted and ignored.
  • The body is the Standard Webhooks JSON by default (data as is with body_format: raw), with the signature and IDs in attributes or metadata. Verify it with the same SDKs as webhooks.
  • RabbitMQ amqp:// and amqps:// URIs are not supported. Pass the management plugin’s HTTP API URL.
  • roleArn and externalId (assuming an IAM role) and endpointUrl are not supported. Pass an access key.
  • There are no SNS, BigQuery, ClickHouse, Snowflake, Redshift, Postgres or OpenTelemetry destinations.
  • uid is ignored. The endpoint ID is ep_…; 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;
}
FieldDescription
payloadThe 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)
headersHeaders to add (up to 20, 8 KB in total). Headers used for delivery, such as webhook-* and content-type, cannot be set
methodPOST, PUT or PATCH
urlOnly the path and query can change; the scheme, host and port stay the same
canceltrue skips the delivery. It becomes cancelled (not a failure, not retried)
eventTypeThe event type (read only)
envThe variables saved with the transformation (read only; {} when there are none)
transformationsParamsThe 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 error reads transformation: <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).
  • variables holds string values, up to 50 entries and 4 KB as JSON; transformations_params is up to 4 KB as JSON.
  • Saving enabled: false keeps the code and variables but 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.log output 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, id and headers (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 check JSON.stringify(payload) with the endpoint’s secret.
  • Calling with the previous response’s iterator acknowledges the previous page and marks those deliveries success (the fetch shows as an attempt with via: "poll"). Save the iterator after processing a page and continue from it.
  • Delivery is at least once. Calling with an older iterator returns the same messages again, and calling without one starts from the beginning of the retention period, so deduplicate by id if 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: fifo do 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 Poller in the SDKs (Node.js, Python, Go, PHP) reads page after page and returns the iterator.

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.

MethodPathDescription
GET/api/v1/app/{app_id}/poller/{endpoint_id}{ 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/api/v1/app/{app_id}/poller/{endpoint_id}/consumer/{consumer_id}We keep the position and move it forward on every read
POST…/consumer/{consumer_id}/seek{ after } (ISO 8601) → { iterator }. Moves the position to that time
GET/api/v1/auth/app/{app_id}/poller/{endpoint_id}/tokenWith the sender’s API key (endpoints:write): the current token { token, id, name, createdAt, expiresAt, scopes }, created if there is none
POST…/token/rotate{ expiry?, oldTokenExpiry? } (seconds; the old token stops working after 300 seconds by default, or right away with 0)
  • Errors use { code, detail }, with detail in English. An invalid iterator returns 400 invalid_iterator.
  • The event_type, channel and after query parameters are accepted too (Poll).
  • Endpoint types are http and polling; pollingEndpoint is 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 serviceWebhook 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 /api/v1/app/{app_id}/msg)POST /v1/messages, with the consumer’s external_id as consumer
Transformations (handler(webhook))Same function format, including webhook.env and webhook.transformationsParams
POST /v1/endpoints
{
  "consumer_id": "con_…",
  "url": "https://example.com/webhooks",
  "secret": "whsec_…",
  "body_format": "raw"
}
  • secret is whsec_ followed by standard base64 that decodes to 24 to 75 bytes. The whsec_ 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.
  • secret can be passed when creating through the API, MCP or the SDKs. The dashboard and the customer portal generate a new one.
  • body_format can be set in the dashboard, the API, MCP, the SDKs and the customer portal. With raw, the signature is still in webhook-id, webhook-timestamp and webhook-signature, computed as the Standard Webhooks specification defines.

Transformations can also be read and written through compatible routes (Pro plan and above).

MethodPathDescription
GET/api/v1/app/{app_id}/endpoint/{endpoint_id}/transformation{ code, enabled, variables, updatedAt }. With no transformation, code, variables and updatedAt are null and enabled is false (logs:read)
PATCHSame path{ code?, enabled?, variables? } → 204. code is 10 to 51,200 characters; null removes it. Omitted fields keep their values (endpoints:write)
POST…/transformation/simulate{ code, eventType, payload, variables? } → { url, payload, method, cancel }, where payload is the body as a string. Nothing is sent or saved (endpoints:write)
  • app_id is the consumer id or external_id; authenticate with an API key.
  • enabled: false delivers without transforming and keeps the code and variables.
  • For an endpoint with body_format: "raw", webhook.payload is data as is.
  • Errors on the compatible routes use { code, detail } with detail in English. A 422 also has fields, 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 /v1 API.

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-* and content-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 failed when 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.

ConditionOptionsDefault
Delivery failuresRight after the first failure, or after failing for 5, 15 or 60 minutes60 minutes
Auto-disabledWhen an endpoint is disabled after failing for 5 days—
Retry backlogWhen an environment has more than 100, 1,000 or 10,000 deliveries waiting to retry (once a day)1,000
UsageWhen this month’s messages reach 50%, 80% or 100% of your limit80%
Polling endpoint not polledWhen 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

  1. Create a source (provider: stripe). The signing secret can be added later.
  2. Copy the source’s ingress URL.
  3. 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.
  4. 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

  1. Create a source (provider: github) and set a hard-to-guess string as its signing secret.
  2. In the repository or organization, open Settings › Webhooks › Add webhook. Enter the ingress URL as the Payload URL, choose application/json as the Content type, and enter the same string as the Secret.
  3. Choose the events and save. The ping GitHub sends right away is accepted as a message with the event type ping.

Shopify

  1. Create a source (provider: shopify).
  2. Subscribe to webhooks from your app (shopify.app.toml or 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.
  3. 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

  1. Create a source (provider: slack) and set the Signing Secret from your Slack app’s Basic Information › App Credentials as its secret.
  2. 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.
  3. Choose the events to subscribe to and save.

Meta (Facebook, WhatsApp, Instagram)

  1. 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 as config.verify_token.
  2. 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.
  3. Meta’s subscription check (a GET with hub.mode=subscribe) is answered with hub.challenge as text/plain when hub.verify_token matches config.verify_token. Otherwise, or when verify_token is not set, it returns 403.
  4. Subscribe to the fields you need. The event type joins the body’s object and entry[0].changes[0].field (for example whatsapp_business_account.messages).
{
  "provider": "meta",
  "config": { "verify_token": "…" }
}

Zoom

  1. Create a source (provider: zoom) and set the Secret Token shown under Features › Access of your Zoom App Marketplace app as its signing secret.
  2. 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 with plainToken and encryptedToken.
  3. Choose the events and save. The event type is the body’s event (for example meeting.started).

LINE

  1. Create a source (provider: hmac) with the config below, and set the Channel secret from the Basic settings tab of your channel in the LINE Developers Console as its signing secret.
  2. In the channel’s Messaging API tab, enter the ingress URL as the Webhook URL and turn on Use webhook.
  3. The request sent when you click Verify (with empty events) is accepted as a message with the event type inbound.
{
  "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

FieldProvidersDescription
headerhmac, api_keyName of the signature header (required for hmac), or of the key header for api_key
queryapi_keyName of the query parameter that carries the key (use header or query)
encodinghmachex (default) or base64
prefixhmac, api_keyText before the value, such as sha256= or "Bearer ". It is removed before comparing
usernamebasicThe username (required; the password is the signing secret)
verify_tokenmetaThe string compared with hub.verify_token in the subscription check
event_type_fieldhmac, basic, api_key, noneBody field to read the event type from; . for nesting and numbers for array items (for example data.type, events.0.type)
id_headerhmac, basic, api_key, noneHeader used to detect duplicates (the sender’s delivery ID)
event_typeAllEvent type to use when none can be read (default inbound)
forward_headersAllReceived headers to pass on to your endpoints (up to 20; Forwarding the original request)
responseAllThe response when a request is accepted (Custom responses)

Signature verification

providerHeaders checkedTimestamp
stripeStripe-SignatureWithin 5 minutes
githubX-Hub-Signature-256—
shopifyX-Shopify-Hmac-Sha256—
slackX-Slack-Signature, X-Slack-Request-TimestampWithin 5 minutes
metaX-Hub-Signature-256 (keyed with the App secret)—
zoomx-zm-signature, x-zm-request-timestampWithin 5 minutes
standardwebhook-id, webhook-timestamp, webhook-signature (or svix-id, svix-timestamp, svix-signature)Within 5 minutes
hmacThe header set in config.header—
basicAuthorization (Basic …)—
api_keyThe 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.challenge as text/plain

Event types and duplicates

providerEvent typeDuplicate detection
stripeBody type (for example invoice.paid)Body id (evt_…)
githubX-GitHub-Event plus the body’s action (for example pull_request.opened)X-GitHub-Delivery
shopifyX-Shopify-Topic (orders/create becomes orders.create)X-Shopify-Webhook-Id
slackBody event.type, or typeBody event_id
metaBody object plus entry[0].changes[0].field (just object without a field)The body (a redelivery of the same body is a duplicate)
zoomBody eventThe body (a redelivery of the same body is a duplicate)
standardBody typewebhook-id (or svix-id)
hmac, basic, api_key, noneThe field in config.event_type_fieldThe header in config.id_header
  • When no event type can be read, config.event_type is used (default inbound). / 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 duplicate in the request log.
  • A JSON body becomes the message’s data as 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>"
  }
}
FieldDescription
status200 to 299 (default 200). 204 and 205 are sent without a body
content_typeDefault text/plain; charset=utf-8
bodyUp 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 error reads transformation: 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 data JSON otherwise. Polling endpoints receive the data JSON as before.
  • If the original body is no longer kept because the retention period has passed, the data JSON 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 held and in the dashboard’s source list and details. They show as held in 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), in source of 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 }