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

# Receive SDK events

> Handle provider webhooks and Fused auth lifecycle events through one durable SDK receiver.

A payment clears, an issue moves, or a user finishes connecting an account. The generated SDK receives all of these through one durable event receiver.

The event source determines how it is registered:

1. A **provider webhook** uses a `kind: webhook` config for ingress. The SDK attaches to it and selects the provider events it wants.
2. A **Fused auth lifecycle event** is included automatically when the SDK's selected auth path uses connected OAuth or OIDC. It needs no webhook config or attachment.

Both arrive through `FusedWebhooks`. Creating an SDK client does not start a listener; your application registers handlers when it is ready to consume events.

<Note>
  MCP servers can also select provider webhooks as [event resources](/docs/mcp/receive-events). SDK receivers offer durable ack/nack delivery; MCP offers live update notifications and a read of the latest retained occurrence. Fused auth lifecycle events described here remain an SDK receiver surface.
</Note>

## Register provider ingress

For a [Managed service with event support](/docs/workspace/use-managed-service#receive-managed-provider-events), create a local `relay.source` receiver instead of storing a signing secret. Fused verifies requests at its broker, and your Engine pulls authorized events into this same delivery path. Use the following direct-ingress setup when you own the provider application and signing secret.

A `kind: webhook` config is a named, team-owned bundle that can span several services, with its own plan and apply lifecycle.

```yaml theme={null}
apiVersion: fused/v1
kind: webhook
name: team-x-webhooks
services:
  jira:
    secret: "${bucket.default.secret.jira_signing}"
  github:
    secret: "${bucket.default.secret.github_signing}"
```

```bash theme={null}
fused-cli webhook plan
fused-cli webhook apply
```

Plan validates the local webhook config before contacting the Engine. Use `webhook validate` separately only for an offline-only check.

Apply prints each registration's URL, which is what you give the provider:

```text theme={null}
# reserved default name -> predictable, signature-required:
<engine-url>/webhook/svc/<service>

# every other name -> opaque token:
<engine-url>/webhook/<name>-<service>
```

To look one up later without re-running apply:

```bash theme={null}
fused-cli workspace service webhooks jira
```

Its `SIGNATURE` column reads `set` or `none` — never the secret itself.

### About `name` and `secret`

`name` is this config's identity. The pair `(service, name)` must be globally unique per account: a second config trying to claim a pair another config already owns is a plan-time conflict, never a silent takeover.

`name: default` is reserved. Its registrations get a predictable URL — `/webhook/svc/<service>` — and must therefore be signature-verified: a `default` config whose service declares no inbound webhook signature is rejected at plan time, and an unsigned delivery to a default URL is rejected at ingress.

`services.<slug>.secret` is the signing secret used to verify inbound deliveries. It takes a bucket reference — either `` `${bucket.<name>.secret.<key>}` `` or the shorthand `` `${bucket.secret.<key>}` `` against the `default` bucket. Omit it entirely for a provider that does not sign its webhooks.

<Warning>
  The bucket segment is mandatory here, unlike SDK and MCP injections. Webhook verification has no dispatch-selected bucket to fall back on, so it cannot infer one. The reference must also be the entire field value — no surrounding text.
</Warning>

Removing a service from the map, or deleting the file, is an ordinary apply-time diff. There is no imperative delete command.

## Attach an SDK and pick provider events

```yaml theme={null}
apiVersion: fused/v1
kind: sdk
name: jira-sdk
version: "1.2.0"
bucket: default
webhook_attachment: team-x-webhooks
services:
  jira:
    operations: [createIssue]
    webhooks: ["issue.created", "issue.updated"]
  github:
    operations: [listRepos]
    webhooks: ["push"]
```

`webhook_attachment` is top-level, a sibling of `name` and `bucket` — not nested under `services`, because one webhook config can span services the SDK also uses.

Manage the event list from the CLI if you prefer:

```bash theme={null}
# fused-cli sdk webhook <add|remove> <service-slug> <webhook-id...>
fused-cli sdk webhook add jira issue.created issue.updated
fused-cli sdk webhook remove jira issue.updated
```

`sdk webhook add` accepts `--interactive` when you would rather pick from a list than remember event IDs.

## Fused auth lifecycle events

An SDK whose selected operation auth path uses connected OAuth or OIDC gets these generated event types automatically:

| Generated member                                       | Event suffix                               | When it arrives                                          |
| ------------------------------------------------------ | ------------------------------------------ | -------------------------------------------------------- |
| `JiraWebhook.FUSED_AUTH_CONNECTION_COMPLETED`          | `fused.auth.connection.completed`          | A user connection initiated by this SDK family completes |
| `JiraWebhook.FUSED_AUTH_TOKEN_REFRESHED`               | `fused.auth.token.refreshed`               | Fused refreshes the connected grant                      |
| `JiraWebhook.FUSED_AUTH_TOKEN_REFRESH_FAILED`          | `fused.auth.token.refresh_failed`          | A refresh attempt fails                                  |
| `JiraWebhook.FUSED_AUTH_CONNECTION_RECONNECT_REQUIRED` | `fused.auth.connection.reconnect_required` | The user must authorize the service again                |

Use the enum generated for your service. Its wire value includes the service identity before the event suffix, which keeps events from different services distinct. You do not add these names under `services.<slug>.webhooks`. They need no `kind: webhook`, provider callback URL, signing secret, or `webhook_attachment`. A service merely offering an unused OAuth alternative does not enable them.

The implicit subscription is scoped to the SDK family and service. A later SDK version in the same family can continue receiving its connection events, but another SDK sharing the bucket cannot. Connections started directly from the CLI are not routed into an SDK receiver.

## Handle them in your code

The generated package ships the receiver. Your process **dials out** to the Engine and events stream back down that connection, so there is no public endpoint to host, no ingress to open, and no signature to verify yourself — this works unchanged on a laptop, in a private VPC, or on a serverless container.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import {
    FusedWebhooks,
    FusedAuthConnectionCompletedPayload,
    JiraWebhook,
  } from 'support-sdk';

  const receiver = FusedWebhooks.listen('support', process.env.FUSED_SDK_TOKEN!);

  receiver.on(JiraWebhook.ISSUE_CREATED, async (payload, ctx) => {
    try {
      await triage(payload);
      ctx.ack();
    } catch {
      ctx.nack();
    }
  });

  receiver.on(JiraWebhook.ISSUE_UPDATED, handleChange);

  receiver.on<FusedAuthConnectionCompletedPayload>(
    JiraWebhook.FUSED_AUTH_CONNECTION_COMPLETED,
    async (event) => {
      await handleConnectionComplete(event.end_user_ref);
    }
  );
  receiver.on(JiraWebhook.FUSED_AUTH_CONNECTION_RECONNECT_REQUIRED, handleReconnect);
  ```

  ```python Python theme={null}
  import os

  from fused.support_sdk import FusedWebhooks
  from fused.support_sdk.webhooks import JiraWebhook

  receiver = FusedWebhooks.listen("support", os.environ["FUSED_SDK_TOKEN"])

  async def on_created(payload, ctx):
      try:
          await triage(payload)
      except Exception:
          await ctx["nack"]()
          return
      await ctx["ack"]()

  receiver.on(JiraWebhook.ISSUE_CREATED, on_created)
  receiver.on(JiraWebhook.ISSUE_UPDATED, handle_change)

  receiver.on(JiraWebhook.FUSED_AUTH_CONNECTION_COMPLETED, handle_connection_complete)
  receiver.on(JiraWebhook.FUSED_AUTH_CONNECTION_RECONNECT_REQUIRED, handle_reconnect)
  ```
</CodeGroup>

The receiver name is yours. `on` takes one event or a list, and only events you registered a handler for are dispatched. The token is the SDK family's execution token, not a CLI control key or provider credential.

| Behaviour                      | What it means for you                                                                                                                  |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| Auto-ack                       | A handler that returns without calling `ctx.ack()` or `ctx.nack()` acknowledges. Call `nack()` explicitly to get the event redelivered |
| Auto-reconnect                 | Backs off from 1s to a 30s cap on its own, so a dropped connection needs no supervision                                                |
| Stops retrying on auth failure | `UNAUTHENTICATED` and `PERMISSION_DENIED` disable reconnect rather than hammering the Engine with a bad key                            |

Once an event reaches the durable receiver, delivery is at least once. Fused auth lifecycle payloads include an `id` for idempotency and omit app provenance, provider payloads, scopes, URLs, and token material. Call `nack()` when work fails and `receiver.close()` on shutdown. Python also exposes `SyncFusedWebhooks` when your process has no event loop.

<Note>
  Auth state commits do not wait for lifecycle publication. Until Fused uses a transactional outbox for this boundary, a broker outage immediately after the commit can prevent that lifecycle event from being emitted.
</Note>

## The rules that bite

| Rule                                                                              | What happens if you miss it                                                                 |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| An omitted or empty `webhooks` list means **no provider events**                  | Fused auth lifecycle events may still be available for a selected connected OAuth/OIDC path |
| `webhook_attachment` is required as soon as any service selects provider webhooks | Rejected at plan time, locally and by the Engine                                            |
| One attachment per SDK                                                            | A list is not supported yet                                                                 |
| The named config must exist **and** register every service selecting webhooks     | Rejected with a named error at plan and again at apply                                      |

That last one is checked by the Engine rather than the CLI, so a name that was never applied passes `validate` and fails at plan.

`webhooks_select_all: true` takes every event a service offers. It is independent of `select_all` for operations — you can take all events and only some operations, or the reverse.

Two registrations for the same service and event never cross-deliver: an SDK only receives from the config it attached to.

## Verification headers

Where a service's imported contract declares `verification_headers`, treat that list as the complete reviewed header set. Source order and spelling are preserved; Fused drops blank and case-insensitive duplicate names.

Only when a `signature_header` list is absent does OpenAPI import infer one from required header parameters on webhook operations. Do not add guessed provider headers.

## Flags

### `webhook plan`

| Flag            | What it does                                                          | Example                                                  |
| --------------- | --------------------------------------------------------------------- | -------------------------------------------------------- |
| `--json`        | Prints the plan result and notifications instead of writing a receipt | `fused-cli webhook plan --no-input --json`               |
| `--owner-team`  | Sets the owning team; defaults to you                                 | `fused-cli webhook plan --owner-team payments`           |
| `--receipt-out` | Writes the receipt to a chosen path                                   | `fused-cli webhook plan --receipt-out ./ci/wh.plan.json` |

### `webhook apply`

| Flag        | What it does                    | Example                                               |
| ----------- | ------------------------------- | ----------------------------------------------------- |
| `--plan-id` | Applies one exact remote plan   | `fused-cli webhook apply --plan-id pln_44c…`          |
| `--receipt` | Applies from a specific receipt | `fused-cli webhook apply --receipt ./ci/wh.plan.json` |

## Permissions

| Action               | Needs                                                                  |
| -------------------- | ---------------------------------------------------------------------- |
| Plan a new bundle    | `app.create` and `service.read`                                        |
| Plan an update       | `app.manage` and `service.read`                                        |
| Any secret reference | `bucket.read` for each named bucket                                    |
| Apply                | `service.consume` per service, plus `bucket.use` per referenced bucket |

A registration with no secret reference carries no bucket permission requirement at all.

<Card title="Store the signing secret" icon="key" href="/docs/bucket/store-credentials">
  The reference resolves at verification time. The secret must be in the bucket before then.
</Card>
