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

# Send and receive a world's webhooks

> A world can notify your agent the way its vendor would. Declare where subscriptions live, send from a handler, then receive deliveries on your machine or let the platform push them to a public URL.

A world can notify the way its vendor does. When the agent registers a webhook through the world's
API (`POST /v0/webhooks` on AgentMail, `POST /repos/{owner}/{repo}/hooks` on GitHub), every
matching event the world later sends reaches that URL. Each delivery is signed the vendor's way, so
your receiver verifies it with the secret the world returned when the webhook was registered.

Every delivery is also recorded in the world's request ledger, so a check can grade it whether or
not anything was listening.

## Declare where subscriptions live

Add a `[webhooks]` table to `connector.toml`. It names the entity the vendor's own routes write
subscriptions into, and the fields of that entity that matter:

```toml theme={null}
[webhooks]
subscriptions = "webhooks"              # the entity POST /v0/webhooks writes
url = "url"
events = "event_types"                  # absent or empty: every event
secret = "secret"
enabled = "enabled"                     # absent: always on
scope = { inbox_id = "inbox_ids" }      # an event scoped to an inbox reaches rows listing it, or listing none
signing = "svix"
```

| `signing` | Headers each delivery carries |
| - | - |
| `svix` | `svix-id`, `svix-timestamp`, `svix-signature` (AgentMail, Resend, Clerk) |
| `github` | `x-github-event`, `x-github-delivery`, `x-hub-signature-256` |
| `linear` | `linear-event`, `linear-delivery`, `linear-signature` |
| `stripe` | `stripe-signature` (`t=…,v1=…`) |
| `hmac-sha256` | `sha256=` plus the hex signature, under the header `signature_header` names |
| `none` | none; `secret` is not needed |

`gateway worlds schema check` refuses a table that names an entity or field the contract does not
declare.

## Send from a handler

A route handler calls `ctx.deliver` with the event type, the body and, when the table declares one,
the scope:

```python theme={null}
def send_message(ctx, args, query, body):
    message = ctx.insert("messages", {...})
    ctx.deliver(
        "message.sent",
        {"type": "event", "event_type": "message.sent", "message": message},
        scope={"inbox_id": args["inbox_id"]},
    )
    return message
```

A tool sends the same way: `from gateway_env import webhooks`, then `webhooks.deliver(...)`.

Every subscription that is enabled, wants the event and holds its scope gets one delivery. A
delivery is kept only when the call that sent it succeeds: a call that answers 4xx or 5xx, or
raises, sends nothing, just as it writes nothing. The body is fixed as JSON when you call `deliver`,
so what the receiver gets is exactly what was signed.

<Info>
  Limits: 100 deliveries per event, 500 per call, 256 KB per body. A subscription whose URL no
  receiver could be at, or whose secret its scheme cannot sign with, is skipped; the call still
  succeeds.
</Info>

The GitHub, Linear and Jamf templates already send their vendors' webhooks: `gateway worlds schema
init ./x --connector github` gives you a world whose issue and pull request routes notify registered
hooks.

## See a session's webhooks

```bash theme={null}
gateway worlds session webhooks <sessionId>
```

This lists the subscriptions the session's world holds: URL, events, scope, on or off, and how many
deliveries each was sent. Secrets are never shown. The session's page in the app shows the same list
under **Webhooks**.

## Receive them on your machine

`session listen` forwards each delivery to a receiver you run locally, with its signed headers and
exact body:

```bash theme={null}
gateway worlds session listen <sessionId> --forward-to http://localhost:3000/webhooks
```

Your receiver's answer (its status, or `0` and why it never answered) goes back to the world's
request ledger, once per delivery.

| Flag | What it does |
| - | - |
| `--replay` | Also forward what the session already holds, not only what it sends from now |
| `--once` | Forward everything the session holds now, then exit (implies `--replay`) |
| `--interval-ms` | Wait between polls when nothing is new (100 to 3600000) |

A failed poll is retried until you press Ctrl-C. A session that ended stops `listen` with exit
code `1`.

<Warning>
  Svix receivers refuse a delivery older than about five minutes. Use `--replay` only for deliveries
  that are still fresh.
</Warning>

## Let the platform push them to a public URL

When your receiver is on the internet, the platform can deliver for you. Turn it on per project in
**Settings → General → World webhooks → Send webhooks to their receivers**. Only project admins and
owners can change it.

While it is on, every running session of the project has its new deliveries posted to their
subscriptions' URLs within seconds, and each receiver's answer is recorded in the world's ledger.

* Only `https` URLs on port 443 receive. Private, loopback and other non-public addresses are
  refused.
* Redirects are not followed. A receiver has 10 seconds to answer.
* Each delivery is sent at most once. A failure is recorded with its reason and not retried.
* A session opened while push is on is pushed from its first delivery. Sessions already running
  when you turn it on start from that moment, so nothing sent while push was off is replayed.

`session listen` and push can run together. The first answer to a delivery is the one kept.

### Send a registered webhook somewhere else

The agent registers whatever URL it chooses. To have those deliveries arrive at your own receiver
instead, open **World settings → Webhook destinations** and map the registered URL to a destination,
or select the pencil next to a subscription in a session's **Webhooks** section.

* The mapping applies to every session of the world, including ones opened later.
* The world itself is unchanged: the agent still reads back the URL it registered, and the ledger
  records it.
* A destination follows the same rules as push: `https` on port 443, a public address, no
  credentials in the URL.
* Push must be on for the project. `session listen` always sends to its own `--forward-to`.

## Grade what the world sent

Deliveries are requests in the world's ledger with `via` `webhook`. The event type is the
`operation`; a receiver's answer is a second entry whose operation ends in `.answer`.

```bash theme={null}
gateway worlds requests acme-world --via webhook
gateway worlds session requests <sessionId> --via webhook
```

A check reads them like any request:

```json theme={null}
{ "entity": "gateway_requests", "where": { "via": "webhook", "operation": "message.received" } }
```

This counts the deliveries the world sent, whether or not anyone was listening. Use
`"operation": "message.received.answer"` to count what receivers answered.


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