> For the complete documentation index, see [llms.txt](/llms.txt).
> A full single-fetch corpus is available at [llms-full.txt](/llms-full.txt).
---
title: Webhooks
description: Trigger workflows from external events sent by Standard Webhooks, Sentry, Stripe, GitHub, or Slack, with signature verification and idempotent delivery.
last_verified: 2026-06-04
---

A webhook lets an external system start one of your workflows by POSTing an event to AGNT5. The gateway verifies the sender's signature, turns the delivery into a durable event, and starts every workflow subscribed to it. You write a workflow and declare which event it listens for. AGNT5 handles receipt, verification, deduplication, and dispatch.

This page covers the mechanism end to end using **Standard Webhooks**, a publisher you control. To connect a specific third-party service, see [Integrations → Event sources](/docs/integrations/event-sources/overview.md), which layers provider-specific setup on top of everything here.

---

## How it works

1. You create a webhook integration in Studio and receive a URL and a signing secret.
2. The external system POSTs events to `/v1/webhooks/{source}/{integration_id}` (Studio shows the full URL).
3. The gateway verifies the HMAC signature against your secret and rejects anything that fails with `401` before any workflow runs.
4. It derives an event name of the form `{source}.{event}` and starts every workflow whose trigger matches.
5. It returns `200` once the event is durably persisted to the log, which happens before your workflow finishes running.

---

## Declare a trigger

Subscribe a workflow to a webhook event with the `webhook()` trigger:

```python
from agnt5 import workflow, webhook

@workflow(
    name="triage_issue",
    triggers=[webhook("sentry", event="issue.created")],
)
async def triage_issue(ctx, event: dict) -> dict:
    payload = json.loads(event["body"])  # raw request body, as a string
    issue = payload["data"]["issue"]
    ...
```

`webhook("sentry", event="issue.created")` subscribes the workflow to the event `sentry.issue.created`. The `source` is one of `standard`, `sentry`, `stripe`, `github`, or `slack`. The `event` is the identifier within that source. The format differs per provider; see [Event names](#event-names) below.

A single event can fan out to multiple workflows: every workflow whose trigger matches `{source}.{event}` starts independently.

---

## What your workflow receives

Each matched workflow starts with one envelope as its input:

```json
{
  "_webhook": true,
  "source": "sentry",
  "integration_id": "int_abc123",
  "event_type": "sentry.issue.created",
  "idempotency_key": "req_9f3c…",
  "timestamp": 1733337600,
  "headers": { "sentry-hook-resource": "issue", "request-id": "req_9f3c…" },
  "body": "{\"action\":\"created\",\"data\":{ … }}"
}
```

`body` is the raw request body as a string. Parse it yourself so you operate on exactly the bytes that were signature-verified. `headers` keys are lowercased. `idempotency_key` is the provider's stable per-delivery id (absent for Slack, see [Delivery semantics](#delivery-semantics)).

---

## Set up an integration

You connect the sending side in Studio, once per source:

1. **Studio → Integrations → New**, then pick a source.
2. Choose the **environment** whose deployment should receive the triggers.
3. Provide the **signing secret**. For GitHub and Standard Webhooks, AGNT5 generates the secret. Copy it into the publisher. For Stripe, Slack, and Sentry, paste the secret the provider issues; generating one here would never match.
4. Copy the **webhook URL** Studio shows (`…/v1/webhooks/{source}/{integration_id}`) into the provider's webhook settings.

Provider-by-provider walkthroughs live under [Integrations → Event sources](/docs/integrations/event-sources/overview.md).

---

## Signature verification

Every delivery must carry a valid signature; AGNT5 rejects unsigned or mismatched requests with `401` before any workflow runs. Each source uses its provider's native HMAC-SHA256 scheme:

| Source | Signature header | Signed payload | Replay window |
|---|---|---|---|
| `standard` | `webhook-signature` (`v1,<base64>`) | `{id}.{timestamp}.{body}` | 5 min |
| `sentry` | `sentry-hook-signature` (hex) | raw body | n/a |
| `stripe` | `Stripe-Signature` (`t=…,v1=…`) | `{timestamp}.{body}` | 5 min |
| `github` | `X-Hub-Signature-256` (`sha256=…`) | raw body | n/a |
| `slack` | `X-Slack-Signature` (`v0=…`) + timestamp | `v0:{timestamp}:{body}` | 5 min |

Standard Webhooks and Stripe accept several signatures on one delivery, so you can rotate signing keys without dropping events.

---

## Delivery semantics

Webhook delivery is **at-least-once**. Publishers retry on any non-2xx response, and AGNT5 collapses retries using the provider's per-delivery idempotency key: `webhook-id` for Standard Webhooks, `Request-ID` for Sentry, the event `id` for Stripe, `X-GitHub-Delivery` for GitHub. A retried delivery replays the original run instead of starting a new one.

Two cases fall outside that deduplication, and either can start a workflow more than once for the same event:

- **Slack** carries no stable per-delivery id, so Slack retries are not de-duplicated.
- The idempotency cache is per–gateway instance and time-bounded. A retry that arrives after a gateway restart, or lands on a different gateway in a multi-node deployment, sees a cold cache.

<Callout type="warning">**Make webhook-triggered workflows idempotent.** Key your side effects off `event_type` and `idempotency_key` (or an id inside the body) so a re-delivery is a no-op. AGNT5 guarantees a workflow runs at least once per event, not exactly once.</Callout>

---

## Event names

The event your trigger matches is always `{source}.{event}`. What you pass as `event` depends on the source:

| Source | `event` you pass | Resulting event name |
|---|---|---|
| `standard` | the `webhook-event` header value | `standard.<event>` |
| `sentry` | `<resource>.<action>` | `sentry.issue.created` |
| `stripe` | the body `type` | `stripe.payment_intent.succeeded` |
| `github` | `<event>.<action>`, or just `<event>` | `github.issues.opened` |
| `slack` | the event-callback type | `slack.app_mention` |

---

## Related

- [Integrations → Event sources](/docs/integrations/event-sources/overview.md): connect Sentry, Stripe, GitHub, and Slack
- [Integrations → Overview](/docs/integrations/overview.md): how integrations and signing secrets are stored
- [Workflows](/docs/build/workflows.md): what a workflow is and how it runs
