Deliveries

Every forward attempt with exactly what left the vault and exactly what came back: method, URL, headers and body both ways, status, timing, and what the transformation rules did.

A delivery is one attempt to forward a captured webhook to the endpoint's destination. Retries and replays are attempts too. For every attempt WebhookVault keeps the full record: the request as it went over the wire, after transformation, and the response as it came back. When a destination misbehaves, the evidence is already there.

What is recorded

Sent

  • method and the exact URL
  • every header that went out, including headers set by transformation rules and the X-WebhookVault-Forwarded: true marker
  • the body, as text, or base64 for a binary capture

Received

  • the status code and the content type
  • every response header
  • the response body, as text (base64 with a base64: prefix when it is not valid UTF-8)

Outcome and rules

  • state: Succeeded, Failed (non-2xx), Errored (unreachable), DeadLetter (the attempt that exhausted the retry budget), Pending (in flight, no result yet), or Interrupted (the attempt was cut short before any outcome was known, and the delivery was re-queued); the error text; timing
  • what the transformation rules did, per rule and version

Bodies are stored up to 256 KB each way and flagged bodyTruncated when cut; the full body still went over the wire. Attempts made before this record existed show the outcome only (hasDetail: false).

Where it shows

  • Endpoint page, Deliveries tab: every attempt on the endpoint, newest first, filterable by outcome and by request id. Pick one for its Sent, Received and Rules panes. "Open request" jumps to the capture it came from.
  • Request detail: each attempt in the delivery history has a details link that opens the same record.
  • Deep link: /Endpoint/{id}?delivery={attemptId} opens an attempt directly, and ?tab=deliveries opens the tab.

Signing deliveries

A destination can carry a signing secret. When it does, every delivery leaves with a wv-signature header, so the receiver can prove the request came from WebhookVault and that nothing changed it on the way. Set it on the endpoint's Forwarding tab, or with forwardSigningSecret on the endpoint API. It is stored protected and never returned; the API reports only whether it is set and when.

Do not confuse it with the endpoint's own signing secret, which is the sender's secret and is used to verify what arrives. This one signs what leaves.

The header looks like this:

wv-signature: t=1757439000,v1=6f1c...a9

t is the Unix time the delivery was signed. v1 is the lowercase hex HMAC-SHA256 of t, a full stop, and the raw request body, keyed with the secret. The timestamp is inside the signed material, so it cannot be edited to make an old delivery look recent.

Verify it against the raw body bytes, before any JSON parsing. A reserialised body is a different byte sequence and will not match.

const crypto = require('crypto');

function verify(header, rawBody, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t));
  if (!Number.isFinite(age) || age > toleranceSeconds) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(parts.t + '.')
    .update(rawBody)
    .digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
import hashlib, hmac, time

def verify(header, raw_body, secret, tolerance_seconds=300):
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(int(time.time()) - int(parts["t"])) > tolerance_seconds:
        return False

    expected = hmac.new(
        secret.encode(),
        parts["t"].encode() + b"." + raw_body,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, parts.get("v1", ""))

Compare in constant time, and reject anything older than your tolerance so a captured delivery cannot be replayed later.

Three things worth knowing:

  • The signature covers the body after transformation rules run, because that is the body the receiver is handed.
  • A wv-signature that arrived on the original webhook is never passed through. On a signed destination it is replaced with ours; on an unsigned one it is dropped, so the receiver is never given a signature that cannot verify.
  • The same scheme signs alert webhooks, so a receiver writes the verification once.

API

CallPurpose
GET /api/v1/endpoints/{id}/deliveriesa page of attempts (page, pageSize up to 100, state, requestId)
GET /api/v1/endpoints/{id}/deliveries/{attemptId}one attempt in full: sent, received, transformNote
GET /api/v1/endpoints/{id}/requests/{requestId}/attemptsthe attempts of one request (outcome only, with hasDetail)

Response bodies from destinations are stored as they were received. Treat them as data from a third party when you read them back.

On this page