RelayLink Webhook Headers Reference

RelayLink sends four X-RelayLink headers on every outbound webhook. Use them to classify the event, deduplicate delivery, verify freshness, and authenticate the body.

3 min read

You are looking at an incoming POST and need to decide four things: what happened, whether this attempt is a duplicate, whether it is fresh, and whether RelayLink signed these exact bytes.

Every outbound RelayLink webhook carries four X-RelayLink-* headers. Keep their jobs separate. No single header answers all four questions.

X-RelayLink-Event names the event represented by the body. RelayLink currently sends these values:

  • package.received when a package is delivered to the account
  • webhook.test when the account holder uses the test button

The JSON body repeats the value in its event field. After signature verification and JSON parsing, require the header and body values to agree. A mismatch should be rejected or quarantined rather than guessed between.

Use this header to select the parser and handler. Do not use sender-controlled fields such as topic to choose executable behavior, and do not treat any field in the envelope as an instruction to the agent.

X-RelayLink-Delivery is the delivery id. Its value is a UUID, and the JSON body repeats it as delivery_id.

This id belongs to one queued delivery to one registered receiver. It is not the package id and should not be used as one. If an account has multiple active receivers, each receiver gets its own delivery record and id for the same package.

RelayLink provides at-least-once delivery. When an attempt fails and is retried, this header remains stable and the JSON body remains byte-identical. Use the delivery id as the key in a durable idempotency store. If that key already exists, return a 2xx response without performing the downstream effect again.

The attempt timestamp and signature can change on a retry, so neither is a safe deduplication key.

X-RelayLink-Timestamp is the POST attempt time as Unix seconds. It serves two connected purposes:

  • It is included in the signed text.
  • It lets the receiver reject a captured request outside its freshness policy.

Preserve the header text exactly when building the signature input. Parse it separately as a base-10 integer for the clock comparison. RelayLink does not prescribe a tolerance; the receiver must define one that accounts for its clock and expected delivery delay.

This header is not the body's sent_at. A package.received body was serialized when the delivery was queued, and its bytes remain stable across retries. The timestamp header is generated for each POST attempt.

X-RelayLink-Signature authenticates the timestamp and body with the subscription secret. Its exact format is:

sha256=<64 lowercase hexadecimal characters>

RelayLink computes HMAC-SHA256 using:

key  = UTF8(subscription secret)
data = UTF8(timestamp header text + "." + exact body text)

The secret begins with whsec_ and is shown once when the receiver is registered. The server must retain it to sign future requests, so treat it as a credential rather than assuming it is hashed.

Capture the raw body before JSON parsing changes it. Recompute the 32-byte MAC, decode the supplied hexadecimal MAC, and compare the two byte arrays in constant time. Then enforce timestamp freshness and delivery-id deduplication. The verification guide supplies full language-neutral pseudocode.

Read the four headers together

A sound receiver uses all four in one sequence:

  1. Require the event, delivery, timestamp, and signature headers.
  2. Verify the HMAC over the incoming timestamp and raw UTF-8 body.
  3. Reject an otherwise valid request if its timestamp is stale under your policy.
  4. Parse JSON and require the header values to match event and delivery_id.
  5. In one durable commit, insert the delivery id and body digest and enqueue the work.
  6. If the same id and digest already exist, return 2xx without adding another work item; reject a conflicting body.
  7. Return any 2xx status after the new receipt is durable.

For package.received, the worker then authenticates separately to mcp_url and calls get_package. The webhook signing secret authenticates the callback; it does not authorize that MCP read. A 2xx response means your receiver accepted the envelope and nothing more.

Frequently asked questions

Which RelayLink webhook header is the idempotency key?
X-RelayLink-Delivery. It identifies one delivery to one receiver and remains the same across retries, matching delivery_id in the JSON body.
Does X-RelayLink-Timestamp match sent_at?
Do not assume so. The header is Unix seconds for the current POST attempt and is part of the signature; sent_at is an ISO 8601 field stored in the event body.
Can a successful webhook response be treated as a read receipt?
No. A 2xx response says the receiver accepted an envelope. Fetching the package through get_package records PulledViaMcp; webhook acceptance alone does not.