> ## Documentation Index
> Fetch the complete documentation index at: https://trigger.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Sources and verification

> Choose an endpoint's source, configure verification, and type the event its subscribers receive.

An endpoint's `source` defines how Trigger.dev verifies requests and types the event every subscriber receives. Choose a named provider source or supply a verification configuration with `webhooks.custom()`.

## Presets

Presets supply the verification configuration, and most also carry the provider's part of the endpoint's [setup prompt](https://trigger.dev/docs/webhooks/connect.md#set-it-up-with-an-ai-agent). Set the endpoint's signing secret or public key as described in [Connect a provider](https://trigger.dev/docs/webhooks/connect.md).

```ts /trigger/stripe.ts theme={"theme":"css-variables"}
import { webhook, webhooks } from "@trigger.dev/sdk";

export const stripe = webhooks.endpoint.define({ id: "stripe", source: webhooks.stripe() });

export const stripeEvents = webhook({
  id: "stripe-events",
  endpoint: stripe,
  onEvent: async ({ event }) => {
    if (event.type === "payment_intent.succeeded") {
      console.log(event.id);
    }
  },
});
```

```ts /trigger/github.ts theme={"theme":"css-variables"}
import { webhook, webhooks } from "@trigger.dev/sdk";

export const github = webhooks.endpoint.define({ id: "github", source: webhooks.github() });

export const githubEvents = webhook({
  id: "github-events",
  endpoint: github,
  onEvent: async ({ headers }) => {
    // GitHub puts the event type in a header
    console.log(headers.get("x-github-event"));
  },
});
```

```ts /trigger/svix.ts theme={"theme":"css-variables"}
import { webhook, webhooks } from "@trigger.dev/sdk";

export const svix = webhooks.endpoint.define({ id: "svix", source: webhooks.svix() });

export const svixEvents = webhook({
  id: "svix-events",
  endpoint: svix,
  onEvent: async ({ event }) => {
    console.log(event);
  },
});
```

```ts /trigger/composio.ts theme={"theme":"css-variables"}
import { webhook, webhooks } from "@trigger.dev/sdk";

export const composio = webhooks.endpoint.define({ id: "composio", source: webhooks.standardWebhooks() });

export const composioEvents = webhook({
  id: "composio-events",
  endpoint: composio,
  onEvent: async ({ event }) => {
    console.log(event);
  },
});
```

The available presets are `stripe()`, `github()`, `svix()`, `standardWebhooks()`, `square()`, and `discord()`.

### Svix and Standard Webhooks

`svix()` and `standardWebhooks()` use HMAC-SHA256 over `id.timestamp.body` with a base64-encoded `whsec_` secret. Their header names differ:

| Source | Headers |
| - | - |
| `svix()` | `svix-id`, `svix-timestamp`, `svix-signature` |
| `standardWebhooks()` | `webhook-id`, `webhook-timestamp`, `webhook-signature` |

Named sources such as `webhooks.clerk()` and `webhooks.composio()` select the corresponding configuration.

## Named providers

Each function below returns a source with its provider's verification configuration. Call it through `webhooks`, for example `webhooks.calCom()`.

| Verification | Source functions |
| - | - |
| Svix | `clerk()`, `resend()`, `brex()`, `autumn()` |
| Standard Webhooks | `openai()`, `replicate()`, `recallai()`, `composio()`, `loops()`, `granola()` |
| HMAC-SHA256 with `X-Hub-Signature-256` | `whatsapp()` |
| Shared secret in `X-Gitlab-Token` | `gitlab()` |
| Shared secret as `Authorization: Bearer` | `supabase()` |
| Hookdeck relay (HMAC-SHA256 with `X-Hookdeck-Signature`) | `hookdeck()` |
| Provider-specific HMAC | `calendly()`, `calCom()`, `sentry()`, `pagerduty()`, `vercel()`, `linear()`, `notion()`, `typeform()`, `docusign()`, `jira()`, `intercom()`, `zendesk()`, `attio()`, `workos()`, `elevenlabs()`, `vapi()`, `retell()`, `shopify()`, `metronome()`, `mux()`, `tailscale()`, `langfuse()`, `infisical()`, `planetscale()`, `plain()`, `firecrawl()` |

Provider IDs containing a hyphen use a camel case function name: the `cal-com` provider is `webhooks.calCom()`.

Supabase database webhooks aren't signed, so `supabase()` checks a shared secret instead. Generate the endpoint's secret on Trigger.dev, then add an `Authorization` header with the value `Bearer <secret>` under the webhook's HTTP Headers in Supabase.

Slack also ships as a chat channel in `@trigger.dev/slack`.

For a complete project built on `resend()`, see the [Resend email agent example](https://github.com/triggerdotdev/examples/tree/main/resend-slack-email-agent). It drafts replies to a Resend inbox and waits for your approval in Slack before sending them.

### Hookdeck

`hookdeck()` verifies requests that a [Hookdeck](https://hookdeck.com) connection forwards to the endpoint. Hookdeck forwards the original provider's body unchanged, so pass that provider's event type:

```ts /trigger/stripe-via-hookdeck.ts theme={"theme":"css-variables"}
import type Stripe from "stripe";
import { webhook, webhooks } from "@trigger.dev/sdk";

export const stripeViaHookdeck = webhooks.endpoint.define({
  id: "stripe-via-hookdeck",
  source: webhooks.hookdeck<Stripe.Event>(),
});

export const paymentSucceeded = webhook({
  id: "payment-succeeded",
  endpoint: stripeViaHookdeck,
  filter: "event.type == 'payment_intent.succeeded'",
  onEvent: async ({ event }) => {
    console.log(event.id);
  },
});
```

Use the endpoint URL as the connection's HTTP destination, keep the destination's authentication on Hookdeck Signature (the default for new destinations), and store the Hookdeck project's signing secret from **Settings > Secrets** as the endpoint's signing secret.

* **Secret rotation:** when you roll the signing secret in Hookdeck with an expiry, Hookdeck also signs each request with the previous secret in `X-Hookdeck-Signature-2`, so deliveries keep verifying until you store the new secret.
* **Deduplication:** retries dedupe on `X-Hookdeck-Eventid`, which is the same on every attempt of a Hookdeck event.
* **Custom header prefix:** if you changed the header prefix in the Hookdeck project's settings, pass it with `webhooks.hookdeck({ headerPrefix: "x-acme" })`. The prefix renames the event id header; the signature headers keep the `X-Hookdeck-` name.

## Endpoint validation

Some sources also define a response for the provider's endpoint-validation request. These requests don't create deliveries or task runs.

`discord()` answers a verified `{ type: 0 }` PING with HTTP `204` and no body. It also returns `204` for accepted events and `401` for invalid signatures. Set the Discord application's public key as the endpoint's verification credential.

Slack's `url_verification` challenge is handled the same way by `@trigger.dev/slack`.

`whatsapp()` answers Meta's GET verification request when `hub.mode` is `subscribe` and `hub.verify_token` matches the endpoint's verify token. It returns `hub.challenge` as text. The verify token is separate from the App Secret used to verify event signatures; see [Verify a WhatsApp endpoint URL](https://trigger.dev/docs/webhooks/connect.md#verify-a-whatsapp-endpoint-url). Endpoints without GET verification return `405` to GET requests.

## Custom providers

`webhooks.custom()` accepts a verification configuration. This source verifies an HMAC-SHA256 signature over the raw body in `x-webhook-signature` and uses the event's `id` for deduplication:

```ts /trigger/internal.ts theme={"theme":"css-variables"}
import { webhook, webhooks } from "@trigger.dev/sdk";

export const internal = webhooks.endpoint.define({
  id: "internal",
  source: webhooks.custom<{ id: string; message: string }>(
    {
      scheme: "hmac",
      algorithm: "sha256",
      encoding: "hex",
      signatureHeader: "x-webhook-signature",
      signingString: "raw",
      idempotencyField: { from: "body", name: "id" },
    },
    { setupPrompt: "Add the URL under Settings > Webhooks in the admin app and paste the secret there." }
  ),
});

export const internalEvents = webhook({
  id: "internal-events",
  endpoint: internal,
  onEvent: async ({ event }) => {
    console.log(event.message);
  },
});
```

The optional `setupPrompt` is the provider-specific part of the endpoint's [setup prompt](https://trigger.dev/docs/webhooks/connect.md#set-it-up-with-an-ai-agent): where the provider's webhook settings are and how to register a URL there. Trigger.dev adds the URL, the subscribers' filters and the signing secret steps around it.

### Form-encoded bodies

A verified body sent as `application/x-www-form-urlencoded` reaches subscribers as an object of its fields, so filters, `chat.event` keys and waiter `match` paths read form fields the way they read JSON fields. A repeated field becomes an array of its values. Slack slash commands and other providers that post forms work this way.

By default an accepted request gets a `200` with a small JSON receipt. Some providers show the response body to the user who triggered the request, so pass `response: { acceptedBody: "empty" }` to answer with an empty `200` instead:

```ts /trigger/forms.ts theme={"theme":"css-variables"}
import { webhook, webhooks } from "@trigger.dev/sdk";

export const forms = webhooks.endpoint.define({
  id: "forms",
  source: webhooks.custom<{ command: string; text: string; user_id: string }>(
    { scheme: "url-secret", placement: "query", paramName: "token" },
    { response: { acceptedBody: "empty" } }
  ),
});

export const commands = webhook({
  id: "commands",
  endpoint: forms,
  filter: "event.command == '/deploy'",
  onEvent: async ({ event }) => {
    console.log(event.user_id, event.text);
  },
});
```

## Typing the event

Stripe and GitHub sources provide default event shapes; other sources default to `unknown`. Pass an event type to the source function for typed handler parameters and autocomplete. This example uses `Stripe.Event` from the `stripe` package, which must be installed in your project:

```ts /trigger/stripe.ts theme={"theme":"css-variables"}
import type Stripe from "stripe";
import { webhook, webhooks } from "@trigger.dev/sdk";

export const stripe = webhooks.endpoint.define({ id: "stripe", source: webhooks.stripe<Stripe.Event>() });

export const stripeEvents = webhook({
  id: "stripe-events",
  endpoint: stripe,
  onEvent: async ({ event }) => {
    console.log(event.type, event.id);
  },
});
```

For `webhooks.custom<T>()`, pass your own event type as `T`. The endpoint's event type flows to every subscriber: `webhook()` handlers, `chat.event` keys and filters, and [waiter](https://trigger.dev/docs/webhooks/waiters.md) `match` paths are all checked against it.

> [!NOTE]
>
> Event types are compile-time annotations. Signature verification doesn't validate the body against
> a TypeScript type. Validate `event` inside `onEvent` if your handler needs runtime schema checks.

## Verification schemes

Requests must pass verification before they reach any subscriber. Sources can check a signature or credential, enforce a timestamp tolerance, and derive an idempotency key for [deduplication](https://trigger.dev/docs/webhooks/deliveries.md#deduplication). Verification failures return the source's rejected HTTP status (`400` by default).

Custom sources support these schemes:

| Scheme | Verification |
| - | - |
| `hmac` | Computes an HMAC over the raw body or a signing template and compares it with a request header. |
| `shared-secret` | Compares a static token supplied in a header, bearer token, basic authentication, or request body. |
| `url-secret` | Compares a token in a URL query parameter. Hosted endpoints don't support path placement. |
| `asymmetric` | Verifies an Ed25519, ECDSA (P-256 with SHA-256, P-384 with SHA-384), or RSA-SHA256 signature using the provider's public key. Set `prehash: "sha256"` when the provider signs a SHA-256 digest of the signing string instead of the string itself. |

> [!WARNING]
>
> Query parameters can appear in access logs, proxies, and traces. Prefer a header-based scheme when
> the provider supports one.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.