Keeping in sync
Partner callbacks are informational, sparse, and delivered at least once. They are not commands and must not be used as the sole source of case or finality state.
Recover with the events cursor
Poll GET /api/cases/{id}/events with a credential authorized for the case. OAuth requires cases:read. The response’s data contains schemaVersion: case-event-page-v1, caseId, events, nextCursor, and hasMore. Each event contains schemaVersion: case-event-v1, id, cursor, type, and occurredAt.
| Query parameter | Required | Meaning |
|---|---|---|
after | No | The last durably processed cursor from this case and authorized stream. Omit on the first read. |
limit | No | Integer from 1 to 100; defaults to 50. |
Process a page, persist nextCursor with your work, and pass it as after on the next read. Continue while hasMore is true. An empty page retains the input cursor, or returns null on an initial empty read. Cursors are opaque; do not sort or increment them. An unavailable cursor returns 422 invalid_event_cursor; refresh the case and restart the stream with durable event-ID deduplication.
New audit-derived webhook deliveries include caseEvent.id and caseEvent.cursor. Use the ID to reconcile a callback with a polled event. Do not advance your polling checkpoint to an out-of-order callback: drain the stream from the last committed checkpoint first. The callback’s eventId remains its delivery-deduplication identity. Older callbacks may have no caseEvent.
Events expose only the caller’s permitted record. Fetch /api/cases/{id} after a notification to read current state; polling does not grant access to sealed evidence.
Consumer algorithm
- Verify the Ed25519 signature using the published key set.
- Deduplicate using body/header
eventId, notdeliveryId. - Check
x-pc-event-timestampagainst the replay window. - Return a successful response promptly.
- Fetch the authoritative authenticated case, decision, or Award resource.
- Verify Award signatures before initiating any separately authorized settlement action.
Settlement events are intentionally distinct: decision_served means a signed decision is available but does not prove execution; settlement_completed, settlement_failed, settlement_blocked, and settlement_manual_action_required report the execution boundary outcome. Never treat decision service as settlement completion.
procedural_message_received is also sparse: fetch GET /api/cases/{id}/messages?channel=partner_notice for the authoritative immutable message. Callback bodies never carry message text or evidence content.
Every callback and signed evidence-export notification carries exactly these seven lowercase logical headers:
| Header | Required value |
|---|---|
content-digest | sha-256=: + base64 SHA-256 of the exact request bytes + : |
x-pc-event-id | Stable safe identifier (A-Z, a-z, digits, ., _, :, -) |
x-pc-event-timestamp | Canonical UTC ISO 8601 instant; it must round-trip through toISOString() unchanged |
x-pc-key-id | Safe identifier of the Ed25519 verification key |
x-pc-schema-version | Exact body schema constant: case-event-v2 for callbacks or evidence-export-notification-v1 for export notifications |
x-pc-signature-version | Constant pc-webhook-ed25519-v1 |
x-pc-signature | ed25519=: + base64 64-byte Ed25519 signature + : |
Header names are case-insensitive on the wire. Canonicalization uses the uppercase method, exact path including query, and this newline-delimited input with no trailing newline:
@method: POST
@target-path: /exact/path?exact=query
x-pc-event-id: <event-id>
x-pc-event-timestamp: <canonical-timestamp>
x-pc-key-id: <key-id>
x-pc-schema-version: <schema-version>
content-digest: <digest-of-exact-body-bytes>The signature-version header selects this canonicalization contract; it and the signature itself are not lines in the signed input. The signature binds the HTTP method, exact path and query, event ID, event timestamp, key ID, schema version, and exact-body SHA-256 digest. Retries retain eventId, deliveryId, sequence, and occurrence time but receive a fresh signing timestamp and signature. Operator replay retains eventId and sequence while creating a new deliveryId.
Ordinary REST mutations use Idempotency-Key. Signed evidence-export notifications are the exception: they omit that header and deduplicate by the authenticated exporter plus signed event identity, while rejecting a reused identity with conflicting content.
Ordering and retries
sequence is monotonic per partner when an event enters the durable outbox. Delivery completion can occur out of order because endpoints, retries, and overlap deliveries are independent. Consumers should persist the highest observed sequence, tolerate gaps, and fetch authoritative resources; they must not delay acknowledgement waiting for a missing sequence.
Retryable transport failures use exponential backoff and enter dead_letter after eight failed attempts. Inspect with GET /api/webhooks/deliveries, then use the idempotent operator replay endpoint after correcting the receiver. Automatic and operator deliveries are both at least once.
Rotation overlap
POST /api/webhooks/endpoints can replace the active endpoint with a 60–86400 second overlap. During overlap, both active and retiring endpoints receive the same event ID and sequence with different delivery IDs. Signing-key rotation likewise publishes one active and one retiring public key; retain the retiring key through its expiry.
Endpoint registration resolves the hostname and rejects loopback, private, link-local, metadata, failed-DNS, and mixed public/private answers. Delivery resolves again and pins the vetted addresses, refuses redirects, and therefore fails closed if DNS changes. PARTY_WEBHOOKS_ALLOW_PRIVATE=true is incompatible with multi-tenant Partner API readiness; it is accepted only with the explicit isolated PARTNER_API_V2_SINGLE_TENANT=true profile.
Startup also validates the entire signing key set: exactly one currently valid active Ed25519 private key, matching public/private material, a real sign/verify round trip, and valid overlapping expiry windows for retiring keys.