> ## 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.

# Notification webhooks

> Use agreement.notification.triggered webhook events when Shodai should evaluate agreement notification rules and call your backend with the resolved notification content.

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

Notification webhooks are `agreement.notification.triggered` events. They use the same webhook subscription, signing, delivery, retry, and filtering system as agreement activity webhooks, but they are created by the notification rules attached to an agreement.

Use notification webhooks when Shodai should evaluate agreement notification rules while your backend owns the final side effect, such as sending email through your own SES account.

## How notification webhooks work

There are two required setup steps:

1. Create a webhook subscription that includes `agreement.notification.triggered`.
2. Deploy an agreement with an agreement-scoped `notificationTemplate` whose rules use `notification.channel: "external_webhook"`.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const webhook = await client.createWebhook({
  url: 'https://example.com/shodai/webhooks',
  eventTypes: ['agreement.transitioned', 'agreement.notification.triggered'],
});
```

A subscription alone does not create notification events. Shodai sends `agreement.notification.triggered` only after an `external_webhook` notification rule resolves at least one recipient.

## Attach notification rules at deploy

Pass `notificationTemplate` to `deployWithPermit`:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
await client.deployWithPermit({
  agreement,
  displayName: 'Consulting Agreement',
  chainId: 59141,
  signer,
  deadline,
  signature,
  notificationTemplate: {
    rules: [
      {
        id: 'deployment-follow-up',
        name: 'Deployment follow-up',
        trigger: { type: 'onTransition', inputs: ['__deploy'] },
        recipients: ['@observers'],
        notification: {
          channel: 'external_webhook',
          subject: 'Agreement deployed',
          body: 'Agreement ${agreementId} is ready for review.',
        },
      },
    ],
  },
});
```

After a successful deploy, the External API scopes the template to the authenticated API principal, deployed agreement ID, and agreement template ID. If the deploy response includes a post-deploy state, Shodai replays the deploy transition once so `__deploy` notification rules can evaluate.

If the post-deploy state is not available, deploy-triggered notification evaluation is skipped for that deployment. Future agreement transitions can still evaluate matching notification rules.

## onTransition rules

`onTransition` rules evaluate when an agreement transition event reaches the notification service.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const notificationTemplate = {
  rules: [
    {
      id: 'payment-received',
      name: 'Payment received',
      trigger: {
        type: 'onTransition',
        from: ['AWAITING_PAYMENT'],
        to: ['WORK_IN_PROGRESS'],
        inputs: ['submitInitialPaymentProof'],
      },
      recipients: ['clientWalletAddress', '@observers'],
      notification: {
        channel: 'external_webhook',
        subject: 'Payment received for ${agreementName}',
        title: 'Work can begin',
        body: '${agreementName} moved to ${toState}.',
        ctaLabel: 'View agreement',
      },
    },
  ],
};
```

For `onTransition` triggers:

* `from`, `to`, and `inputs` are optional arrays
* omitted arrays act as wildcards
* `inputs: ["__deploy"]` matches deploy notification replay when the deploy transition replay occurs
* `fromState`, `toState`, and `input` are available for interpolation
* the webhook payload includes a `transition` block

## Temporal rules

Temporal rules evaluate while an agreement is in one of the configured states.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const notificationTemplate = {
  rules: [
    {
      id: 'invoice-due-soon',
      name: 'Invoice due soon',
      trigger: {
        type: 'temporal',
        states: ['AWAITING_PAYMENT'],
        condition: {
          type: 'deadlineApproaching',
          variable: 'paymentDueAt',
          threshold: { value: 2, unit: 'days' },
        },
        fireOnce: true,
      },
      recipients: ['customerWalletAddress'],
      notification: {
        channel: 'external_webhook',
        subject: 'Payment due soon for ${agreementName}',
        body: 'Payment is due soon for agreement ${agreementId}.',
      },
      constraints: {
        maxOccurrences: 1,
        cooldown: { value: 1, unit: 'days' },
      },
    },
  ],
};
```

Temporal triggers support:

| Field                                    | Behavior                                                                        |
| ---------------------------------------- | ------------------------------------------------------------------------------- |
| `states`                                 | States where the temporal rule is active.                                       |
| `condition.type: "deadlineApproaching"`  | Fires when a date variable is within the threshold and still in the future.     |
| `condition.type: "stateAge"`             | Fires when time in the current state reaches the threshold.                     |
| `condition.type: "elapsedSinceVariable"` | Fires when elapsed time since a date variable reaches the threshold.            |
| `threshold.unit`                         | `seconds`, `minutes`, `hours`, `days`, or `weeks`.                              |
| `fireOnce`                               | Defaults to `true`; when true, fires once per state visit.                      |
| `constraints.maxOccurrences`             | Caps temporal sends for the rule and agreement.                                 |
| `constraints.cooldown`                   | Requires a minimum time between temporal sends for the same rule and agreement. |

`checkInterval` is accepted and stored on temporal triggers, but current hosted evaluation uses a system-wide temporal sweep. Do not rely on `checkInterval` as a per-rule schedule or cadence guarantee.

Temporal notification payloads do not include a `transition` block.

## Recipient resolution

Each notification rule has `recipients`. Shodai resolves those tokens before emitting notification webhook events.

| Recipient token                               | Resolution behavior                                                                      |
| --------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Variable key, such as `customerWalletAddress` | Reads that variable value and resolves it to an email.                                   |
| `*`                                           | Scans agreement variable values for wallet-address-shaped strings and resolves each one. |
| `@observers`                                  | Uses observer email addresses on the agreement.                                          |

Resolved raw values are converted to email addresses in this order:

1. participant email matching the wallet address or matching the variable key
2. companion email variable, such as `customerEmail` for `customerWalletAddress`
3. direct email value, when the raw value itself contains an email address

Literal email strings are not collected when placed directly in `recipients`. To notify a literal email address, pass it as an observer or as the value of a recipient variable.

Resolved email addresses are deduped case-insensitively. Unresolved recipients are skipped and logged.

## Content interpolation

`subject`, `title`, and `body` support `${variable}` interpolation.

For `onTransition` notifications, interpolation context includes the agreement variables plus:

* `agreementName`
* `agreementId`
* `fromState`
* `toState`
* `input`

For temporal notifications, interpolation context includes the stored agreement variable snapshot plus:

* `agreementName`
* `agreementId`

Unknown variables remain in the rendered string as `${variableName}`.

## Payload shape

An `onTransition` notification webhook includes notification content and transition context:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "id": "evt_456",
  "type": "agreement.notification.triggered",
  "apiVersion": "2026-06-01",
  "createdAt": "2026-06-02T18:00:00.000Z",
  "data": {
    "agreementId": "agr_123",
    "agreementName": "Advisory Retainer",
    "templateId": "did:template:service-retainer-v0-1",
    "notificationTemplateId": "external:principal-1:agr_123",
    "ruleId": "payment-received",
    "triggerType": "onTransition",
    "recipient": "client@example.com",
    "notification": {
      "subject": "Payment received for Advisory Retainer",
      "title": "Work can begin",
      "body": "Advisory Retainer moved to WORK_IN_PROGRESS.",
      "ctaLabel": "View agreement"
    },
    "variables": {
      "agreementName": "Advisory Retainer",
      "agreementId": "agr_123",
      "fromState": "AWAITING_PAYMENT",
      "toState": "WORK_IN_PROGRESS",
      "input": "submitInitialPaymentProof"
    },
    "transition": {
      "fromState": "AWAITING_PAYMENT",
      "toState": "WORK_IN_PROGRESS",
      "inputId": "submitInitialPaymentProof",
      "occurredAt": "2026-06-02T17:59:58.000Z"
    }
  }
}
```

A temporal notification webhook omits `transition`:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "id": "evt_789",
  "type": "agreement.notification.triggered",
  "apiVersion": "2026-06-01",
  "createdAt": "2026-06-04T18:00:00.000Z",
  "data": {
    "agreementId": "agr_123",
    "agreementName": "Advisory Retainer",
    "templateId": "did:template:service-retainer-v0-1",
    "notificationTemplateId": "external:principal-1:agr_123",
    "ruleId": "invoice-due-soon",
    "triggerType": "temporal",
    "recipient": "client@example.com",
    "notification": {
      "subject": "Payment due soon for Advisory Retainer",
      "body": "Payment is due soon for agreement agr_123."
    },
    "variables": {
      "agreementName": "Advisory Retainer",
      "agreementId": "agr_123",
      "paymentDueAt": "2026-06-06T18:00:00.000Z"
    }
  }
}
```

One resolved rule-recipient pair produces one `agreement.notification.triggered` event. That event is delivered as signed POST requests to each active webhook subscription that matches the event type and filters.

## Filter notification events

Notification webhooks support these filters:

| Filter         | Matches                                  |
| -------------- | ---------------------------------------- |
| `agreementIds` | `data.agreementId`                       |
| `templateIds`  | `data.templateId`                        |
| `ruleIds`      | `data.ruleId`                            |
| `inputIds`     | `data.transition.inputId` when present   |
| `fromStates`   | `data.transition.fromState` when present |
| `toStates`     | `data.transition.toState` when present   |

Temporal notification payloads do not include `transition`, so they do not match `inputIds`, `fromStates`, or `toStates` filters.

## Handle notification events

Your receiver handles notification webhooks like any other Shodai webhook:

1. verify the signature with the subscription secret
2. store and dedupe by event `id`
3. return `2xx` after durable receipt
4. perform your final side effect asynchronously

For example, the Shodai Reference App converts vendored notification templates to `external_webhook`, lets hosted Shodai services evaluate transition and temporal rules, receives `agreement.notification.triggered`, and sends final email through its own AWS SES configuration.

## Related pages

* [Receive webhooks](/webhooks/receive-webhooks)
* [Agreement activity webhooks](/webhooks/agreement-activity-webhooks)
* [Shodai Reference App](/examples/reference-app)
* [TypeScript client](/sdks/typescript-client)
