Docs · reference

outbound events
HMAC-verifiable

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=8b6f9e7a2c1d4f5b6e7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e

Payload 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=d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f

Payload 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=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2

Payload 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=eeff00112233445566778899aabbccddeeff00112233445566778899aabbccdd

Payload 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 skew
  • Shell · openssl

    bash · macOS · linux

    Reproduce 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"; fi
  • Node · 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.