---
id: web--webhook-sender
title: "Webhook sender"
summary: "Turns any record into a signed webhook request. Connect its `Output` to an **Http client**, which does the actual sending (auth, TLS, client certificates, timeouts). The signature formats are the same ones the **Webhook receiver** checks, so a Viaducta flow can send to another one."
audience: user
keywords: "input, output, uri, method, signaturescheme, secret, signatureheader, signatureprefix, hmacalgorithm, signatureencoding, eventid, eventtype, eventtypeheader, envelope, contenttype, omitnullvalues, receiverlanguage"
---
# Webhook sender

Turns any record into a signed webhook request. Connect its `Output` to an **Http client**, which does the actual sending (auth, TLS, client certificates, timeouts). The signature formats are the same ones the **Webhook receiver** checks, so a Viaducta flow can send to another one.

## Pins

| Pin | Kind | Types | Description |
|---|---|---|---|
| Input | input | any | The payload, see below. |
| Output | output | `Viaducta.Plugins.Http.Types.WebHttpRequest` | The signed request. Connect it to an Http client. |

## What is sent

| Input | Body |
|---|---|
| `TextRecord` | `Value` as is, with the record's `ContentType` (else `ContentType` from config). |
| `WebHttpRequest` | Its body (`ByteBody`, else `Body`) as is. Its `Uri` (when config `Uri` is empty), headers, cookies and content type are kept. Form requests are refused. |
| `NoInputData` | Refused (an error). |
| anything else | The record as JSON (compact, without the internal `recordId`), content type `ContentType` from config. |

The bytes go in `ByteBody` (`Body` is empty), so the Http client sends exactly what was signed.

## Configuration

| Field | Type | Default | Description |
|---|---|---|---|
| `Uri` | string | | Target URL. Empty: the input `WebHttpRequest` Uri, else the Http client's Uri. Bind to an expression for a URL per receiver. |
| `Method` | string | `POST` | HTTP method. |
| `SignatureScheme` | dropdown: `0` None, `1` HmacHeader, `2` GitHub, `3` Stripe, `4` Slack, `5` StandardWebhooks, `6` SharedSecretHeader, `7` MicrosoftGraph | `5` | See below. MicrosoftGraph (`7`) can't be sent (error at start). |
| `Secret` | password | | The signing secret. StandardWebhooks: `whsec_` + base64 key. Bind to an expression for a secret per receiver; if empty when sending, the record fails. |
| `SignatureHeader` | string | `X-Signature` | HmacHeader / SharedSecretHeader: the header name. |
| `SignaturePrefix` | string | | HmacHeader: written before the signature (for example `sha256=`). |
| `HmacAlgorithm` | dropdown: `0` Sha256, `1` Sha1, `2` Sha512 | `0` | HmacHeader only. |
| `SignatureEncoding` | dropdown: `0` Hex, `1` Base64 | `0` | HmacHeader only. |
| `EventId` | string | | The `webhook-id`. Empty: `msg_` + the record id (stable across Retry). |
| `EventType` | string | | Event type for the envelope and the type header. |
| `EventTypeHeader` | string | | Header to carry the event type (for example `X-Event-Type`). Empty: none. |
| `Envelope` | boolean | `false` | Wrap as `{"type", "timestamp", "data"}` (Standard Webhooks style). The payload must be JSON. |
| `ContentType` | string | `application/json` | Content type for JSON records and TextRecords without one. |
| `OmitNullValues` | boolean | `false` | Leave out null properties when serializing records. |
| `ReceiverLanguage` | code generator (`CSHARP`, `PYTHON`, `GO`, `JAVA`, `PHP`) | `CSHARP` | Designer only: the button opens generated receiving code for an app that gets these webhooks, see "Receiver code" below. Not used at run time. |

## Signature schemes

| Scheme | Headers sent |
|---|---|
| None | no signature (warning at start) |
| HmacHeader | `SignatureHeader: <prefix><hex or base64 HMAC(body)>` |
| GitHub | `X-Hub-Signature-256: sha256=<hex>` |
| Stripe | `Stripe-Signature: t=<ts>,v1=<hex HMAC("ts." + body)>` |
| Slack | `X-Slack-Request-Timestamp`, `X-Slack-Signature: v0=<hex HMAC("v0:ts:" + body)>` |
| StandardWebhooks | `webhook-signature: v1,<base64 HMAC("id.ts." + body)>` |
| SharedSecretHeader | `SignatureHeader: <secret>` |

Every scheme also sends `webhook-id` and `webhook-timestamp` (unix seconds), so receivers can drop repeated deliveries. The `Webhook` metadata gets `EventId`, `EventType` and `Scheme`.

## Retries

Put a **Retry** component in front: Retry → **Webhook sender** → Http client. When the Http client fails, Retry sends the original record again through the sender, which signs it again with a fresh timestamp (receivers reject old timestamps) and the same `webhook-id` (from the record id, or `EventId`). Connect Retry's `MaxRetries` pin to a log or database for deliveries that never succeeded.

## Receiver code

The `ReceiverLanguage` button opens code for the app that receives these webhooks (Download saves it): C# (.NET 8+,
ASP.NET Core minimal API, no packages), Python (3.10+, standard library only), Go (1.24+, standard library only, an `http.Handler`) Java (17+, JDK HttpServer, Jackson 2.15+ for the payload) or PHP (8.1+, no packages, a plain script or `php -S`). It contains the payload types (the
type connected to `Input`; a TextRecord gives text, a WebHttpRequest the raw body, anything else JSON), a
`<Payload>WebhookVerifier` that checks the signature the way `SignatureScheme` signs it (constant time, 300 seconds
tolerance for the timestamp of Stripe, Slack and StandardWebhooks, a secondary secret while rotating), unwraps the
envelope and returns the event id, type, timestamp and payload, and an endpoint on the path of `Uri`:
`app.Map<Payload>Webhook(...)` in C#, `serve(...)` (http.server) in Python with a FastAPI example, `verifier.Handler(...)` in Go, `serve(port, verifier, handler)` (JDK HttpServer) in Java with a Spring Boot controller as a comment, `<Payload>WebhookEndpoint::handle(...)` in PHP with a Laravel route as a comment. The same menu gives an OpenAPI 3.1 document (YAML or JSON) of what the sender
sends (`webhooks`), with the signature in `x-viaducta-signature`. The endpoint answers
401 for a failed check, 400 for a body that is not the expected JSON, 500 when the app's handler fails (a Retry in front
of the sender sends the event again with the same `webhook-id`) and 200 otherwise. The secret is read from the
`VIADUCTA_WEBHOOK_SECRET` (and `VIADUCTA_WEBHOOK_SECRET_SECONDARY`) environment variable or the constructor, never
written into the code. Settings bound to expressions are noted in the file header.

## Notes

- On the Http client leave `ContentType` empty and `Method` at `GET` (the default), otherwise they override what the sender set.
- Headers set on the Http client are added too; they are not part of the signature.
- One webhook per record; batches are not collected.
- Retry keeps pending retries in memory only.

## Example

Database query → Retry → **Webhook sender** (StandardWebhooks, `Uri` = expression on the subscriber URL, `Secret` = expression on the subscriber secret, `EventType` = `order.created`) → Http client
