Skip to navigation

Setting up a webhook Surface

Set up a webhook Surface that receives HTTP events from an external provider, verifies their signatures, and routes each event to a Flow or Agent Capability.

Before you begin

Create a Product with at least one enabled Capability. Get access to the external provider’s webhook settings so you can register the Webhook URL and copy its signing secret.

Create a webhook Surface

To create a webhook Surface, follow these steps:

  1. Open the Product that receives the events.
  2. On the Product page, click Surfaces, then click Add Surface.
  3. Select Webhook.
  4. Enter a name for the Surface.
  5. Connect each Capability that handles events to the Surface. In Visual view, drag a connection from the Capability node to the Surface node, then click Save. For more information, see Connect a Capability to a Surface.
  6. On the Webhook tab, copy the Webhook URL.
  7. Register the URL in the external provider’s webhook settings.

The Surface considers only enabled Capabilities that you connected to it. When no enabled Capability is connected, a delivery returns 404, except while the Surface is learning a schema.

Provide a payload shape

A webhook Surface uses a confirmed schema to describe the events it receives. On the Webhook tab, open Setup and choose one of these options:

  • Paste Schema: paste an example JSON object payload, then click Infer Schema. The Surface moves to Schema Proposed. Runtype infers the schema from the pasted object as if it were an event, so paste a real event rather than a JSON Schema or AsyncAPI document.
  • Send Test: click Start Listening, then send test events from the external provider. After three observed payloads, the Surface proposes a schema that marks each field as required or optional.

The Status badge on the Webhook tab shows one of these states:

  • Draft: the Surface has no observations yet.
  • Listening: the Surface collects test events to infer a schema.
  • Schema Proposed: the Surface has a proposed schema for you to review.
  • Active: you confirmed the schema, and the Surface runs a Capability for each event.
  • Drift Detected: recent payloads differ significantly from the confirmed schema.

When the Surface reaches Schema Proposed, review the proposed schema and click Confirm. To start over, click Clear to remove the observations, then send another set of test events.

After you click Start Listening or Infer Schema, and until you confirm a schema, the Surface records each event as an observation. It doesn’t run a Capability, so no execution appears in Logs. A new Surface on which you haven’t chosen a setup option runs a Capability for each event that it receives.

If you send test events with a webhook API key, use a key that can reach all Capabilities. A key scoped to specific Capabilities receives 403 while the Surface is learning a schema.

Verify webhook signatures

Configure signature verification after you confirm the schema. While the Surface is learning a schema, it rejects deliveries with 401 if a Signing Secret is saved and the request carries no webhook API key.

To verify signatures, follow these steps:

  1. On the Webhook tab, open Signature Verification.
  2. In Provider, select the provider that sends the events.
  3. In Signing Secret, paste the secret that the provider uses to sign requests. For SendGrid, paste the verification key from SendGrid’s signed event webhook settings.
  4. Save your changes.

Runtype verifies signatures only when a Signing Secret is saved. Except while the Surface is learning a schema, every request then needs a valid signature, even a request that carries a webhook API key. When verification fails, Runtype returns 401 and doesn’t run the Capability.

The supported providers send their signature in these headers:

  • Standard Webhooks: webhook-signature.
  • Stripe: stripe-signature.
  • GitHub: x-hub-signature-256.
  • Shopify: x-shopify-hmac-sha256.
  • Twilio: x-twilio-signature.
  • SendGrid: x-twilio-email-event-webhook-signature.
  • Slack: x-slack-signature.
  • Custom HMAC-SHA256 and Custom HMAC-SHA1: x-webhook-signature.

For the custom providers, sign the raw request body with the secret. Send the signature hex-encoded for SHA-256 or base64-encoded for SHA-1, with an optional sha256= or sha1= prefix. The Signature Header field doesn’t change the header that Runtype reads.

Standard Webhooks, Stripe, SendGrid, and Slack sign a timestamp. Runtype rejects those events when the timestamp differs from the current time by more than 5 minutes. The Timestamp Tolerance (seconds) field doesn’t change this window.

Select No verification only for an internal source that doesn’t sign requests. Selecting No verification doesn’t remove a saved Signing Secret, and Runtype then verifies requests as Custom HMAC-SHA256. To turn off verification, clear Signing Secret and save before you select No verification.

With No verification and no webhook API key, anyone who has the Webhook URL can trigger the Capability.

To authenticate an internal source without signatures, open the Keys tab and click Generate Key. Send the key, which starts with wh_, as Authorization: Bearer wh_YOUR_KEY or in the X-API-Key header. Replace wh_YOUR_KEY with the full key. After the Surface has an active key, it rejects requests that carry neither a valid key nor a valid signature.

Route events to Capabilities

When one webhook Surface receives several event types, route each type to a Capability. To configure event routing, follow these steps:

  1. Open the Routing tab.
  2. Set Event Type Path to the dot path of the field that names the event, such as type for Stripe.
  3. Add a route for each Event Pattern and select its Capability. A pattern such as payment_intent.* matches a family of event types.
  4. Optional: select a Fallback Capability for events that match no route.
  5. Click Save Routing.

If the Event Type Path field is missing from a payload, Runtype reads the event name from the type, event, event_type, or action field, or from the x-github-event header.

An event that matches no route and has no fallback goes to a Capability whose name matches the event name. If no name matches, the event goes to the first connected Capability. With a webhook API key scoped to specific Capabilities, it goes to the first connected Capability that the key can reach.

Read the event in a Flow

Runtype sends each event to the Capability as the user message, available as {{userMessage}}. The message is a JSON object with these fields:

  • event: the detected event name, or webhook when Runtype can’t detect one.
  • payload: the complete parsed request body.
  • metadata: the event ID in webhook_event_id, the time Runtype received the event in webhook_timestamp, and whether the signature was verified in signature_verified.

To read nested fields from the message, parse it in a step, such as a transform step.

By default, Runtype also passes each top-level payload field as a Flow input with the same name. For example, {{customerId}} resolves when the payload has a customerId field. Runtype omits top-level fields that start with _ because that prefix is reserved.

To pass nested or renamed fields as inputs, set inputMapping in the Surface’s behavior when you define the Product in code or update the Surface through the API. Each key is a Flow input name, and each value is a dot path into the payload. This example maps two nested Stripe fields:

Surface behavior with an input mapping
{
"type": "webhook",
"inputMapping": {
"amount": "data.object.amount",
"customerId": "data.object.customer"
}
}

When you set an input mapping, Runtype passes only the mapped fields as Flow inputs. Map every field that the Flow reads as an input. The Mappings tab in the dashboard doesn’t change the inputs that a delivery passes to the Flow.

Supported request bodies

Runtype parses the request body based on its Content-Type header:

  • JSON (application/json, any +json type, or no content type): Runtype parses the body. Invalid JSON returns 400.
  • Form-encoded (application/x-www-form-urlencoded): each form field becomes a string field, such as Body and From for Twilio.
  • Multipart (multipart/form-data): not supported. Runtype returns 400.
  • Any other type: the payload is { "raw": "BODY" }, so the only input is raw.

Choose a response mode

On the Webhook tab, Response Mode defaults to Async. Choose the mode that matches what the provider expects in the response:

  • Async: Runtype accepts the event, responds right away with 202, and runs the Capability afterward. Use this mode when the provider doesn’t need the execution result.
  • Synchronous: the provider waits for the Flow to finish and receives its result. Synchronous mode supports Flow Capabilities only. For an Agent Capability, use Async or Custom response.
  • Custom response: Runtype returns a fixed response that you define and runs the Capability afterward.

An Async response looks like this example:

Async response (202)
{
"success": true,
"mode": "async",
"message": "Webhook received and queued for processing",
"eventId": "evt_123",
"capability": "handle-payment",
"executionId": "exec_EXECUTION_ID"
}

A Synchronous response returns the Flow result as a string in the response field. A provider that parses the reply as a bare body can’t use this wrapper, so use Custom response instead:

Synchronous response (200)
{
"success": true,
"mode": "sync",
"eventId": "evt_123",
"capability": "handle-payment",
"response": "FLOW_RESULT",
"executionId": "exec_EXECUTION_ID"
}

If the Flow fails, a Synchronous request returns 500.

To return a custom response, follow these steps:

  1. In Response Mode, select Custom response.
  2. Click a preset, or fill in the fields yourself. Twilio (empty TwiML) returns an empty TwiML document as application/xml. Empty (204 No Content) returns status 204 with no body, which suits Shopify.
  3. Optional: edit Content-Type, Status, and Body. Status must be from 200 to 599 and defaults to 200.
  4. Save your changes.

Retries and duplicate events

An Async or custom acknowledgment confirms that Runtype accepted the event, not that the execution succeeded. Runtype returns 200 for a duplicate delivery of an accepted event. With a custom response, a duplicate receives the same custom response.

Runtype identifies an Async duplicate by the first ID that it finds, in this order:

  1. The x-github-delivery, x-shopify-webhook-id, or webhook-id header.
  2. The id or event_id field in the body.
  3. The raw request body, when neither an ID header nor an ID field exists.

Duplicate suppression lasts 24 hours from acceptance. Keep the event ID unchanged when you retry a delivery. For repeated events with identical bodies, such as a scheduled integration without an event ID, send a different webhook-id header for each event.

If Runtype can’t accept an event, it returns 500 or 503, including in Custom response mode. Configure the provider to retry these errors. In Async and Custom response modes, the event and its Flow inputs together must be 8 MiB or smaller. Runtype stores the payload both in the message and as inputs, so a request smaller than 8 MiB can exceed the limit. Runtype returns 413 for a larger event.

Test the connection

To verify the connection, follow these steps:

  1. Send a test event from the external provider.
  2. If the Surface is Listening, send two more test events so that it can propose a schema.
  3. When the Surface reaches Schema Proposed, review the schema and click Confirm.
  4. Send another test event.
  5. In the provider’s delivery history, confirm that the response names the expected capability. A Custom response doesn’t name the Capability.
  6. In Logs, open the execution and inspect the input that the Flow received.

When the Surface shows Drift Detected, it keeps running a Capability for each event. Check that the Flow still handles the changed payload fields.

Troubleshoot webhook setup

Runtype rejects some deliveries before it runs a Capability, so they don’t appear in Logs. Check the provider’s delivery history for the status code and the response body.

The following table lists the rejections that you might see:

StatusCauseFix
400 PARSE_ERRORThe body is invalid JSON or multipart form data.Send JSON, form-encoded, or plain text.
400 SURFACE_PAUSEDThe Surface is paused.Resume the Surface.
400 Event not allowedThe event matches no route, and the Surface’s events list doesn’t include it.Add the event to the events list in the Surface’s behavior, or route the event to a Capability.
400 EXPIRED_KEYThe webhook API key has expired.Generate a key on the Keys tab.
401 INVALID_SIGNATUREThe signature doesn’t match.Check the Provider, the Signing Secret, and the header that the provider sends.
401 UNAUTHORIZEDThe Surface has a signing secret or an active webhook API key, and the request carries neither a valid signature nor a valid key.Send a signed request or a valid wh_ key.
403The webhook API key can’t reach the routed Capability, or a key scoped to specific Capabilities sent an event while the Surface is learning a schema.Use a key that can reach all Capabilities.
404The Webhook URL is wrong, or no enabled Capability is connected to the Surface.Copy the URL again from the Webhook tab, and connect an enabled Capability.
429 RATE_LIMITEDThe request exceeded a rate limit.Retry later. If the response has a Retry-After header, wait that many seconds.
501A Synchronous Surface routed the event to an Agent Capability.Select Async or Custom response.

Use these checks for other problems:

  • The Surface stays in Listening: send three test events with JSON object payloads, and confirm that they reach the Webhook URL.
  • Unexpected routing: check Event Type Path, the route patterns, and the Fallback Capability. Without a matching route or fallback, an event goes to the Capability named after the event, or else to the first connected Capability.
  • A Flow input is empty: if you set an input mapping, map every input that the Flow reads.

For provider-specific checks, see Inbound webhook rejected.

Next steps

Continue with these guides: