Callback Verification Contract
Defines the receiver implementation requirements for callbacks thatqtg sends to external systems.
This document is written so a receiver can be implemented in a language-agnostic way.
For the Python reference implementation, see examples/callback_receiver_fastapi.py.
1. Envelope Format
HTTP Headers
The Content-Type is always
application/json.
Payload Fields
Backward compatibility: the receiver must ignore unknown JSON keys. New fields may be added to the payload in the future. Do not raise an error for unknown keys.
1-A. detail Fields by Event Type
The structure of the detail object varies by event_type.
The receiver must ignore unknown keys, and new event types may be added in the future.
request_approved
Sent when the movement is approved. The detail field can distinguish the approval path.
Auto-approve example:
frontier_advanced
Sent when node execution progresses. detail can include information about the advanced node.
request_completed / request_failed
Sent when the movement reaches a terminal state.
recovery_alarm
Sent when CCIP recover() remains in an ambiguity state (rpc_error, pending_only, implausible_nonce_pair) for a long time without terminal evidence.
Even if the receiver does not handle this event type yet, it can ignore it as an unknown event.
The sender does not treat that as a failure.
2. Signature Scheme (v3)
2.1 Body Canonicalization
The HTTP body is serialized with the following rule:- keys sorted alphabetically (
sort_keys=True) - compact separators: no space after comma, no space after colon
- non-ASCII characters are preserved as-is (
ensure_ascii=False) - UTF-8 encoding
2.2 Canonical String
\n is a literal newline (0x0a).
2.3 Signature Creation
- key: UTF-8 encoded shared secret
- message: UTF-8 encoded canonical string
- result: lowercase hex digest
2.4 Signature Verification Pseudocode
Important: signature comparison must use constant-time comparison. This is to prevent timing side-channel attacks. Python:hmac.compare_digest(), Go:crypto/subtle.ConstantTimeCompare(), Node.js:crypto.timingSafeEqual().
3. Receiver Implementation Checklist
3.1 Signature Verification (Required)
- Confirm that
x-qtg-callback-signature-versionis"v3" - Build the canonical string:
{timestamp}\n{nonce}\n{sha256(body)} - Compute HMAC-SHA256 and compare the signature with constant-time comparison
- Return
401on mismatch
3.2 Timestamp Freshness (Required)
- Parse
x-qtg-callback-timestampfrom the header - Reject if
abs(current_time - timestamp) > max_age_seconds - Default
max_age_seconds = 300(5 minutes)
Because the header timestamp is refreshed on retry, an old callback does not fail the freshness check.
3.3 Nonce Duplicate Rejection (Required)
- Attempt to record the received
x-qtg-callback-noncein persistent storage - If it already exists, treat it as a replay -> reject
- Recommended store implementations:
- single instance: PostgreSQL +
UNIQUEconstraint - multiple instances: Redis
SET NX EXor PostgreSQLINSERT ... ON CONFLICT
- single instance: PostgreSQL +
- Recommended TTL:
max_age_seconds * 2or more (e.g., 600 seconds)
Retries of the same callback use the same nonce.
A nonce that is already recorded in the nonce store is automatically rejected on retry.
This is intentional — the sender retries only when it did not receive a 200 ACK, so
if the receiver already finished processing, duplicate processing must be prevented.
3.4 callback_id Dedup (Recommended)
callback_id in the payload is the unique ID of the callback outbox entry.
Separately from nonce-based dedup, it can be used as an idempotency key at the business-logic level.
3.5 event_sequence Tracking (Recommended)
event_sequence is an integer that represents the event order within a single movement.
If events are received out of order, use it for warning logs or buffering when ordering is required.
Callbacks may arrive out of order depending on network conditions.
event_sequence is a hint for receiver-side ordering, not a strict ordering guarantee.
4. Error Response Recommendations
- The
200response body format is free, but{"ok": true}is recommended. - Use
401for signature verification failure. 429can be used for rate limiting, and the sender retries it the same way as5xx.
5. Retry Behavior
From the receiver’s point of view: retries use the same nonce, so if the nonce is already recorded in the nonce store, it is rejected as a replay. This prevents duplicate processing in the case of “already processed successfully, but the ACK was lost”.
6. Dual Timestamp Structure
Callbacks contain two timestamps:- Use the header timestamp for signature verification.
- Use the payload timestamp for recording the event time.
- Only the header timestamp is refreshed on retry, so it can pass the freshness check.
- The payload timestamp preserves the original event occurrence time exactly.
7. CCIP request_attention Detail Extension
The CCIP lane keeps the existing callback envelope / event_type contract. The receiver must not
expect a new top-level schema, and all fields below must be handled forward-compatibly only inside
the existing detail JSON.
Based on the current code, the CCIP callback emitter reuses the generic request_attention path. Therefore,
the receiver should be implemented with the contract that the keys below may be added, and should not
treat their absence as an error.
attention_reason
CCIP manual-intervention/attention events can use the reason values below.
ccip_execute_failure_manual_exec_requiredccip_lane_cursedccip_router_drift_detectedccip_router_snapshot_staleccip_stalled_no_executionccip_submit_orphaned
requires_manual_recovery
- Type:
boolean - Meaning: whether the operator must run a separate manual recovery procedure
ccip_recovery_context
- Type: object
- Purpose: helper information so the receiver can identify the CCIP recovery target in an operator UI / incident log
- Expected keys:
execution_intent_idmessage_idsource_chain_selectordest_chain_selectorrouterreceivertokenamountsend_tx_hashlast_observed_statussdk_message_status
recovery_hint
- Type: string
- Meaning: recommended operator command or runbook hint
- Example:
uv run python -m qtg.interfaces.tools.ccip_manual_execute --message-id 0x... --source-selector ...
- Do not parse
detailwith a strict schema; ignore unknown keys - Even if
attention_reasonhas a CCIP value, reuse the existingrequest_attentionhandling pipeline as-is ccip_recovery_context/recovery_hintmay be absent, so treat them as optional
8. Python Reference Implementation
Usingverify_callback() from the qtg.callback_auth module lets you handle
signature verification, timestamp freshness, and nonce dedup in one call.
nonce_recorder Interface
Full Receiver Handler Example
For the full runnable example, seeexamples/callback_receiver_fastapi.py.