Skip to content

Commit a46f330

Browse files
ericallamclaude
andcommitted
feat(sdk,cli,core): webhook() with typed provider sources, filters, and indexing
The public SDK half of hosted webhooks: `webhook()` declares an endpoint that routes a verified, typed event to `onEvent`, with provider presets (`webhooks.stripe()`, `webhooks.github()`, `webhooks.svix()`, and the rest of the config-table producers) or `webhooks.custom<T>()`, and an optional type-checked `filter`. The CLI indexes declared webhooks into the worker manifest and fails indexing on duplicate webhook ids. Docs cover sources, connecting a provider, deliveries, and filters. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PPzBQgFgqD8aPQcEYZXty
1 parent 752573c commit a46f330

16 files changed

Lines changed: 775 additions & 2 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@trigger.dev/core": minor
3+
"@trigger.dev/sdk": minor
4+
"trigger.dev": minor
5+
---
6+
7+
Add hosted webhooks: receive and verify provider webhooks as a task, with no ingress or verification code of your own.
8+
9+
- `webhook()` declares an endpoint that routes a verified, typed event to an `onEvent` handler. Choose a source with a preset (`webhooks.stripe()`, `webhooks.github()`, and others) or `webhooks.custom<T>(config)`. Declared webhooks are discovered like tasks and synced to a hosted URL on deploy.
10+
- `filter` gates which deliveries run, using a type-safe expression checked against the event at author time (`event.`/`header.`/`webhook.` paths, `&&`/`||`, comparison and `in`/`contains` operators, field-to-field comparison, and array quantifiers). A non-matching delivery is still recorded, not routed.
11+
- HTTP API for listing webhook endpoints and deliveries, plus rotate-secret, enable/disable, and replay.

docs/docs.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,16 @@
152152
}
153153
]
154154
},
155+
{
156+
"group": "Webhooks",
157+
"pages": [
158+
"webhooks/overview",
159+
"webhooks/sources",
160+
"webhooks/connect",
161+
"webhooks/deliveries",
162+
"webhooks/filters"
163+
]
164+
},
155165
{
156166
"group": "Configuration",
157167
"pages": [

docs/webhooks/connect.mdx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
title: "Connecting a provider"
3+
description: "Point a provider at the webhook URL and set the signing secret."
4+
sidebarTitle: "Connecting a provider"
5+
---
6+
7+
When you deploy (or run `dev`), each webhook task gets an **endpoint** with a unique, unguessable webhook URL. Open the webhook in the dashboard, go to **Endpoints**, and open the endpoint to find its **Connect** panel.
8+
9+
<Steps>
10+
<Step title="Copy the webhook URL">
11+
Copy it from the endpoint's Connect panel. On Trigger.dev Cloud it looks like
12+
`https://webhooks.trigger.dev/webhooks/v1/ingest/<id>`. A self-hosted instance serves it from that
13+
instance's own base URL. This is what you give the provider as its webhook destination.
14+
</Step>
15+
<Step title="Set the signing secret">
16+
A webhook can't accept deliveries until its signing secret is set. Until then every request is
17+
rejected. There are two flows, and the Connect panel shows the right one for the provider:
18+
19+
- **The provider generates the secret** (Stripe, Svix): copy it from the provider and paste it
20+
into **Set secret**.
21+
- **You choose the secret** (GitHub, or a service you control): click **Generate secret** and
22+
Trigger.dev mints a strong secret and shows it once. Paste that into the provider's webhook config.
23+
</Step>
24+
<Step title="Point the provider at the webhook URL">
25+
Add the webhook URL as the destination in your provider's dashboard. The Connect panel
26+
shows the exact signature scheme (header, algorithm, signing string) the provider should use.
27+
</Step>
28+
</Steps>
29+
30+
<Warning>
31+
The signing secret is stored encrypted and is never shown again after it's set. To rotate it,
32+
use **Rotate secret** (or **Regenerate**) and update the provider with the new value.
33+
</Warning>
34+
35+
Once a provider is sending events, watch them arrive on the [Deliveries](/webhooks/deliveries) page, which also explains what an [endpoint](/webhooks/deliveries#endpoints) is.

docs/webhooks/deliveries.mdx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
title: "Deliveries and endpoints"
3+
description: "Observe inbound webhook requests, the runs they trigger, and their payloads in the dashboard."
4+
sidebarTitle: "Deliveries & endpoints"
5+
---
6+
7+
The dashboard surfaces two concepts under the **Webhooks** section.
8+
9+
## Deliveries
10+
11+
A delivery is a single inbound request that passed verification. The **Deliveries** page lists every
12+
delivery across all your webhooks (much like the Runs page), and you can filter by webhook, status,
13+
delivery id, or run id.
14+
15+
Open a delivery to see:
16+
17+
- Its **status** and the **run** it triggered (linked).
18+
- The verified **event payload** and the inbound **request headers**, on separate tabs.
19+
- The external delivery id, idempotency key, and timestamps.
20+
21+
<Note>
22+
Duplicate deliveries are deduplicated automatically. The idempotency key is the provider's event id
23+
(e.g. the Stripe event id, or GitHub's `X-GitHub-Delivery`), so a provider retry of the same event
24+
resolves to the original delivery and won't trigger a second run.
25+
</Note>
26+
27+
## Endpoints
28+
29+
An endpoint is the connection instance for a webhook: its webhook URL, signing-secret state,
30+
verification scheme, and delivery history. Each webhook's **Endpoints** tab lists its endpoints (a
31+
declared webhook has one), and opening an endpoint shows its [Connect panel](/webhooks/connect) and
32+
its scoped deliveries.
33+
34+
## What happens to a request
35+
36+
<Steps>
37+
<Step title="Verify">
38+
The signature, timestamp, and idempotency key are checked. A failure returns `400` and records
39+
nothing.
40+
</Step>
41+
<Step title="Record">
42+
A verified request becomes a delivery, with its parsed event and headers stored.
43+
</Step>
44+
<Step title="Route">
45+
The delivery is routed to your webhook task, which runs and calls `onEvent`. The delivery's status
46+
reflects that run's outcome.
47+
</Step>
48+
</Steps>

docs/webhooks/filters.mdx

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
title: "Filtering deliveries"
3+
description: "Gate which verified webhook deliveries run, with a type-safe filter checked against the event."
4+
sidebarTitle: "Filters"
5+
---
6+
7+
By default every verified delivery runs your `onEvent`. A **filter** is a server-side predicate that decides whether a delivery is routed at all. A delivery that does not match is still received and recorded, it just does not run anything.
8+
9+
Filtering happens at the endpoint, before any run is triggered, so a filtered-out event costs you nothing.
10+
11+
## Adding a filter
12+
13+
Pass a `filter` string to `webhook()`. It is a small expression checked, at build time, against the event shape from your [source](/webhooks/sources#typing-the-event):
14+
15+
```ts
16+
import { webhook, webhooks } from "@trigger.dev/sdk";
17+
18+
export const onOrder = webhook({
19+
id: "orders",
20+
source: webhooks.stripe(),
21+
// only route succeeded payment intents over $100
22+
filter: "event.type == 'payment_intent.succeeded' && event.data.object.amount >= 10000",
23+
onEvent: async ({ event }) => {
24+
// only runs for deliveries that matched
25+
},
26+
});
27+
```
28+
29+
The filter is type-safe: referencing a field that does not exist, or comparing it to the wrong kind of literal, is a compile error, not a runtime surprise.
30+
31+
## What a non-match does
32+
33+
A delivery that does not match is **not dropped**. It still returns `200` to the provider and is recorded as a [delivery](/webhooks/deliveries) with the status `FILTERED` and a reason naming the clause that failed (and the value it saw). It just never triggers a run. This keeps a filtered delivery auditable: you can see in the dashboard that it arrived and why it was not routed.
34+
35+
<Note>
36+
If a filter throws while evaluating (for example, a malformed event), the delivery is routed rather
37+
than dropped. Filters fail open so a filter bug never silently swallows real events.
38+
</Note>
39+
40+
## The expression language
41+
42+
A filter is one or more `path operator value` clauses combined with `&&` and `||` (use parentheses to group).
43+
44+
### Paths
45+
46+
A path reads from one of three namespaces:
47+
48+
- `event.*`: the verified, parsed request body, for example `event.data.object.amount`.
49+
- `header.*`: an inbound request header, matched case-insensitively, for example `header.x-github-event`.
50+
- `webhook.*`: endpoint metadata (`webhook.source`, `webhook.id`, `webhook.deliveryId`, and for per-tenant endpoints `webhook.externalRef` / `webhook.tenantId`).
51+
52+
### Operators
53+
54+
| Operator | Meaning |
55+
| --- | --- |
56+
| `==` `!=` | equality |
57+
| `>` `<` `>=` `<=` | numeric comparison |
58+
| `in` `not in` | membership in a list, for example `event.type in ['a','b']` |
59+
| `startsWith` `endsWith` `contains` | string matching |
60+
61+
Values are strings in single quotes (`'created'`), numbers (`10000`), booleans (`true`), or a list for `in` / `not in`.
62+
63+
### Comparing two fields
64+
65+
The right-hand side can be another path instead of a literal, so you can compare two fields of the same event:
66+
67+
```ts
68+
filter: "event.billing.country == event.shipping.country";
69+
```
70+
71+
### Matching inside a list
72+
73+
`any` and `all` quantify over an array, testing a sub-path on each element:
74+
75+
```ts
76+
// route only if at least one line item has a positive quantity
77+
filter: "event.items any ( quantity > 0 )";
78+
```
79+
80+
### Spacing
81+
82+
The type checker reads the filter as a token stream, so a couple of spots are strict about spacing: keep `in` / `not in` lists unspaced (`['a','b']`, not `[ 'a', 'b' ]`) and put spaces around the quantifier parentheses (`any ( ... )`).
83+
84+
To match only certain event types, write a clause against the field that carries the type: `event.type` for Stripe / Svix / Square / Discord, or the `x-github-event` header for GitHub (the filter DSL can read a `header.` namespace too):
85+
86+
```ts
87+
import { webhook, webhooks } from "@trigger.dev/sdk";
88+
89+
export const onGithub = webhook({
90+
id: "github",
91+
source: webhooks.github(),
92+
filter: "header.x-github-event in ['issues','pull_request']",
93+
onEvent: async ({ event }) => {},
94+
});
95+
```

docs/webhooks/overview.mdx

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
---
2+
title: "Webhooks overview"
3+
description: "Receive and verify webhooks from external providers as a task, with a hosted webhook URL."
4+
sidebarTitle: "Overview"
5+
---
6+
7+
A webhook is a task that runs when an external provider (Stripe, GitHub, Svix, your own service, …) sends an HTTP request. Trigger.dev gives each webhook a hosted webhook URL, verifies the incoming request's signature, and routes the verified event to your task's `onEvent` handler.
8+
9+
You don't host an endpoint yourself, and you don't write verification code: you declare which provider the webhook is from, point the provider at the webhook URL, and set the signing secret.
10+
11+
## Defining a webhook task
12+
13+
A webhook is created with `webhook()`. It takes an `id`, a `source` (which provider, and how to verify it), and an `onEvent` handler:
14+
15+
```ts
16+
import { webhook, webhooks } from "@trigger.dev/sdk";
17+
18+
export const onStripeEvent = webhook({
19+
id: "stripe-events",
20+
source: webhooks.stripe(),
21+
onEvent: async ({ event, headers, ctx }) => {
22+
// `event` is the verified, parsed body
23+
console.log("Received", event.type, event.id);
24+
25+
// `headers` is a standard Web Headers object
26+
console.log(headers.get("stripe-signature"));
27+
28+
// `ctx` is the usual run context
29+
console.log(ctx.run.id);
30+
},
31+
});
32+
```
33+
34+
`onEvent` receives:
35+
36+
- **`event`**: the verified request body, parsed from JSON and typed by the source (see [Typing the event](/webhooks/sources#typing-the-event)).
37+
- **`headers`**: the inbound request headers as a Web [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object (case-insensitive `.get()` / `.has()`).
38+
- **`ctx`**: the run context, the same one regular tasks receive.
39+
40+
<Note>
41+
A webhook is a first-class task kind. It runs on a real run (with retries, logs, and everything else
42+
tasks get), and shows up in the dashboard alongside your other tasks.
43+
</Note>
44+
45+
## How it works
46+
47+
<Steps>
48+
<Step title="Declare the webhook">
49+
Define a `webhook()` with a `source`. The source is a provider preset (like `webhooks.stripe()`)
50+
or a `webhooks.custom()` config. See [Sources and verification](/webhooks/sources).
51+
</Step>
52+
<Step title="Connect a provider">
53+
Deploying the webhook creates an endpoint with a hosted webhook URL. Set its signing secret and
54+
point your provider at the URL. See [Connecting a provider](/webhooks/connect).
55+
</Step>
56+
<Step title="Receive verified events">
57+
Each inbound request is verified, recorded as a delivery, and routed to a run that calls your
58+
`onEvent`. See [Deliveries and endpoints](/webhooks/deliveries).
59+
</Step>
60+
</Steps>
61+
62+
## Beyond fan-out
63+
64+
A few things build on the basic model:
65+
66+
- **[Filters](/webhooks/filters)** gate which deliveries run. A non-matching delivery is recorded but never triggers a run.
67+
68+
<CardGroup cols={2}>
69+
<Card title="Sources and verification" icon="shield-check" href="/webhooks/sources">
70+
Provider presets, custom verification, and typing the event.
71+
</Card>
72+
<Card title="Connecting a provider" icon="plug" href="/webhooks/connect">
73+
The webhook URL and signing secret.
74+
</Card>
75+
<Card title="Deliveries and endpoints" icon="inbox" href="/webhooks/deliveries">
76+
Observe inbound requests in the dashboard.
77+
</Card>
78+
<Card title="Filters" icon="filter" href="/webhooks/filters">
79+
Route only the deliveries you care about.
80+
</Card>
81+
<Card title="Scheduled tasks" icon="clock" href="/tasks/scheduled">
82+
The other declarative task trigger.
83+
</Card>
84+
</CardGroup>

0 commit comments

Comments
 (0)