Request headers and verification
Every delivery also carries
Linktrail-Timestamp, Linktrail-Signature, Linktrail-Delivery-Id and Linktrail-Event-Type with identical values. That compatibility set exists for receivers written against the working name; read either set, and move to the AgentLinkOps names before the dated cutover release removes the old ones (DP-0029-T05, after pilot customers are notified). The signing procedure is the same for both.
Compute HMAC-SHA-256 using the full whsec_ secret over the timestamp, a literal dot, and the exact raw UTF-8 body. Compare hex signatures in constant time. Reject invalid timestamps or timestamps more than 300 seconds away from the receiver clock. During rotation, accept a match against either active secret and any offered signature.
Persist the raw notification or enqueue it durably before answering 2xx. Deduplicate work by AgentLinkOps-Delivery-Id in durable storage. Do not mark a delivery processed before its work has been durably accepted. The event ID supplies a separate identity when reconciling with pulled feed records.
Per-event payload
feed is events or target_events. subject_id identifies the watch or destination. transition is the resulting state; previous is the prior state. State, uncertainty, checked time, observation identifier and evidence reference may be null when absent. Evidence references contain no publisher page bytes. Read evidence with your own authenticated credential; expired bytes return EVIDENCE_EXPIRED.
resume.feed identifies the independent feed. resume.after_sequence gives the sequence position for recovery through the named endpoint. Maintain your own durable checkpoint from the authenticated feed so a delivered notification cannot cause you to skip earlier unseen events.
Event types
Confirmed absence/unavailability requires repeated conclusive evidence according to the relevant reducer; a blocked or incomplete check remains uncertain. Endpoint event-type filters advance their cursor across filtered-out types. A syntactically accepted type with no emitter matches no events.
Digest payload
Digest mode summarizes the events feed over daily or weekly windows. A payload contains:
Each subjects row has subject_id, project_id, previous, state, uncertain, events, types, first_seen_at, last_seen_at, checked_at and evidence_key. A subject that changes repeatedly appears once with its final state and its event count. The read limit and subject limit are separate; inspect both flags. Quiet windows are delivered with computed:true and zero totals. Digest notifications use the same signature, delivery ID and retry mechanism.
Retention gap payload
Dispatch onkind before treating a message as a normal event. When retention overtakes an endpoint cursor, a kind:"history_gap" notification has type webhook.history_gap and code CURSOR_EXPIRED. It includes id, feed, workspace_id, project_id, created_at, computed:false, totals:null and resync_required:true.
history_gap contains after_sequence, through_sequence, scope:“workspace_feed”, reason:“history_unavailable” and affected_project_events:“unknown”. snapshot_endpoint names the current-state read; resume_cursor is the supplied opaque resume token. resume carries feed, after_sequence and endpoint. A digest gap also carries delivery_mode:“digest” and window. Preserve the gap explicitly. A current snapshot cannot recover expired history.
The local receive command currently accepts per-event identity only. Handle digest and history-gap notifications in your receiver and use the authenticated feed/snapshot endpoints for recovery.
Retries and recovery
A 2xx response completes a delivery. HTTP 410 disables the endpoint immediately with receiver_reported_gone. Other failures retry with a ten-second send timeout and exponential backoffmin(3600, 15 * 2^min(attempt,8)) seconds. Actual delivery ticks can make retries later than that minimum. Twenty consecutive failures disable the endpoint; a successful delivery resets the count. Oversized payloads above 256 KiB fail terminally.
Read list_webhook_deliveries to inspect failures. Setting an endpoint active resumes pending work, but does not resend terminal failed rows. Recover missing notifications through list_link_events and list_target_events, using separate cursors. Terminal delivery rows are retained for 30 days after becoming terminal; pending rows are not pruned by that terminal-history policy.
Rotation and endpoint changes
Call rotate_webhook_secret with retire:false, save the once-returned secret, and deploy receiver support for both secrets. After verifying new-secret delivery, call it with retire:true to retire the older secret. A second rotation is refused while a rotation is open. An endpoint URL is not an editable field. Create and verify a replacement endpoint, then disable or delete the old endpoint. Deduplicate notifications while both operate. Review HTTP route differences if using resource aliases.This reference follows the source of the deployed release. Delivery still depends on an active endpoint and enabled delivery in your hosted workspace; check capability status before depending on it.
Downloadable notification schemas
These examples come from the actual payload builders with fixture records. They are not live deliveries. Inspect kind and feed before reading a payload.event
Download event schemav(number, required): must equal 1feed(string, required): must equal “events”type(string, required): values: “placement_acquired”, “placement_recovered”, “placement_changed”, “placement_lost”, “source_unavailable”, “watch.checked”, “watch.state_changed”, “watch.check_quality_changed”id(string, required).sequence(integer, required): minimum: 1workspace_id(string, required): Identifier returned by the related operation.project_id(string, required): Identifier returned by the related operation.subject_id(string, required): Identifier returned by the related operation.created_at(string, required): pattern: ”^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$”transition(string / null, required).previous(string / null, required).uncertain(boolean / null, required).checked_at(string / null, required).observation_id(string / null, required): Identifier returned by the related operation.evidence_key(string / null, required).resume(object, required): additional fields rejectedfeed(string, required): must equal “target_events”type(string, required): values: “target_healthy”, “target_recovered”, “target_unavailable”, “target.checked”, “target.state_changed”, “target.check_quality_changed”evidence_key(null, required).feed(string, required): must equal “citation_events”type(string, required): values: “citation.declined”, “citation.grown”data(object, required): additional fields rejected
All fields and nested objects
All fields and nested objects
digest
Download digest schemav(number, required): must equal 1kind(string, required): must equal “digest”workspace_id(string, required): Identifier returned by the related operation.window(object, required): additional fields rejectedcomputed(boolean, required): must equal truetotals(object, required): additional fields rejectedsubjects(array, required): maxItems: 200truncated(boolean, required).events_truncated(boolean, required).resume(object, required): additional fields rejected
All fields and nested objects
All fields and nested objects
historyGap
Download historyGap schemav(number, required): must equal 1kind(string, required): must equal “history_gap”type(string, required): must equal “webhook.history_gap”code(string, required): must equal “CURSOR_EXPIRED”id(string, required).feed(string, required): must equal “events”workspace_id(string, required): Identifier returned by the related operation.project_id(string / null, required): Identifier returned by the related operation.created_at(string, required): pattern: ”^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$”computed(boolean, required): must equal falsetotals(null, required).resync_required(boolean, required): must equal truehistory_gap(object, required): additional fields rejectedsnapshot_endpoint(string, required): pattern: ”^/v1/exports/watches(?:\?projectId=[^#]*)?$”resume_cursor(string, required): pattern: ”^[A-Za-z0-9_-]+$”resume(object, required): additional fields rejecteddelivery_mode(string, optional): must equal “digest”window(object, optional): additional fields rejectedfeed(string, required): must equal “target_events”snapshot_endpoint(string, required): pattern: ”^/v1/targets(?:\?projectId=[^#]*)?$”feed(string, required): must equal “citation_events”snapshot_endpoint(string, required): pattern: ”^/v1/citation-epochs(?:\?projectId=[^#]*)?$“
All fields and nested objects
All fields and nested objects