Skip to main content

Callback Verification Contract

Defines the receiver implementation requirements for callbacks that qtg 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:
Manual 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

Each element: \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)

  1. Confirm that x-qtg-callback-signature-version is "v3"
  2. Build the canonical string: {timestamp}\n{nonce}\n{sha256(body)}
  3. Compute HMAC-SHA256 and compare the signature with constant-time comparison
  4. Return 401 on mismatch

3.2 Timestamp Freshness (Required)

  1. Parse x-qtg-callback-timestamp from the header
  2. Reject if abs(current_time - timestamp) > max_age_seconds
  3. 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)

  1. Attempt to record the received x-qtg-callback-nonce in persistent storage
  2. If it already exists, treat it as a replay -> reject
  3. Recommended store implementations:
    • single instance: PostgreSQL + UNIQUE constraint
    • multiple instances: Redis SET NX EX or PostgreSQL INSERT ... ON CONFLICT
  4. Recommended TTL: max_age_seconds * 2 or 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.
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.
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 200 response body format is free, but {"ok": true} is recommended.
  • Use 401 for signature verification failure.
  • 429 can be used for rate limiting, and the sender retries it the same way as 5xx.

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_required
  • ccip_lane_cursed
  • ccip_router_drift_detected
  • ccip_router_snapshot_stale
  • ccip_stalled_no_execution
  • ccip_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_id
    • message_id
    • source_chain_selector
    • dest_chain_selector
    • router
    • receiver
    • token
    • amount
    • send_tx_hash
    • last_observed_status
    • sdk_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 ...
Receiver implementation rules:
  • Do not parse detail with a strict schema; ignore unknown keys
  • Even if attention_reason has a CCIP value, reuse the existing request_attention handling pipeline as-is
  • ccip_recovery_context / recovery_hint may be absent, so treat them as optional

8. Python Reference Implementation

Using verify_callback() from the qtg.callback_auth module lets you handle signature verification, timestamp freshness, and nonce dedup in one call.

nonce_recorder Interface

Implementation example (PostgreSQL):
Implementation example (Redis):

Full Receiver Handler Example

For the full runnable example, see examples/callback_receiver_fastapi.py.

8. Other Language Implementation Guide

Go

Node.js


Appendix: Checklist Summary