---
id: web--webhook-receiver
title: "Webhook receiver"
summary: "Receives webhooks (HTTP callbacks other systems send when something happens). Place it right after an **Http listener** (on its `Output` pin). The listener owns the URL, port and TLS; this component does the webhook specific work: answers URL checks, checks the signature, drops repeated deliveries and answers the sender quickly."
audience: user
keywords: "input, output, rejected, signaturescheme, secret, secondarysecret, signatureheader, signatureprefix, hmacalgorithm, signatureencoding, timestamptoleranceseconds, ackmode, ackstatuscode, rejectstatuscode, handlehandshakes, verifytoken, deduplicationwindowseconds, contractcheck, eventidheader, eventtypeheader, clientlanguage"
---
# Webhook receiver

Receives webhooks (HTTP callbacks other systems send when something happens). Place it right after an **Http listener** (on its `Output` pin). The listener owns the URL, port and TLS; this component does the webhook specific work: answers URL checks, checks the signature, drops repeated deliveries and answers the sender quickly.

## Pins

| Pin | Kind | Types | Description |
|---|---|---|---|
| Input | input | `Viaducta.Plugins.Http.Types.WebHttpRequest` | Connect to the Http listener `Output` pin. |
| Output | output | `Viaducta.Plugins.Http.Types.WebHttpRequest` | The accepted request. The `Webhook` metadata holds `EventId`, `EventType`, `IsVerified`. |
| Rejected | error | `Viaducta.Plugins.Http.Types.WebHttpRequest` | Called after a request failed the signature check (already answered with `RejectStatusCode`). `Webhook.RejectReason` says why. Optional. |

## Configuration

| Field | Type | Default | Description |
|---|---|---|---|
| `SignatureScheme` | dropdown: `0` None, `1` HmacHeader, `2` GitHub, `3` Stripe, `4` Slack, `5` StandardWebhooks, `6` SharedSecretHeader, `7` MicrosoftGraph | `0` | How the sender signs requests, see below. |
| `Secret` | password | | The signing secret. StandardWebhooks: the `whsec_...` value. SharedSecretHeader: the expected header value. MicrosoftGraph: the subscription `clientState`. |
| `SecondarySecret` | password | | Also accepted; use while rotating secrets. |
| `SignatureHeader` | string | `X-Signature` | HmacHeader / SharedSecretHeader only: the header that carries the signature or secret. |
| `SignaturePrefix` | string | | HmacHeader only: removed from the header value before comparing (for example `sha256=`). |
| `HmacAlgorithm` | dropdown: `0` Sha256, `1` Sha1, `2` Sha512 | `0` | HmacHeader only. |
| `SignatureEncoding` | dropdown: `0` Hex, `1` Base64 | `0` | HmacHeader only. |
| `TimestampToleranceSeconds` | integer | `300` | Stripe, Slack, StandardWebhooks: max age of the signed timestamp (replay protection). `0` = off. |
| `AckMode` | dropdown: `0` Immediate, `1` AfterFlow | `0` | Immediate: answer the sender right after the checks, then run the flow. AfterFlow: the flow answers with an **Http response** component. |
| `AckStatusCode` | integer | `200` | Status sent for accepted requests in Immediate mode (2xx). |
| `RejectStatusCode` | integer | `401` | Status sent when the signature check fails. |
| `HandleHandshakes` | boolean | `true` | Answer URL checks (see below). |
| `VerifyToken` | password | | Meta style subscription check token. Empty = off. |
| `DeduplicationWindowSeconds` | integer | `600` | Deliveries with an event id seen within this window are answered but not passed on. `0` = off. |
| `ContractCheck` | dropdown: `0` Off, `1` Warn, `2` Reject | `1` | What to do when a generated client sends a contract hash (`X-Viaducta-Contract`) that differs from the payload type's, see "Contract check". |
| `EventIdHeader` | string | | Header with the event id. Empty = scheme default. |
| `EventTypeHeader` | string | | Header with the event type. Empty = scheme default. |
| `ClientLanguage` | code generator (`CSHARP`, `PYTHON`, `GO`, `JAVA`, `PHP`) | `CSHARP` | Designer only: the button opens generated client code that calls this receiver, see "Client code" below. Not used at run time. |

## Signature schemes

| Scheme | Sender examples | What is checked |
|---|---|---|
| None | | Nothing; every request is accepted (warning at start). |
| HmacHeader | Shopify (Base64), your own senders | HMAC of the body, in `SignatureHeader`. |
| GitHub | GitHub, Meta (Facebook, WhatsApp, Instagram) | `X-Hub-Signature-256: sha256=<hex>` (falls back to `X-Hub-Signature` sha1). |
| Stripe | Stripe | `Stripe-Signature: t=..,v1=..` over `t.body`, timestamp tolerance. |
| Slack | Slack Events API | `X-Slack-Signature: v0=..` over `v0:ts:body`, timestamp tolerance. |
| StandardWebhooks | Standard Webhooks, Svix based (Clerk, Resend, ...) | `webhook-id/-timestamp/-signature` (or `svix-*`), timestamp tolerance. |
| SharedSecretHeader | simple senders | The header value equals the secret. |
| MicrosoftGraph | Microsoft Graph change notifications (Outlook mail, calendar, Teams, ...) | Every `value[].clientState` equals the secret. |

Event id / type defaults: GitHub `X-GitHub-Delivery` / `X-GitHub-Event`; StandardWebhooks `webhook-id` / body `type`; Stripe body `id` / `type`; Slack body `event_id` / `event.type`. Other schemes have no default id, so duplicates are only detected when `EventIdHeader` is set.

## Handshakes (HandleHandshakes)

- **Microsoft Graph** (scheme MicrosoftGraph): a request with `?validationToken=...` is answered `200 text/plain` with the token.
- **Slack** (scheme Slack): a signed `{"type":"url_verification"}` is answered with its `challenge`.
- **Meta** (any scheme, when `VerifyToken` is set): `GET ?hub.mode=subscribe&hub.verify_token=..&hub.challenge=..` is answered with the challenge, or `403` for a wrong token.

Handshakes are answered and not passed to `Output`.

## Behaviour

1. Handshake? Answer and stop.
2. Check the signature against the **raw body bytes**. On failure: answer `RejectStatusCode`, call `Rejected`, stop. The detailed reason is logged, the sender only gets a generic text.
3. Read event id and type into the `Webhook` metadata.
4. Event id seen within the window? Answer (2xx) and stop.
5. Immediate: answer `AckStatusCode` with an empty body and close the response, then call `Output`. AfterFlow: call `Output`; the flow must send the answer. If the flow fails the event id is forgotten so the sender's retry is processed.

## Client code

The `ClientLanguage` button opens client code for this receiver in the designer (Download saves it): C# (.NET 8+, no
packages), Python (3.10+, standard library only), Go (1.24+, standard library only) Java (17+, Jackson 2.15+ for the
payload, one `<Payload>WebhookClient.java` with the types nested in it) or PHP (8.1+, no packages; decimals are exact strings;
HTTPS needs the openssl extension). The same menu gives an OpenAPI 3.1 document (YAML or JSON) of the request clients
send (`paths`): the payload schema, the headers, and the signature in `x-viaducta-signature` (OpenAPI can't express HMAC
signing; SharedSecretHeader is an `apiKey` security scheme), for tools such as openapi-generator or Postman. It contains the payload types, taken from the Json deserializer after the `Output` pin (without
one the client sends JSON text), and a `<Payload>WebhookClient` that serializes, signs the request the way
`SignatureScheme` checks it, posts it to the listener's address and retries network errors, timeouts, 408, 429 and 5xx
with the same event id (dropped as a duplicate when `DeduplicationWindowSeconds` is on and the scheme carries the id in
a header: StandardWebhooks, GitHub, or `EventIdHeader`). The secret is read from the constructor or the
`VIADUCTA_WEBHOOK_SECRET` environment variable, never written into the code. MicrosoftGraph receivers get the
payload types only.

## Contract check

Generated clients send the contract hash of their payload type (a fingerprint of its JSON shape: names, types,
nullability, omit rules, enum values) in `X-Viaducta-Contract`. At start the receiver computes the hash of the type the
Json deserializer after its `Output` pin produces, with the same code. When a client sends a different hash it was
generated for an older (or newer) version of the type:

- **Warn** (default): the delivery is accepted and a warning names both hashes (once per hash).
- **Reject**: the delivery is answered `409` ("regenerate the client") and passed to `Rejected` with
  `Webhook.RejectReason`; it is checked after the signature, so only authenticated senders see it, and the event id is
  not remembered, so the regenerated client can deliver it again.
- **Off**: not checked.

Requests without the header (other senders, curl) are always accepted. Without a single flow type coming out of a Json
deserializer after the receiver nothing is checked. Any change of the JSON shape changes the hash, also an added
optional property that old clients can ignore, so Warn is the safe default.

## Notes

- Turn on **Keep raw body** in the Http listener. Signatures are made over the exact bytes; without it the text body is re-encoded as UTF-8, which usually but not always gives the same bytes (warning at first use), and form posts (`application/x-www-form-urlencoded`) can't be checked at all.
- In Immediate mode an Http response later in the branch fails with "already sent"; errors in the flow are logged and no longer reach the sender. Senders usually wait only 5-10 seconds and retry on timeout, so Immediate is the safe default.
- Duplicate detection is in memory per component: it does not span several hosts or restarts.
- The flow keeps the listener's request slot (`MaxParallelRequests`) until it finishes, also in Immediate mode.

## Example

Http listener (`https://0.0.0.0:8443/hooks/github`, Keep raw body on) → **Webhook receiver** (GitHub, secret) → Switch on `Webhook.EventType` → ...
