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

# Receive webhooks

> Register signed webhook endpoints, verify Shodai deliveries, test receivers, and choose which webhook event families your integration receives.

For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt).

Webhooks are the signed delivery mechanism Shodai uses to send compact events to your backend. A webhook subscription belongs to an API principal, points to one HTTPS receiver URL, and can receive one or more subscribed event families.

Use this page for subscription setup, verification, retries, testing, and shared delivery behavior. Use [Agreement activity webhooks](/webhooks/agreement-activity-webhooks) for agreement lifecycle reconciliation, and [Notification webhooks](/webhooks/notification-webhooks) for notification-rule deliveries.

## Webhook taxonomy

| Concept                     | Event type                         | Use it for                                                                                                          |
| --------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Webhooks                    | N/A                                | Shared subscription, signing, delivery, retry, test, and filtering infrastructure.                                  |
| Agreement activity webhooks | `agreement.transitioned`           | Mirroring or reconciling agreement lifecycle changes.                                                               |
| Notification webhooks       | `agreement.notification.triggered` | Receiving rule-fired notification content after Shodai evaluates notification templates.                            |
| Test deliveries             | `webhook.test`                     | Testing a receiver with a real signed POST. This is a delivery payload type, not a subscribable `eventTypes` value. |

`agreement.transitioned` and `agreement.notification.triggered` are the only subscribable event types. A single subscription can receive either or both.

## How webhooks are managed

You can manage webhook subscriptions in the Developer Portal or through the Agreements API and TypeScript SDK.

Both paths manage subscriptions for the same API principal model. The API-key path uses `/v0/webhooks`. The Developer Portal resolves your signed-in account to the self-serve API principal and manages that principal's subscriptions. A webhook belongs to the principal. If a webhook is created with an API key, `createdByApiKeyId` is audit context, not the ownership boundary.

Webhook management requires `webhooks.read` and `webhooks.write` access for the API principal. For key setup and `401` or `403` troubleshooting, see [Authentication](/authentication).

## Before you start

You need:

* a Shodai API key or Developer Portal access
* a public HTTPS endpoint that can receive `POST` requests
* server-side access to the exact raw request body before JSON parsing
* a secure place to store the webhook signing secret returned at creation

## Register a webhook

Use the TypeScript client when you are already using the SDK:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { ApiClient } from '@cns-labs/agreements-api-client';

const client = new ApiClient({
  environment: 'testnet',
  apiKey: process.env.API_KEY,
});

const created = await client.createWebhook({
  url: 'https://example.com/shodai/webhooks',
  eventTypes: ['agreement.transitioned', 'agreement.notification.triggered'],
  filters: {
    templateIds: ['did:template:service-retainer-v0-1'],
  },
});

console.log(created.id);
console.log(created.secret);
```

The create response includes `secret` once. Store it immediately in your application's secret manager.

<Warning>
  Shodai does not include the signing secret when you list, get, update, disable, or test webhook subscriptions. If the secret was not stored from the create response, create a new webhook subscription.
</Warning>

For raw HTTP integrations, call `POST /v0/webhooks` with the same `url`, optional `eventTypes`, and optional `filters` fields. Use the generated Webhooks pages in the API Reference group for exact request and response schemas.

## Choose event types

On create, omitted, `null`, or empty `eventTypes` defaults the subscription to `agreement.transitioned`.

On update, omitted `eventTypes` leaves the existing event types unchanged. An explicit empty or `null` `eventTypes` value resets the subscription to `agreement.transitioned`.

Subscribe to `agreement.notification.triggered` only when your integration also attaches `external_webhook` notification rules to agreements. A subscription alone does not create notification-triggered events. See [Notification webhooks](/webhooks/notification-webhooks).

## Verify deliveries

Prefer the SDK receiver helper instead of hand-rolling signature verification:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import {
  constructWebhookEvent,
  WebhookVerificationError,
} from '@cns-labs/agreements-api-client/webhooks';

export async function receiveWebhook(request: Request) {
  const rawBody = await request.text();

  try {
    const event = constructWebhookEvent(
      rawBody,
      request.headers,
      process.env.SHODAI_WEBHOOK_SECRET!,
    );

    await storeWebhookEvent(event);
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) {
      return Response.json({ error: 'invalid_webhook' }, { status: 400 });
    }
    throw error;
  }
}
```

Verify the raw request body before trusting parsed JSON. The helper checks:

* `x-shodai-webhook-id`
* `x-shodai-webhook-timestamp`
* `x-shodai-webhook-signature`
* timestamp tolerance, which defaults to `300` seconds
* event envelope shape, supported `apiVersion`, supported event type, and header/body event ID match

The signature header uses `sha256=<hex>`. The signed message is `${timestamp}.${rawBody}`, where `timestamp` is the `x-shodai-webhook-timestamp` header and `rawBody` is the exact request body bytes. Shodai signs the message with HMAC-SHA256 and the subscription secret.

## Respond to deliveries

Return a `2xx` response only after signature verification and durable receipt. A common pattern is:

1. verify the signature with `constructWebhookEvent(...)`
2. insert the event into durable storage keyed by event `id`
3. treat duplicate event IDs as already received
4. return `2xx`
5. process business side effects asynchronously

Your application owns deduplication, queueing, business side effects, logging, and observability.

## Understand the event envelope

Every webhook delivery uses the same event envelope:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "id": "evt_123",
  "type": "agreement.transitioned",
  "apiVersion": "2026-06-01",
  "createdAt": "2026-06-02T18:00:00.000Z",
  "data": {}
}
```

The `data` shape depends on `type`.

* `agreement.transitioned` contains compact transition data. See [Agreement activity webhooks](/webhooks/agreement-activity-webhooks).
* `agreement.notification.triggered` contains interpolated notification content for one resolved rule-recipient pair. See [Notification webhooks](/webhooks/notification-webhooks).
* `webhook.test` contains an empty `data` object.

## Filter deliveries

Filters apply before delivery. Filter values are exact string matches. Multiple values inside one filter field act like OR. Different filter fields combine like AND. Empty or omitted filters mean all subscribed events for the API principal.

| Filter         | `agreement.transitioned`                | `agreement.notification.triggered`                                                     |
| -------------- | --------------------------------------- | -------------------------------------------------------------------------------------- |
| `agreementIds` | Matches `data.agreementId`.             | Matches `data.agreementId`.                                                            |
| `templateIds`  | Matches `data.templateId`.              | Matches `data.templateId`.                                                             |
| `inputIds`     | Matches `data.inputId`.                 | Matches `data.transition.inputId` when the notification payload has transition data.   |
| `fromStates`   | Matches `data.fromState`.               | Matches `data.transition.fromState` when the notification payload has transition data. |
| `toStates`     | Matches `data.toState`.                 | Matches `data.transition.toState` when the notification payload has transition data.   |
| `ruleIds`      | Not used for agreement activity events. | Matches `data.ruleId`.                                                                 |

Temporal notification webhooks do not include a `transition` block. They do not match `inputIds`, `fromStates`, or `toStates` filters.

## Test a webhook

Use `client.testWebhook(...)` or `POST /v0/webhooks/{id}/test` to send a real signed `webhook.test` delivery to the subscription URL:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const result = await client.testWebhook(created.id);

console.log(result.ok);
console.log(result.deliveryId);
console.log(result.status);
```

The test response reports the immediate delivery attempt. `ok` is `true` only when that attempt succeeds. Failed tests can include `status`, `responseStatus`, and `error`. Disabled subscriptions cannot be tested.

## Disable a webhook

Use `client.deleteWebhook(webhookId)` or `DELETE /v0/webhooks/{id}` to disable a subscription:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const disabled = await client.deleteWebhook(created.id);

console.log(disabled.status);
```

The route disables the subscription and returns the subscription with `status: "disabled"`. It does not hard-delete the subscription record.

## Handle failures and retries

Delivery status depends on your receiver response:

| Receiver result                | Shodai behavior                  |
| ------------------------------ | -------------------------------- |
| `2xx`                          | Marks the delivery as succeeded. |
| Network failure or no response | Retries when attempts remain.    |
| `5xx`                          | Retries when attempts remain.    |
| `4xx`                          | Marks the delivery as failed.    |

By default, Shodai makes up to `5` total delivery attempts. Retry scheduling is checked about every `60` seconds. Backoff starts at `60` seconds and doubles, capped at `60` minutes. Redirects are not followed.

If a subscription is disabled or missing before a retry, the pending delivery is marked failed.

## Use public HTTPS URLs

Webhook URLs must be valid HTTP or HTTPS URLs and must not include credentials. In normal hosted usage, Shodai requires HTTPS and rejects localhost or private-network targets, including hosts that resolve to private addresses.

For local development, expose your local receiver through a public HTTPS tunnel and register the tunnel URL.

## Troubleshooting

| Problem                         | What to check                                                                                                                                          |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Missing secret                  | Use the secret stored from the create response. If it was not stored, create a new webhook subscription.                                               |
| Signature mismatch              | Verify against the exact raw body and the correct subscription secret before JSON parsing.                                                             |
| Timestamp rejected              | Check receiver clock drift or pass an explicit tolerance to `constructWebhookEvent(...)` if your receiver needs one.                                   |
| No activity events received     | Check subscription status, filters, event types, API principal ownership, and whether the agreement belongs to the same principal.                     |
| No notification events received | Check subscription status, event types, deploy-time `notificationTemplate`, `external_webhook` rule channels, rule triggers, and recipient resolution. |
| Test fails                      | Inspect `responseStatus` and `error`. `4xx` is terminal; network failures and `5xx` can retry when attempts remain.                                    |
| Local URL rejected              | Use a public HTTPS tunnel for local development.                                                                                                       |

## Related pages

* [Agreement activity webhooks](/webhooks/agreement-activity-webhooks)
* [Notification webhooks](/webhooks/notification-webhooks)
* [TypeScript client](/sdks/typescript-client)
* [Authentication](/authentication)
* Use the Webhooks pages in the API Reference group for generated endpoint schemas.
