# Events And Webhooks

**Status:** Normative notification-delivery profile for Supply-Y Protocol 1.0

Supply-Y notifications tell an Agent that Package metadata changed. They never carry the encrypted Package body or customer plaintext. Webhooks provide fast delivery; the ordered notification API provides recovery. A customer should implement both against one durable local inbox.

## The short version

1. Supply-Y records a lifecycle event durably.
2. Supply-Y sends a signed webhook containing metadata only.
3. The Agent verifies the signature and body digest, then writes the event to its own durable inbox.
4. The Agent returns `204` even when the same event was already stored.
5. The Agent processes inbox events in `sequence` order.
6. If a sequence gap appears, the Agent calls `GET /v1/notifications` to fill it.

`204` means **durably stored by the receiver**. It does not mean the Package was opened, accepted by a human or used in a business decision. Those facts require their own signed Package receipts.

## Three identifiers, three jobs

| Field | Stable across | Purpose |
| --- | --- | --- |
| `event_id` | Automatic retries and manual replay | Identifies one immutable lifecycle fact; this is the receiver's primary deduplication key |
| `delivery_id` | Automatic retries to one endpoint | Identifies one delivery task; `attempt` increases while this ID stays fixed |
| `notification_id` | Webhook and polling views | Identifies the receiving organization's retained notification record and acknowledgement target |

An operator-triggered replay creates a new `delivery_id` but keeps the same `event_id`. The receiver therefore cannot mistake replay for a new business event.

## Notification envelope

Every event has a monotonic `sequence` within the receiving organization's stream. It also binds the lifecycle fact to the Package identity, transport mode and original encrypted-content digest.

```json
{
  "notification_id": "notification_material_constraint_001",
  "event_id": "event_material_constraint_accepted_001",
  "event_type": "package.accepted",
  "protocol_version": "supply-y/1.0",
  "sequence": 41,
  "occurred_at": "2026-07-04T00:00:00Z",
  "organization_id": "org_tier2_connector",
  "package_id": "pkg_material_constraint_001",
  "thread_id": "thread_material_constraint_01",
  "state": "accepted",
  "transport_mode": "native",
  "content_digest": "sha-256=:25ek/LF8FWyZGJbg+9ecD3Q5czs7LIvBVU4YtWy/Sd8=:",
  "correlation_id": "corr_material_constraint_001"
}
```

`content_digest` refers to the Package content, not the webhook body. The HTTP `Content-Digest` header separately protects the exact webhook body bytes.

## Signed webhook profile

Webhook requests use TLS and an [RFC 9421](https://www.rfc-editor.org/rfc/rfc9421.html) HTTP Message Signature. The body uses the [RFC 9530](https://www.rfc-editor.org/rfc/rfc9530.html) `Content-Digest` field with SHA-256.

Supply-Y uses:

- signature label `sig1`;
- algorithm `ecdsa-p256-sha256`;
- raw 64-byte P-256 `r || s` signature output;
- `created`, `expires`, `keyid`, `alg` and `tag="supply-y-webhook"` signature parameters;
- a maximum five-minute signature lifetime;
- a pinned Supply-Y notification-signing public key from the verified key directory.

Before endpoint activation, the Agent calls `GET /v1/directory/notification-keys`, pins every eligible active or retiring public key, and records the returned `directory_version`. `POST /v1/webhook-endpoints` returns the exact `signing_key_id` selected for that endpoint. The receiver rejects a webhook whose `keyid` is absent, not yet valid, retired for new delivery or revoked according to the key lifecycle rules.

During planned rotation, the directory publishes the replacement key before Supply-Y begins signing with it. Agents refresh the directory before `valid_until` and keep the old public key only for historical verification. Supply-Y never asks a customer to upload or share a private key for webhook verification.

The signature must cover, in this order:

```text
"@method"
"@authority"
"@path"
"content-digest"
"content-type"
"supply-y-delivery-id"
"supply-y-event-id"
"supply-y-delivery-attempt"
```

The receiver must reject the request before storage when a required component is absent, the key is unknown or ineligible, the signature is outside its time window, the signature fails, the body digest fails, or the header event ID differs from the body event ID.

## Durable inbox rule

The webhook handler should do very little synchronously:

1. read the exact request bytes;
2. validate the required signature profile and time window;
3. recompute `Content-Digest` over those bytes;
4. parse and validate the notification schema;
5. confirm header and body identifiers match;
6. insert by unique `event_id` into a local durable inbox;
7. return `204` for both a new insert and an existing identical event.

Skill execution, ERP writes, Package decryption and human workflow happen after this durable boundary. A slow business system therefore does not cause repeated webhook delivery.

## Retry decisions

Automatic delivery has at most seven attempts. The delay shown is measured after the preceding failed attempt.

| Next attempt | Minimum delay |
| --- | ---: |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 1 hour |
| 6 | 6 hours |
| 7 | 24 hours |

Decision rules:

| Result | Supply-Y action |
| --- | --- |
| Any `2xx` | Complete the delivery |
| Network error, `408`, `425`, `429`, or `5xx` | Retry with backoff |
| `429` or `503` with `Retry-After` | Use the later of normal backoff and `Retry-After`, capped at 24 hours |
| `410` | Disable the endpoint |
| Other `4xx` | Record permanent failure; do not repeat a malformed or unauthorized request |
| Retryable failure on attempt 7 | Move the delivery to the dead-letter queue and alert the endpoint owner |

Webhook failure never rolls back Package acceptance and never resends the business Package. Only the metadata notification is retried.

## Ordering and gap recovery

Webhook requests may arrive out of order. An Agent must store a valid out-of-order event, return `204`, and delay ordered processing until the missing sequence arrives.

The Agent keeps a `last_contiguous_sequence` high-water mark. If sequence `43` arrives while the mark is `41`, the Agent stores `43`, then polls from its last cursor. When `42` arrives, it can apply `42` and the already stored `43` in order.

```http
GET /v1/notifications?after={opaque_cursor}&limit=100
```

Polling pages are ordered by ascending `sequence`. The cursor is opaque and must not be constructed by the client. A client persists the returned cursor only after all events on that page are durably stored.

## Acknowledgement

Polling consumers acknowledge durable inbox storage with:

```http
POST /v1/notifications/{notification_id}/ack
Idempotency-Key: idem_ack_stream_001
```

```json
{
  "event_id": "event_stream_001",
  "disposition": "stored",
  "stored_at": "2026-07-04T00:03:30Z",
  "last_contiguous_sequence": 41
}
```

An exact retry returns the original acknowledgement. Reusing the idempotency key with different content returns `409 idempotency_key_reuse`.

## What operators can replay

Operators may replay a failed **notification delivery** from the durable event stream. They must not replay the original Package creation request, create a second Package, change the event body or assign a new event ID. The audit trail records the new delivery ID, replay actor and reason.

## Executable evidence

Run:

```bash
npm run validate:notifications
```

The committed suite passes `24 / 24` checks across:

- OpenAPI event-envelope validation;
- RFC 9530 body integrity;
- RFC 9421 P-256 verification in Node and WebCrypto;
- covered-header, body and stale-signature rejection;
- retry and dead-letter decisions;
- duplicate, replay and out-of-order inbox behavior;
- ordered polling and cursor resume;
- idempotent acknowledgement and conflict rejection.

The exact signed request is published in [`test-vectors/webhook-signature-0.1.json`](/docs/reference#test-vectors). Delivery behavior inputs are published in `conformance/notification-delivery.cases.json`.

---

Canonical HTML: [Events and Webhooks](https://supply-y.net/docs/events)
Agent documentation index: [llms.txt](https://supply-y.net/llms.txt)
