Skip to main content
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".
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:
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.
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.
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:
FieldBehavior
statesStates 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.unitseconds, minutes, hours, days, or weeks.
fireOnceDefaults to true; when true, fires once per state visit.
constraints.maxOccurrencesCaps temporal sends for the rule and agreement.
constraints.cooldownRequires 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 tokenResolution behavior
Variable key, such as customerWalletAddressReads that variable value and resolves it to an email.
*Scans agreement variable values for wallet-address-shaped strings and resolves each one.
@observersUses 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:
{
  "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:
{
  "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:
FilterMatches
agreementIdsdata.agreementId
templateIdsdata.templateId
ruleIdsdata.ruleId
inputIdsdata.transition.inputId when present
fromStatesdata.transition.fromState when present
toStatesdata.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.