βοΈOperational Patterns
How to deliver vCons reliably in production β signing, idempotency, retries, dead-letter queues, health, metrics.
The vCon construction story is well-defined: use the vcon library helpers and stay on the Spec Compliance Checklist. The delivery story β how a built vCon actually leaves your adapter and reaches a conserver, MCP server, archive, or downstream pipeline β is where most adapters historically went wrong. This page documents the patterns the vcon-adapter-template ships with and that new adapters should adopt.
Every pattern below is implemented in code at webhook_delivery.py and health_server.py. If you scaffolded from the template, you already have this β read this page to know why it works the way it does.
The delivery contract
An adapter SHOULD POST each vCon as a single JSON body to one or more configured webhook endpoints. The HTTP request looks like this:
POST /vcons HTTP/1.1
Content-Type: application/json
Idempotency-Key: 6f1c5a8b-3e2d-4a1f-9c4f-7b8a2f0e1d3c
X-Hub-Signature-256: sha256=2c8f9...
{"vcon":"0.4.0","uuid":"6f1c5a8b-3e2d-4a1f-9c4f-7b8a2f0e1d3c", ...}Three things are non-negotiable for production:
The body is compact JSON β no extra whitespace. The signature is computed over the exact bytes on the wire, so any reformatting on either side breaks verification.
The
Idempotency-Keyis the vCon'suuid. Same vCon β same key β the receiver MUST treat retries as no-ops.The
X-Hub-Signature-256header is computed exactly the same way GitHub computes it for webhooks. This is intentional β most receivers already have well-tested verification code for this format.
HMAC body signing
Every adapter SHOULD sign its outgoing vCon webhooks with X-Hub-Signature-256: sha256=<hexdigest>. The signing recipe:
On the receiver side, the verification flow is:
Read the raw request body (do NOT re-serialize the parsed JSON β the bytes must match the signed bytes exactly)
Recompute the HMAC with the shared secret
Compare in constant time with
hmac.compare_digest
The shared secret lives in VCON_WEBHOOK_HMAC_SECRET (or per-endpoint in config.yaml). It is independent of any JWS keys used for vCon-level signing β they solve different problems (transport authentication vs. content provenance).
Idempotency
Every webhook delivery MUST carry an Idempotency-Key header equal to the vCon's uuid. This serves two purposes:
Retries are safe. When the adapter retries after a 5xx response or a timeout, the receiver sees the same key and can skip work it has already done.
Replay is detectable. If an attacker captures and replays a signed payload, the receiver can spot the duplicate
uuidand reject it.
vCon UUIDs are v4 β sufficiently random that collisions are operationally impossible. The receiver can use them as primary keys directly.
Retries with exponential backoff
Networks fail. Downstream services are deployed, restart, get overloaded. The template's delivery layer retries up to max_attempts times (default 5) with exponential backoff:
1
β (immediate)
2
1 s
3
2 s
4
4 s
5
8 s
Backoff doubles each attempt, capped at max_backoff_seconds (default 60). Both knobs are configurable in config.yaml:
A response counts as success when the status code is 2xx. Anything else β 4xx, 5xx, timeout, connection error β counts as a failure and triggers the next retry (or the dead-letter queue if attempts are exhausted).
Note: 4xx responses are retried by default. If the receiver returns a 4xx for a malformed vCon, retrying won't change the outcome β but it also won't hurt, and it keeps the delivery path simple. If you need 4xx fast-fail semantics for your downstream, customize the retry predicate.
Dead-letter queue
When all retry attempts are exhausted across all configured endpoints, the vCon is written to disk under dead_letter_path (default ./dlq) as <uuid>.vcon.json. This guarantees that:
No vCon is ever silently lost
An operator can inspect, requeue, or hand-deliver failed messages
Audit trails survive receiver outages
The DLQ is a directory of JSON files, not a queue service. This keeps adapters small and stateless. Plug in your own re-injection cron, ops dashboard, or alerting on the directory's size β see the Conserver docs for one way to wire DLQ replay into a broader pipeline.
The vcons_dlq_total Prometheus counter (see below) tracks DLQ writes β alert on it.
Multiple endpoints (fan-out)
The template treats endpoints as a list. A vCon is delivered to every endpoint in the list; success on at least one keeps it out of the DLQ.
This is the standard pattern for shipping the same conversation to a primary processing pipeline and a long-term archive simultaneously. Each endpoint has its own HMAC secret β the body is signed independently per endpoint.
Health and metrics
The template starts an HTTP server on ${SERVER_HOST}:${SERVER_PORT} (default 0.0.0.0:8000) exposing two endpoints:
/healthz
/healthzReturns 200 OK with {"status": "ok"} JSON. Designed for Kubernetes liveness/readiness probes and load-balancer health checks. Wire it into your Dockerfile's HEALTHCHECK instruction (the template already does).
/metrics
/metricsReturns Prometheus exposition format. Three counters out of the box:
vcons_built_total
Every successful vCon construction
β
vcons_delivered_total
Every 2xx response from a webhook endpoint
endpoint
vcons_dlq_total
Every write to the dead-letter queue
β
A reasonable alerting rule pair:
The first catches the failure mode where the receiver is broken; the second catches the failure mode where the adapter is broken upstream.
JWS signing of vCons (optional, content-level)
HMAC webhook signing authenticates the transport β it tells the receiver "this body came from someone holding the shared secret." It does not travel with the vCon if the receiver later forwards it.
For end-to-end provenance, sign the vCon itself with JWS (JSON Web Signature, RS256). The template includes an optional signing path; enable it in config:
The signed form is a JWS with the vCon as its detached payload. Receivers verify with the corresponding public key β usually distributed out of band or via a JWKS endpoint. JWS-signed vCons carry their authenticity across re-forwarding, archival, and downstream tooling.
JWS signing is independent of HMAC webhook signing. Most production deployments use both: HMAC for the transport hop, JWS for content provenance.
Configuration surface
The template's config.example.yaml is the canonical source of truth. The relevant sections for operational concerns:
${ENV_VAR} substitution happens at startup, so secrets never need to be in the YAML file itself.
Logging
The template uses structlog with JSON output by default. Every delivery attempt logs a structured event:
This is grep-friendly and fits cleanly into Loki/Datadog/Splunk pipelines. The uuid field is the join key for tracing a single vCon end-to-end.
What this page is not
This is the operational story for the template's delivery layer. If you're shipping vCons over a different transport (Kafka, NATS, gRPC, files on a shared filesystem, an S3 bucket), the principles above still apply β sign the bytes, key off the UUID for idempotency, retry with backoff, persist failures β but the concrete code will differ. See the Adapter Development Guide for non-webhook delivery patterns.
Last updated
Was this helpful?