Docs · reference
Webhook payload schemas — Driftlock's outbound events, field by field.
Every event Driftlock dispatches to an external receiver lands under one of four envelopes. This page names the shared HMAC signature scheme, prints the full JSON shape for each event, and walks each field with type / required flag / source — then hands the receiver three commands to reproduce the signature against the printed sample payload.
Event schemas
Four events, four envelopes
triage.issue.created · pr.signed · sandbox.failed · dependency.advisory.detected — each with trigger, headers, payload, and field-by-field table.
Field dictionary
Type · R/O · source
The table iterates the typed source so a reviewer naming a field lands on the exact type, the required vs. optional marker, the description, and the originating component that recorded it.
Verification flow
Three commands a receiver runs
A shell one-liner, a Node snippet, and a Python one-liner — each reads the raw body on stdin and prints MATCH or MISMATCH: +N s skew.
reference · outbound · HMAC-SHA256 · t=<unix>,v1=<hex> · 300s tolerance
HMAC signature scheme
One scheme, four events — verify the header against the raw body, every time.
Every outbound event below shares the same signing scheme. Read this block once before the per-event tables — the same algorithm, the same header, and the same tolerance window apply to all four events.
Field · value
Notes
algorithm
HMAC-SHA256
request header
X-Driftlock-Signature
delivery header
X-Driftlock-Delivery
Same UUID v7 across every retry — dedupe on this, not on the JSON id.
signing string
<unix_seconds>.<raw_request_body>
Concatenate the unix seconds and a literal `.` separator with the unmodified request body bytes — never the parsed JSON.
header value
t=<unix_seconds>,v1=<hex_sha256>
Stripe-style envelope — `t` is replay-window bound, `v1` is the scheme version (bump on rotation, not on cosmetic changes).
timestamp tolerance
300s
Reject any delivery whose `t` is more than five minutes off receiver clock — replay protection is enforced by the delay window, not by a single nonce.
custody
A per-tenant secret stored in your KMS, rotated on the schedule the platform team sets; Enterprise tier swaps the secret for an HSM-resident key the watcher reads through PKCS#11.
HMAC-SHA256 · t=<unix>,v1=<hex> · 300s · KMS-resident secret
Outbound events
Four envelopes a receiver subscribes to — trigger, headers, payload, fields.
Each Card below walks one event. The trigger sentence names what fires it, the headers block shows the literal request shape, the JSON example is the body a well-formed delivery carries, and the field table iterates every R and O field with type and origin.
Outbound event
triage.issue.created
Trigger
Fired the moment the watcher hands a draft PR off to the approver queue — the handoff that reconciles the alert, the diff, and the proposed fix.
Headers on the wire
X-Driftlock-Delivery: 019012ab-7cde-7f01-9abc-1f4d2c8aef33
X-Driftlock-Signature: t=1762643514,v1=8b6f9e7a2c1d4f5b6e7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9ePayload example
{
"id": "01J9X8K4W3F2Q9P0RCT1EBNXGA",
"event": "triage.issue.created",
"created_at": "2026-08-08T11:42:17Z",
"tenant": "acme-checkout",
"delivery_id": "019012ab-7cde-7f01-9abc-1f4d2c8aef33",
"attempt": 1,
"data": {
"issue": {
"id": "ISS-7421",
"repo": "acme/checkout-svc",
"severity": "high",
"source": "dependabot"
},
"evidence": {
"advisory_id": "GHSA-xxxx-yyyy-zzzz"
},
"draft_pr": {
"pr_url": "https://github.com/acme/checkout-svc/pull/314",
"branch": "driftlock/ISS-7421",
"head_sha": "8f6c1d5b9c2f6c1d5b9c2f6c1d5b9c2f6c1d5b9c",
"status": "draft"
},
"replay": {
"sandbox_run_id": "run-7421-a1b2",
"hermetic_build_hash": "sha256:9c8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b",
"replay_first": true
}
}
}Field-by-field reference
R marks a required field — receivers should reject the delivery when it is absent. O marks an optional field — surface as `null` in your own schema, do not infer a value.
Field · type · R/O
Description · source
id
stringR
ULID identifying this specific event emission.
source · driftlock-egress
event
stringR
The dotted event name (e.g. `triage.issue.created`).
source · driftlock-egress
created_at
stringR
RFC-3339 timestamp the event was emitted at.
source · driftlock-egress
tenant
stringR
Tenant slug the event belongs to.
source · driftlock-egress
delivery_id
stringR
UUID v7 — same across every retry; receivers dedupe on this.
source · driftlock-egress
attempt
integerR
1-indexed delivery attempt. Same delivery_id across retries.
source · driftlock-egress
data.issue.id
stringR
Internal issue id (`ISS-7421`).
source · driftlock-triage
data.issue.repo
stringR
`<org>/<repo>` slug.
source · driftlock-triage
data.issue.severity
stringR
`low` | `medium` | `high` | `critical`.
source · driftlock-triage
data.issue.source
stringR
Originating signal: `dependabot` | `ci` | `on-call`.
source · driftlock-triage
data.evidence
objectR
Discriminated by `data.issue.source` — `advisory_id` for `dependabot`, `alert_id` for `ci`, `log_url` for `on-call`.
source · driftlock-triage
data.draft_pr.pr_url
stringR
URL of the draft PR the watcher is proposing.
source · driftlock-triage
data.draft_pr.branch
stringR
Watcher-owned branch name, prefixed `driftlock/`.
source · driftlock-triage
data.draft_pr.head_sha
stringR
Head commit SHA on the draft PR branch.
source · driftlock-triage
data.draft_pr.status
stringR
Current PR lifecycle state — `draft` | `open` | `merged` | `closed`.
source · driftlock-triage
data.replay.sandbox_run_id
stringR
Id of the sandbox run that produced the diff.
source · driftlock-sandbox
data.replay.hermetic_build_hash
stringR
Hermetic-build hash pinned to the run — byte-stable across reruns.
source · driftlock-sandbox
data.replay.replay_first
booleanR
True when the sandbox replayed the failing test before applying the diff.
source · driftlock-sandbox
Corresponds to the `triage` row in the per-PR audit trail — link the handler id back via `data.replay.sandbox_run_id`.
Outbound event
pr.signed
Trigger
Fired after the cosign signer stamps a Driftlock-attributed commit — KMS-resident key, Sigstore OIDC, Rekor transparency-log claim recorded.
Headers on the wire
X-Driftlock-Delivery: 019012ac-31a2-7f01-9abc-7e9d1c0b1445
X-Driftlock-Signature: t=1762644120,v1=d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5fPayload example
{
"id": "01J9X8M3T0K9Q4R7PBN1WVZ8CX",
"event": "pr.signed",
"created_at": "2026-08-08T11:48:02Z",
"tenant": "acme-checkout",
"delivery_id": "019012ac-31a2-7f01-9abc-7e9d1c0b1445",
"attempt": 1,
"data": {
"pr": {
"pr_url": "https://github.com/acme/checkout-svc/pull/314",
"repo": "acme/checkout-svc",
"branch": "driftlock/ISS-7421",
"head_sha": "8f6c1d5b9c2f6c1d5b9c2f6c1d5b9c2f6c1d5b9c",
"commit_signed": true
},
"signature": {
"payload_digest": "sha256:1f2e3d4c5b6a79808796a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4",
"base64_signature": "MEUCIQDx...4hY=",
"rekor_uuid": "97f3e9a2cb4d44e0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"signer_kid": "kms://acme-checkout/signing/commit/2026-q3",
"signing_scheme": "kms-resident-cosign-v1"
},
"audit_links": {
"sbom_hash": "sha256:7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e",
"audit_record_id": "AUD-7421-314",
"policy_decision": "allow"
}
}
}Field-by-field reference
R marks a required field — receivers should reject the delivery when it is absent. O marks an optional field — surface as `null` in your own schema, do not infer a value.
Field · type · R/O
Description · source
id
stringR
ULID identifying this specific event emission.
source · driftlock-egress
event
stringR
The dotted event name (e.g. `triage.issue.created`).
source · driftlock-egress
created_at
stringR
RFC-3339 timestamp the event was emitted at.
source · driftlock-egress
tenant
stringR
Tenant slug the event belongs to.
source · driftlock-egress
delivery_id
stringR
UUID v7 — same across every retry; receivers dedupe on this.
source · driftlock-egress
attempt
integerR
1-indexed delivery attempt. Same delivery_id across retries.
source · driftlock-egress
data.pr.pr_url
stringR
URL of the signed PR.
source · driftlock-signer
data.pr.repo
stringR
`<org>/<repo>` slug.
source · driftlock-signer
data.pr.branch
stringR
Watcher-owned branch name.
source · driftlock-signer
data.pr.head_sha
stringR
Head commit SHA that was signed.
source · driftlock-signer
data.pr.commit_signed
booleanR
True on every emission — false would surface as a reject, never a delivery.
source · driftlock-signer
data.signature.payload_digest
stringR
SHA-256 of the payload bytes signed by cosign.
source · cosign / KMS
data.signature.base64_signature
stringR
Base64-encoded cosign signature bytes.
source · cosign / KMS
data.signature.rekor_uuid
stringR
Rekor transparency-log inclusion uuid; queryable on the public log.
source · cosign / Rekor
data.signature.signer_kid
stringR
KMS-resident key id under `kms://<tenant>/signing/commit/<rotation>`.
source · KMS
data.signature.signing_scheme
stringR
Stable scheme tag — `kms-resident-cosign-v1` for KMS; HSM variants share the wire shape.
source · cosign / KMS
data.audit_links.sbom_hash
stringR
SHA-256 of the CycloneDX document emitted in the same run.
source · driftlock-sbom
data.audit_links.audit_record_id
stringR
Per-PR audit-trail record id — linkable back to the run.
source · driftlock-audit
data.audit_links.policy_decision
stringR
`allow` | `block` — driven by the egress / sandbox policy that ran on the diff.
source · driftlock-policy
Corresponds to the `signed-commit` row in the per-PR audit trail — use `data.audit_links.audit_record_id` to walk back to the failing test.
Outbound event
sandbox.failed
Trigger
Fired when a sandbox run fails pre-flight, exhausts its retry budget, matches a deny policy, or diverges from the recorded replay envelope.
Headers on the wire
X-Driftlock-Delivery: 019012ad-9c44-7f01-9abc-2c1a0fbe7711
X-Driftlock-Signature: t=1762645234,v1=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2Payload example
{
"id": "01J9X8N7Q8R2T1U5VPX0WYZ4BS",
"event": "sandbox.failed",
"created_at": "2026-08-08T11:55:09Z",
"tenant": "acme-checkout",
"delivery_id": "019012ad-9c44-7f01-9abc-2c1a0fbe7711",
"attempt": 2,
"data": {
"run": {
"run_id": "run-7422-a1b2",
"repo": "acme/checkout-svc",
"head_sha": "8f6c1d5b9c2f6c1d5b9c2f6c1d5b9c2f6c1d5b9c",
"hermetic_build_hash": "sha256:9c8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b"
},
"failure": {
"kind": "pre-flight",
"message": "toolchain identity mismatch: hermetic build hash diverges from recorded envelope",
"failing_tests": [
"test/checkout/refund.rs: refund_id_round_trip"
],
"log_url": "https://ci.internal/acme/checkout-svc/7421"
},
"policy": {
"policy_id": "POL-2026-q3",
"policy_version": "1.4.2",
"deny_reason": "deny: egress host 'unknown-binary.mirror.example' not in allow-list"
}
}
}Field-by-field reference
R marks a required field — receivers should reject the delivery when it is absent. O marks an optional field — surface as `null` in your own schema, do not infer a value.
Field · type · R/O
Description · source
id
stringR
ULID identifying this specific event emission.
source · driftlock-egress
event
stringR
The dotted event name (e.g. `triage.issue.created`).
source · driftlock-egress
created_at
stringR
RFC-3339 timestamp the event was emitted at.
source · driftlock-egress
tenant
stringR
Tenant slug the event belongs to.
source · driftlock-egress
delivery_id
stringR
UUID v7 — same across every retry; receivers dedupe on this.
source · driftlock-egress
attempt
integerR
1-indexed delivery attempt. Same delivery_id across retries.
source · driftlock-egress
data.run.run_id
stringR
Sandbox run id — the same id linked from the triage event.
source · driftlock-sandbox
data.run.repo
stringR
`<org>/<repo>` slug.
source · driftlock-sandbox
data.run.head_sha
stringR
Head commit SHA the run executed against.
source · driftlock-sandbox
data.run.hermetic_build_hash
stringR
Hermetic-build hash recorded for this attempt.
source · driftlock-sandbox
data.failure.kind
stringR
`pre-flight` | `retry-budget` | `deny-policy` | `replay-mismatch`.
source · driftlock-sandbox
data.failure.message
stringR
Human-readable failure detail — matches the line the platform team triages.
source · driftlock-sandbox
data.failure.failing_tests
array<string>R
One entry per failing test path — `file:name`.
source · driftlock-sandbox
data.failure.log_url
stringR
Internal URL the reviewer drills into for full output.
source · driftlock-sandbox
data.policy.policy_id
stringR
Policy that was applied to the run.
source · driftlock-policy
data.policy.policy_version
stringR
Policy version — surfaces drift between sandbox runs.
source · driftlock-policy
data.policy.deny_reason
stringR
Verbatim deny reason from the policy engine; omitted when `data.failure.kind` is `pre-flight`.
source · driftlock-policy
Corresponds to the `sandboxed-test` row in the per-PR audit trail — when `data.failure.kind` is `deny-policy`, also link `data.policy.deny_reason` to the egress audit line.
Outbound event
dependency.advisory.detected
Trigger
Fired when an advisory watch hits a PURL reachable from the review surface — same vocabulary as the inbound dependency webhook on /docs, normalized to Driftlock's outbound shape.
Headers on the wire
X-Driftlock-Delivery: 019012ae-77b1-7f01-9abc-9d3c8e22af04
X-Driftlock-Signature: t=1762645901,v1=eeff00112233445566778899aabbccddeeff00112233445566778899aabbccddPayload example
{
"id": "01J9X8P5R9S3T7V2WQY0XZA7DT",
"event": "dependency.advisory.detected",
"created_at": "2026-08-08T12:01:22Z",
"tenant": "acme-checkout",
"delivery_id": "019012ae-77b1-7f01-9abc-9d3c8e22af04",
"attempt": 1,
"data": {
"advisory": {
"advisory_id": "GHSA-xxxx-yyyy-zzzz",
"source": "GHSA",
"cve": {
"id": "CVE-2026-9999",
"cvss_v3": 7.5
},
"severity": "high",
"summary": "Prototype pollution in lodash.result function reachable from checkout flow.",
"published_at": "2026-08-07T22:13:00Z"
},
"affected": {
"purl": ["pkg:npm/lodash@4.17.20"],
"repo": ["acme/checkout-svc"],
"versions": {
"introduced": "4.17.20",
"fixed": "4.17.21"
}
},
"reachability": {
"call_graph": [
"src/checkout/discount.ts:7",
"src/utils/dep.ts:23"
],
"reachability": "direct"
},
"draft_fix_pr": {
"pr_url": "https://github.com/acme/checkout-svc/pull/315",
"branch": "driftlock/advisory-GHSA-xxxx-yyyy-zzzz",
"head_sha": "9f7d2e6c8b4a1e0f3d5c7b9a2e4f6c8d0b1a3e5f7c9d1b3a5e7f9c1d3b5a7e9f1",
"status": "draft"
}
}
}Field-by-field reference
R marks a required field — receivers should reject the delivery when it is absent. O marks an optional field — surface as `null` in your own schema, do not infer a value.
Field · type · R/O
Description · source
id
stringR
ULID identifying this specific event emission.
source · driftlock-egress
event
stringR
The dotted event name (e.g. `triage.issue.created`).
source · driftlock-egress
created_at
stringR
RFC-3339 timestamp the event was emitted at.
source · driftlock-egress
tenant
stringR
Tenant slug the event belongs to.
source · driftlock-egress
delivery_id
stringR
UUID v7 — same across every retry; receivers dedupe on this.
source · driftlock-egress
attempt
integerR
1-indexed delivery attempt. Same delivery_id across retries.
source · driftlock-egress
data.advisory.advisory_id
stringR
Originating feed id — GHSA, CVE, or OSV.
source · driftlock-watch (GHSA / NVD / OSV)
data.advisory.source
stringR
Originating feed: `NVD` | `GHSA` | `OSV`.
source · driftlock-watch
data.advisory.cve
objectO
CVE envelope — `id` + `cvss_v3`. Omitted when the advisory is GHSA-only.
source · driftlock-watch
data.advisory.severity
stringR
`low` | `medium` | `high` | `critical`.
source · driftlock-watch
data.advisory.summary
stringR
One-line advisory summary surfaced in the PR body.
source · driftlock-watch
data.advisory.published_at
stringR
RFC-3339 timestamp the feed originally published the advisory.
source · driftlock-watch
data.affected.purl
array<string>R
PURLs of vulnerable package versions reachable from the review surface.
source · driftlock-watch
data.affected.repo
array<string>R
Repos under watch that import at least one vulnerable PURL.
source · driftlock-watch
data.affected.versions
objectR
Bounds — `introduced` and `fixed` semver ranges.
source · driftlock-watch
data.reachability.call_graph
array<string>R
Files in each affected repo that import the vulnerable package.
source · driftlock-watch
data.reachability.reachability
stringR
`direct` | `transitive` | `none`.
source · driftlock-watch
data.draft_fix_pr
objectO
Nullable — set only when the watcher auto-drafted a fix. Shape mirrors `data.draft_pr` on the triage event.
source · driftlock-triage
Correlates to the `triage + drafted-fix` rows in the per-PR audit trail — when `data.draft_fix_pr` is set, link the handler to that PR.
Verification appendix
Three commands a receiver runs against any inbound delivery.
Each command below reproduces the v1 signature over the same signing string the sender used (<unix>.<raw_body>) and prints MATCH or MISMATCH: +N s skew. Pipe the raw request body on stdin and export DRIFTLOCK_SECRET from your KMS-resident secret store.
Header composition reference
Every receiver should reconstruct the X-Driftlock-Signature header from three pieces: the unix seconds from the inbound t= field, the literal . separator, and the raw bytes of the request body — never a re-serialized copy of the parsed JSON.
# Reconstruct what the sender signed
T = <t-field-from-header> # e.g. 1762643514
PAYLOAD = <raw request body bytes> # the body, byte-for-byte
SIG = HMAC_SHA256(secret, "<T>.<PAYLOAD>".encode())
# Expected header the sender wrote
HEADER = "t=<T>,v1=<hex(SIG)>"
# Tolerance window — reject if received.now() - T > 300
DELAY_S = max(0, receiver.now() - T)
REJECT? delay > 300 # MISMATCH: +<DELAY_S> s skewShell · openssl
bash · macOS · linuxReproduce the signature against an inbound payload + received signature — prints `MATCH` or `MISMATCH: +N s skew`. Pipe the raw request body on stdin, pass the received header as `RECEIVED=` and the secret as `DRIFTLOCK_SECRET`.
RECEIVED="t=1762643514,v1=8b6f9e7a2c1d4f5b6e7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e"; T=$(printf "%s" "$RECEIVED" | cut -d, -f1 | cut -d= -f2); R=$(printf "%s" "$RECEIVED" | cut -d, -f2 | cut -d= -f2); SIG=$(printf "%s.%s" "$T" "$(cat)" | openssl dgst -sha256 -hmac "$DRIFTLOCK_SECRET" -hex | awk "{print $NF}"); if [ "$SIG" = "$R" ]; then echo MATCH; else echo "MISMATCH: +$(( $(date +%s) - T )) s skew"; fiNode · crypto.createHmac
node 18+Same proof, in Node. Reads the raw body from stdin and the received header from argv[2]; secret from `DRIFTLOCK_SECRET`.
node -e "const c=require('crypto');const [tp,vp]=process.argv[2].split(',');const t=tp.split('=')[1];const r=vp.split('=')[1];let buf='';process.stdin.on('data',d=>buf+=d).on('end',()=>{const m=c.createHmac('sha256',process.env.DRIFTLOCK_SECRET).update(t+'.'+buf).digest('hex');console.log(m===r?'MATCH':'MISMATCH: +'+(Math.floor(Date.now()/1000)-Number(t))+' s skew');})" "$RECEIVED"Python · hmac.new
python 3.8+Same proof, in Python — drop into a CI step or a server-side verifier inline. Reads the body from sys.stdin, the header from sys.argv[1], the secret from env.
python3 -c "import sys,hmac,hashlib,os,time; tp,vp=sys.argv[1].split(\",\"); t=int(tp.split(\=\")[1]); r=vp.split(\=\")[1]; body=sys.stdin.buffer.read().decode(); calc=hmac.new(os.environ[\"DRIFTLOCK_SECRET\"].encode(),f"{t}.{body}".encode(),hashlib.sha256).hexdigest(); print(\"MATCH\" if calc==r else f\"MISMATCH: +{int(time.time())-t} s skew\")" "$RECEIVED"
openssl · createHmac · hmac.new · MATCH · MISMATCH: +N s skew
Back to the runbook
The parent /docs page covers the four inbound sources — this page covers everything Driftlock dispatches out.
Inbound webhook sources (the GitHub App, CI runner, dependency feeds, on-call envelopes) live on the parent page with their minimum payloads and signature notes. Outbound events — the triage handoff, the commit signature, the sandbox failure, the advisory reachability — live here with field-by-field detail and a six-line sample payload a receiver can replay.
Request a deeper-docs walkthrough
We'll reply with a calendar link and the air-gap or key-handling runbook relevant to your scope.