Skip to main content
Incoming webhooks are available only in Restate Cloud. They’re not yet available in Restate BYOC.
Incoming webhooks are in preview.
Incoming webhooks let external systems invoke a service handler in your Restate Cloud environment through a dedicated URL. Configure the sender with this URL. Each POST request invokes the handler with the request body as input.
Anyone who has the URL can invoke the handler. Verify the sender in your handler, typically by checking the request signature.

Create a webhook

1

Register the target service

A webhook invokes one handler of a public service. Register the deployment that contains the handler before you create the webhook.Virtual objects and workflows can’t be webhook targets. To process events per key, forward them to a virtual object.
2

Create the webhook

In the Restate Cloud UI, open your environment’s Integration & API keys page and create an incoming webhook. Choose the service, the handler, and the delivery mode.
3

Configure the sender

Copy the webhook URL and paste it into the sender’s webhook settings. If you have an admin role, you can copy the URL again from the webhook list.
A webhook is bound to the names of its service and handler. If you remove or rename the service or handler, deliveries fail with 503. You can find the broken webhooks in the Restate Cloud UI. To invoke a renamed handler, create a new webhook and configure the sender with its URL. Deleting a webhook stops its deliveries. Requests usually stop reaching your handler within 30 seconds. If the Restate Cloud control plane is unavailable, this can take up to an hour.

Choose a delivery mode

Choose the delivery mode when you create the webhook. You cannot change it after creation. To switch modes, create a new webhook and update the sender’s URL.
A sync webhook waits at most 60 seconds for your handler. After that, the sender receives 504 Gateway Timeout without the handler’s result, even though the invocation keeps running. Most senders time out much sooner, often after 3 to 10 seconds, so keep sync handlers fast and forward slow work to another handler.

Write the handler

The examples on this page use the TypeScript SDK. Verify the sender’s signature before processing the request. Senders sign each request by computing a signature over the exact bytes of the body and including it in a header. To verify the signature, use the raw body and the request headers:
  • Declare the input as restate.serde.binary and set accept: "*/*" to receive the unmodified request body as raw bytes, regardless of content type.
  • Read the headers from ctx.request().headers. Header names are lowercase.
If the signature doesn’t match, throw a terminal error with errorCode: 401. Restate doesn’t retry the invocation, and in sync mode the sender receives 401.

Verify with the sender’s library

Many senders provide libraries for signature verification, for example @octokit/webhooks-methods for GitHub, LinearWebhookClient from @linear/sdk/webhooks for Linear, and isValidSlackRequest from @slack/bolt for Slack. Pass the raw body and the signature header to the library in its required format. Linear’s client accepts raw bytes; Octokit accepts a string. For example, with Octokit:
TypeScript

Verify signatures manually

Without a library, compute the signature over the raw body with your signing secret, and compare it with the header in constant time. This handler receives GitHub pull request events, verifies their signatures, and forwards the events to a virtual object:
TypeScript

Handle retries and slow work

Senders retry deliveries that fail or time out, and unless the sender sets an Idempotency-Key header (see below), each retry starts a new invocation of your handler. In sync mode, senders may also retry if your handler exceeds their response time limit (see the table below). To process each delivery once:
  1. Verify the request.
  2. Send the event to another handler, with an idempotency key derived from the sender’s delivery ID. Restate uses this key to deduplicate deliveries.
  3. Return immediately.
To process events for each issue, pull request, or channel sequentially, send them to a virtual object keyed by the corresponding ID, as shown in the examples on this page. If a sender sets an Idempotency-Key header, Restate deduplicates requests to the same webhook that carry the same key, so a retry doesn’t start a second invocation. In async mode, the sender then receives "status": "PreviouslyAccepted".

How requests reach your handler

Incoming webhook requests must meet these requirements:
  • POST requests only.
  • Bodies up to 1 MiB, with a Content-Length header. Chunked bodies without this header are rejected.
  • The exact path of the webhook URL. Requests with additional path segments return 404.
  • Up to 100 requests per second per webhook, with bursts of up to 200.
Restate Cloud forwards the following data to your handler:
  • The unmodified request body.
  • The sender’s headers, available through ctx.request().headers with lowercase names. Restate Cloud removes Authorization, Cookie, hop-by-hop headers such as Connection and Transfer-Encoding, and headers that start with x-restate- or x-restatecloud-.
  • The webhook ID in an added x-restatecloud-webhook-id header.
  • The query string appended to the x-restate-ingress-path header, excluding the delay and limit-key parameters.
Restate stores the forwarded headers, including signature headers, with the invocation. You can view them when inspecting the invocation. The sender receives one of these responses:

Configure webhook senders

The incoming webhooks example is a complete project with handlers for GitHub, Linear and Slack, and a script that sends signed sample deliveries to check your setup.

GitHub

  1. In your repository or organization settings, add a webhook with the webhook URL as the Payload URL.
  2. Set Content type to application/json.
  3. Set a Secret and configure your service with the same value. The example above reads it from GITHUB_WEBHOOK_SECRET.
You can use either delivery mode because GitHub accepts any 2xx response. GitHub signs each delivery in the x-hub-signature-256 header: sha256= followed by the hex HMAC-SHA256 of the body. To retry a failed delivery, redeliver it from the webhook’s settings in GitHub; the redelivery keeps its x-github-delivery ID.

Linear

Use sync mode. Linear treats any response other than HTTP 200 as a failure, including the 202 of async mode. In Linear, create a webhook in your workspace’s API settings using the webhook URL, and configure your service with the webhook’s signing secret. Linear signs each delivery in the linear-signature header, the hex HMAC-SHA256 of the body. The webhookTimestamp field in the body is the time Linear sent the delivery, in milliseconds.
TypeScript
Linear recommends rejecting deliveries whose webhookTimestamp is more than a minute old. Your handler may start after a delay, for example if your deployment was unavailable. Increase MAX_AGE_MS if you need to accept these delayed deliveries. ctx.date.now() records the time on the first attempt, so a retry of the same invocation compares against the same time.

Slack

Use sync mode. Slack verifies an Events API request URL by sending a challenge that your handler must return, and it shows the response to a slash command as the reply. The example uses one handler per Slack request URL. Create two webhooks: one for events, used as the Events API request URL, and one for command, used as the slash command’s request URL. To use a single handler for both, distinguish JSON events from form-encoded commands. Slack signs each request in the x-slack-signature header: v0= followed by the hex HMAC-SHA256 of v0:<timestamp>:<body>, keyed with your app’s signing secret. <timestamp> is the value of the x-slack-request-timestamp header. Slack recommends rejecting requests that are more than five minutes old.
TypeScript
Slack requires a response within 3 seconds for both request types.
  • events answers the url_verification request by returning { challenge }, and forwards message events with their event_id as the idempotency key.
  • command receives the form-encoded slash command and returns a response. For processing that exceeds Slack’s response time limit, forward the command to another handler and post the result to the command’s response_url when processing completes.

Limitations

  • Senders that verify the URL with a GET or PUT request are not supported because webhooks accept only POST.
  • Bodies larger than 1 MiB are rejected, even if the sender supports larger payloads. GitHub, for example, sends payloads of up to 25 MB.
  • A sync webhook waits at most 60 seconds for the handler, then returns 504. See Choose a delivery mode.
  • For senders that sign the full URL, such as Twilio and Square, provide the webhook URL through your service configuration to verify the signature. The webhook URL is not forwarded to your handler.
  • Virtual objects and workflows cannot be webhook targets. Configure the webhook to invoke a service handler that forwards events to the object.