--- title: Lead Scoring Webhook v1 description: Contract profile for terminal Lead Scoring webhook delivery and DotB-Omni ingestion. status: draft tags: - lead-scoring - webhook - integration --- # Lead Scoring terminal webhook v1 ## Authority and release This document defines delivery semantics and security handling around the executable contract pack. Exact JSON shapes, constraints, golden payload bytes, and signature vectors are generated from Pydantic models and committed at `contracts/lead-scoring-webhook/v1/`. Generate the pack with `uv run generate-webhook-contract`; verify it with `uv run generate-webhook-contract --check`. `contract.json` identifies the contract pack version and event version. DotB-Omni must pin the pack by AI Platform Git commit when implementation begins and run its consumer tests against those committed files. Do not recreate envelope schemas, payload fixtures, or signature vectors in Omni. This pack is a pre-release baseline. The approved Customer Pilot specification remains the authority for product behavior and ownership. Ticket 08 owns the future dispatcher; this contract does not implement a dispatcher or receiver. ## Delivery Lead Scoring sends `POST /api/v1/internal/lead-scoring/events` only to a deployment-managed registered DotB-Omni subscriber. The endpoint is versioned by its `/api/v1/` path and is never accepted from a scoring-run request. Each committed terminal run emits `lead_scoring.run.terminal` event version `1.0.0` with one of these statuses: - `completed` - `review_required` - `failed`, with machine-readable `error_code` and `retryable` fields The generated `worker_interrupted.json` fixture is the normative failed-outcome example for an in-memory worker interruption (`error_code=worker_interrupted`, `retryable=true`). Other normalized failed outcomes can be added as versioned fixtures when their producer path is enabled. Events contain no V-Key, raw snapshot, raw conversation evidence, or unneeded PII. Delivery is at least once. A retry retains the event identity but creates a fresh Unix timestamp and signature for that delivery attempt. Ingestion success means only that Omni accepted and recorded the event decision; it does not mean EMS Marker Block writeback has completed. ## Signed request Every delivery has these headers: | Header | Value | |---|---| | `Content-Type` | `application/json` | | `X-DotB-Webhook-Id` | The body `event_id` UUID | | `X-DotB-Webhook-Key-Id` | Deployment-managed signing-key identifier | | `X-DotB-Webhook-Timestamp` | Decimal Unix timestamp used for signing | | `X-DotB-Webhook-Signature` | `v1=` | The signature input is the UTF-8 byte sequence `..`. The receiver first rejects timestamps outside 300 seconds before or after its current server clock, then verifies the signature against the unparsed request bytes before JSON parsing. It must compare signatures in constant time and confirm that the header event ID equals the payload event ID. A retry outside the original freshness window is valid only when the dispatcher signs that attempt with a fresh timestamp; event idempotency remains keyed by the unchanged event identity. `signature-vectors.json` contains deterministic test-only secrets and expected signatures for every fixture. Those values are not production credentials and must not be deployed as webhook secrets. Production signing keys live only in the deployment secret store and are never written to source, fixtures, logs, or response bodies. `X-DotB-Webhook-Key-Id` supports rotation. Deployment configuration selects the active key for sends; the receiver may accept the active and immediately previous configured key during an explicitly bounded rotation window. Unknown key IDs or invalid signatures are authentication failures, must be audited without the secret or raw body, and are not acknowledged as successful deliveries. ## Receiver acknowledgement and ordering For an authenticated, schema-valid delivery, DotB-Omni returns HTTP 200 and the body validated by `webhook-ingest-response.schema.json`: - `accepted`: event is eligible for asynchronous writeback handling. - `duplicate`: the same run is already reflected. - `stale`: a newer applicable run is already reflected. - `ordering_conflict`: another run has the same ordering key and `run_created_at`. All four are terminal ingestion acknowledgements. Invalid JSON or schema validation is HTTP 400. Invalid webhook authentication is HTTP 401. The future dispatcher must treat these non-2xx results as observable terminal delivery failures rather than success. When the webhook signature and payload schema are valid but the receiver cannot resolve the tenant destination, the receiver must return the same response for all of these cases: - `tenant_id` is not registered. - Tenant-to-account mapping is invalid. - The mapped account has been deleted. ```http HTTP/1.1 404 Not Found Content-Type: application/json ``` ```json { "error_code": "subscriber_tenant_unavailable", "detail": "Webhook destination is not available" } ``` The executable error shape is `webhook-ingest-error-response.schema.json`, and `fixtures/tenant-unavailable.json` is the shared golden response body. The response must not disclose the tenant ID, account ID, mapping state, or internal reason. The dispatcher classifies this response as terminal `receiver_client_error`, moves the event to the dead-letter state, and does not schedule an automatic retry. After provisioning or mapping repair, recovery uses operational DLQ replay. The receiver may retain a separate sanitized internal audit reason for operations, but that reason is not part of the outward response. Transient receiver failures use non-2xx responses such as HTTP 429 or 5xx and are eligible for Ticket 08's bounded retry and DLQ policy. For the same `(tenant_id, subject_ref, scoring_config_version)`, Omni compares `run_created_at`, never `occurred_at`. A same-run duplicate and stale event are no-ops. Two different run IDs with equal `run_created_at` are an ordering conflict and must not overwrite current state. The receiver must preserve this decision and return the matching acknowledgement even when delivery is replayed. ## Consumer implementation boundary DotB-Omni owns signature verification, schema validation, tenant-scoped ordering, identity-to-EMS resolution, and Marker Block writeback. It resolves the payload `tenant_id` through a deployment-managed tenant-to-account mapping or allowlist and scopes every lookup and writeback to that mapped account; the payload value is never used as a direct record selector. Its background writeback job handles EMS retry classification separately from webhook acknowledgement. AI Platform owns outbox persistence and future dispatch, including subscriber provisioning and DLQ visibility. Neither side should infer transport behavior from free-text error messages.