Skip to main content

V3 API Endpoints Reference

Source files: src/qtg/interfaces/api/routes/, src/qtg/interfaces/api/schemas/
This document covers the QTG v3 HTTP API. All v3 endpoints are mounted under settings.api_prefix (default /v3). Only healthz is mounted at the root, and the /dashboard/* BFF is mounted at the root outside the HMAC scope. The detailed sections below document the core movement / template / registry / route-catalog / capital-transfer lane in full. Additional endpoint families that are registered but documented in less detail here are listed under Additional endpoint families — refer to their route source files for request/response schemas.

Inbound auth overview

When MG_AUTH_ENABLED=true, all /v3/** requests except /healthz require inbound HMAC authentication. Required headers:
  • X-QTG-Key-Id
  • X-QTG-Timestamp
  • X-QTG-Nonce
  • X-QTG-Signature

Role axis (admin / operator / agent)

Authorization is a 3-role axis, not the legacy 6-purpose model. Each api_client_keys row carries a role constrained by a DB CHECK to one of admin, operator, agent. Keys are seeded with seed_auth_client --role <role> (there is no --purpose flag, and there is no read role). Per-route allowed roles live in ROLE_ALLOWED_ROUTES in src/qtg/interfaces/api/middleware/auth/hmac.py (Pro routes extend it at boot via register_route_role()). The three role sets used by the route map:
  • admin — admin only.
  • operator — admin + operator (the _OPS set).
  • agent — agent only.
  • All roles — admin + operator + agent (the _ALL set).
Representative route -> role mapping: The agent role is bound 1:1 to a network-scoped AgentAuthority. AgentNamespaceMiddleware enforces that the request path namespace matches the authority’s namespace, returning 403 agent_namespace_mismatch on a mismatch.
Note: Older sections of this doc still reference a Purpose: scope per endpoint (e.g. read, capital_transfer). Those are legacy labels — the live authorization control is the role axis above. Treat a legacy read as “all roles”, approval/operate/capital_transfer as “operator”, admin/write as documented by ROLE_ALLOWED_ROUTES.
See the document below for canonical string construction, signing examples, and the key seed procedure:
  • docs/reference/infrastructure/auth-rollout.md — HMAC authentication enablement procedure and validation record
Auth failures generally respond as follows.
  • 401 — missing header / invalid key / signature mismatch / replay / timestamp out of range
  • 403 — key is valid but role not allowed for the route (or agent namespace mismatch)
  • 503 — route role mapping missing (fail-closed)

Rate Limiting

When MG_RATE_LIMIT_ENABLED=true, a per-key in-memory fixed-window rate check is performed after HMAC authentication. Buckets are keyed by role class:
  • read-class (agent role): 300 RPM (default MG_RATE_LIMIT_READ_RPM)
  • write-class (admin / operator roles): 60 RPM (default MG_RATE_LIMIT_WRITE_RPM)
  • When the limit is exceeded: 429 {"detail": "rate limit exceeded"} + Retry-After header
  • /healthz and /dashboard/* are not subject to rate limiting
  • Configuration: MG_RATE_LIMIT_ENABLED, MG_RATE_LIMIT_READ_RPM, MG_RATE_LIMIT_WRITE_RPM

Endpoint Summary


Capital transfer API family

capital-transfers is a treasury/capital-ops subsystem separated from the existing movement/template/route-catalog lane. v1 contract:
  • provider-neutral public surface
  • admin/operator-only
  • first create returns 201 Created
  • same idempotency_key + semantically same request returns 200 OK with the existing resource
  • same idempotency_key + materially different request returns 409 Conflict
  • pending provider outcomes remain in PENDING_PROVIDER_CONFIRMATION with source-side hold retained and destination capacity unchanged

1. GET /healthz

Checks service state. It is mounted at the root without a prefix.

Response

200 OK

2. POST /v3/plan-templates

Registers a transfer plan template. Multiple versions can be registered for the same template_key.

Request Body — PlanTemplateCreateRequest

What risk_controls actually enforces: risk_controls is a free-form object. The API accepts any keys, and every key is carried into the compiled plan snapshot and into compiled_plan_hash, so whatever you put here is approval-pinned and auditable. But only two keys change runtime behaviour: max_amount is denominated in the intent asset, not USD — it is compared against the node’s resolved amount. On an XRP template, "max_amount": "1000" is 1,000 XRP. Every other key is descriptive metadata. asset_allowlist (seeded templates, and the quickstart examples) and network_allowlist (seeded templates only) are read by nothing — a template carrying "asset_allowlist": ["XRP"] does not reject a movement whose intent.asset is USDT. Amount-shaped keys other than max_amount and min_amount (max_amount_usd, hardcap_usd) are likewise inert; use max_amount if you want a cap that binds. Fund-safety controls that do bind live elsewhere, and none of them is configured through a plan-template field: the address allowlist (a DB plane, checked at on-chain dispatch), the approval-policy plane (the approval_policy_targets table), and the outflow / balance-reservation caps. In particular the template’s own approval_policy object — listed just above risk_controls in the table — is not that plane. Nothing reads its keys; approval is required by default and is waived only by the policy plane. It is covered by the plan-graph hash, so it is auditable as a declaration of intent, but setting {"required_approvals": 2} there does not require two approvals. PlanNodeCreate: PlanEdgeCreate: CompletionPolicyView: completion_assurance allowed values: provider_completed, destination_observed, destination_credited, destination_finalized, protocol_finalized

Request Example

executor_selector is an abstract selector. action_type is required and provider / chain_family are optional. On-chain nodes use signer_selector in the form {"chain_family": "evm", "profile": ...} when needed.

Response — PlanTemplateCreateResponse

201 Created

Error Responses


3. GET /v3/plan-templates/

Retrieves detailed information about a registered template.

Path Parameters

Response — PlanTemplateDetailResponse

200 OK All PlanTemplateCreateResponse fields plus the additional fields below: PlanNodeView: all PlanNodeCreate fields plus id (UUID) PlanEdgeView:

Error Responses


4. POST /v3/movements

Creates a movement request. Immediately after creation it enters the PENDING_APPROVAL state.

Request Body — MovementCreateRequest

If input_params.node_config is an object, it supplements each node’s template config at execution time. For the same key, the template config takes precedence. Example: Hyperliquid hl.topup passes the Arbitrum RPC endpoint via input_params.node_config.arbitrum_rpc_endpoint.

Request Example

Response — MovementCreateResponse

201 Created

Internal processing flow

  1. Look up the plan version by template_key + template_version
  2. If executable_in_v3_0 == false, return 409 Conflict
  3. Load nodes/edges, then finalize executor/signer bindings via resolve_bindings()
  4. Determine DAG root nodes via current_frontier_node_keys()
  5. Generate the hash via build_compiled_snapshot() + compute_compiled_plan_hash()
  6. Create MovementRequest (state: PENDING_APPROVAL, expires after approval_ttl_seconds)
  7. Create a MovementRequestNode for each plan node (state: BLOCKED)

Error Responses


5. GET /v3/movements

Retrieves the list of movement requests. Sorted by creation time descending; supports state, strategy_id, and limit filters.

Query Parameters

Response — MovementListResponse

200 OK MovementSummary:

Example


6. GET /v3/movements/

Retrieves detailed information about a movement request.

Path Parameters

Response — MovementDetailResponse

200 OK TemplateRef: MovementNodeView: MovementArtifactView:

Error Responses


7. GET /v3/movements//timeline

Retrieves the movement request event timeline (audit trail). Ordered chronologically.

Path Parameters

Response — list[MovementTimelineEventView]

200 OK

8. POST /v3/movements//approve

Approves a movement request. On approval, frontier nodes transition to READY state and execution begins.

Path Parameters

Request Body — MovementApproveRequest

Response — MovementActionResponse

200 OK

Internal processing flow

  1. Check that request state is PENDING_APPROVAL
  2. Verify compiled_plan_hash match
  3. Verify availability of executor/signer bindings for frontier nodes
  4. approval_status -> approved, reservation_status -> held
  5. Transition frontier nodes BLOCKED -> READY
  6. Transition request state to APPROVED
  7. Enqueue request_approved callback

Error Responses


9. POST /v3/movements//reject

Rejects a movement request.

Request Body — MovementRejectRequest

Response — MovementActionResponse

200 OK (request_state: REJECTED)

Internal processing

  1. Check that request state is PENDING_APPROVAL
  2. approval_status -> rejected
  3. Transition request state to REJECTED
  4. Enqueue request_rejected callback

Error Responses


10. POST /v3/movements//actions/resume

Resumes a request in WAITING_MANUAL_ACTION or MANUAL_INTERVENTION state.

Request Body

None (empty POST).

Response — MovementActionResponse

200 OK (request_state: EXECUTING)

Internal processing

  1. Check that request state is WAITING_MANUAL_ACTION or MANUAL_INTERVENTION
  2. Transition request state to EXECUTING
  3. Enqueue request_resumed callback

Error Responses


11. POST /v3/movements//actions/retry

Finds and retries nodes in FAILED or UNKNOWN state.

Request Body

None (empty POST).

Response — MovementActionResponse

200 OK (request_state: EXECUTING)

Internal processing

  1. Search FAILED or UNKNOWN nodes ordered by updated_at ascending
  2. Transition the node to READY
  3. Set current_frontier to that node’s node_key
  4. Transition request state to EXECUTING
  5. Enqueue request_retried callback

Error Responses


12. POST /v3/movements//actions/cancel

Cancels a movement request. Cancellation is possible only for templates that define a cancel path (on_cancel edge).

Request Body

None (empty POST).

Response — MovementActionResponse

200 OK (request_state: CANCELLED)

Internal processing

  1. Check that the plan version has an on_cancel edge
  2. Transition request state to CANCELLED
  3. If there are completed nodes with side effects, keep reservation_status as held
  4. If no side effects, set reservation_status to released
  5. Enqueue request_cancelled callback

Error Responses


13. GET /v3/executors

Retrieves the registered executor registry. Merges DB-persisted entries with in-memory registrations.

Response — list[ExecutorRegistryView]

200 OK

14. PATCH /v3/executors/

Changes the executor registry state. This is an operational control that blocks/allows new movement approvals; it does not perform in-process hot reload. Even if no DB row exists, when an in-memory binding exists for the same key, the row is auto-materialized before the state is reflected.

Request Body — UpdateRegistryStatusRequest

Response

200 OK

Error Responses


15. GET /v3/signers

Retrieves the registered signer registry. Returns a merge of the persistent registry and the in-memory registry.

Response — list[SignerRegistryView]

200 OK

16. PATCH /v3/signers/

Changes the signer registry state. This is an operational control that blocks/allows new movement approvals; it does not perform in-process hot reload. Even if no DB row exists, when an in-memory binding exists for the same key, the row is auto-materialized before the state is reflected. PATCH allows only active and disabled as status.

Request Body — UpdateRegistryStatusRequest

Response

200 OK

Error Responses


17. PUT /v3/signers//expected-identity

Pins the expected identity for an enrolled EVM signer. This is an admin-only Free registry operation; it neither rotates a signer nor changes movement bindings.

Request Body — SetExpectedSignerIdentityRequest

Request Example

CAS and runtime semantics

The route validates the target is a persisted EVM signer, resolves the live identity through the optional signer identity protocol, and locks the registry row. A change succeeds only when the supplied previous_expected_address equals the stored value and the submitted expected address equals the freshly resolved runtime identity. No-op precedence is explicit: when stored expected identity, submitted identity, and live identity are all A, the response is noop even if previous_expected_address is stale C; the stale compare-and-set value matters only when a change is requested.

Response — SetExpectedSignerIdentityResponse

200 OK

Stable error codes

This surface establishes only the expected-versus-observed registry contract. It does not enforce movement-time or rotation-time address drift; that enforcement is a separate follow-up.

18. POST /v3/auto-approve-policies

Creates an auto-approval policy. The client_id + strategy_id combination must be unique.

Request Body — CreatePolicyRequest

AllowedTemplateEntry:

Response — PolicyResponse

201 Created

Error Responses


19. GET /v3/auto-approve-policies

Retrieves the list of registered auto-approval policies.

Response — list[PolicyResponse]

200 OK

20. GET /v3/auto-approve-policies/

Retrieves auto-approval policy details.

Path Parameters

Response — PolicyResponse

200 OK

Error Responses


21. PATCH /v3/auto-approve-policies/

Updates an auto-approval policy. Updates only the submitted fields.

Path Parameters

Request Body — UpdatePolicyRequest

Response — PolicyResponse

200 OK

Error Responses


Auto-approve behavior flow

When MG_AUTO_APPROVE_ENABLED=true and a POST /v3/movements request contains strategy_id:
  1. Extract client_id from HMAC auth
  2. Look up the matching policy set by client_id + strategy_id in auto_approve_policies
  3. Five-stage evaluation: is_active -> asset -> template -> amount -> binding_available
  4. If all checks pass: PENDING_APPROVAL -> APPROVED auto transition, frontier nodes READY
  5. On failure: stay in PENDING_APPROVAL, record the reason in auto_approve_result
Important: policies are bound to client_id. Client A’s policy does not apply to client B’s request.

22. GET /v3/route-catalog

Retrieves the catalog of available routes. Returns a combination of template metadata, execution statistics, and executor health.

Query Parameters

Response — RouteCatalogResponse

200 OK

Response fields

Error Responses

Auth

Purpose: read

23. POST /v3/routes/recommend

Retrieves an optimal route recommendation (advisory). Based on route catalog candidates, it computes feasibility and priority scores.

Request Body

Response — RouteRecommendResponse

200 OK

Feasibility rules

  • not_executable
  • executor_unavailable
  • amount_below_min
  • amount_above_max
Feasible routes always rank ahead of infeasible ones.

Error Responses

Auth

Purpose: read

MovementActionResponse common response

approve, reject, resume, retry, and cancel all use the same response schema.

Authentication model

When MG_AUTH_ENABLED=true, the HMAC authentication middleware applies to all /v3/** requests except /healthz. The middleware validates the X-QTG-Key-Id / X-QTG-Timestamp / X-QTG-Nonce / X-QTG-Signature headers and controls route access based on the 3-role axis (admin / operator / agent) described in Inbound auth overview. When MG_RATE_LIMIT_ENABLED=true, a per-key fixed-window rate limit check is performed additionally after authentication passes. In the approval API (approve), validating compiled_plan_hash equality serves as a plan-integrity check; the approver_id is the operator label carried in the request body and is stored as such. For the authentication enablement procedure and key seed CLI usage, refer to docs/reference/infrastructure/auth-rollout.md.

Callback event kinds

Callback events enqueued to the outbox on state changes:

24. GET /v3/budgets/

Retrieves the per-strategy remaining budget. Returns active policies with a window cap configured.

Path Parameters

Query Parameters

Response

200 OK

Auth

  • Purpose: read

25. GET /v3/balances

Retrieves the latest per-venue balance snapshots. The lookup unit is the latest row by (venue_key, asset, account_type), and only the latest row per combination is returned.

Query Parameters

Response — BalanceResponse

200 OK

Response meaning

Behavior notes

  • When fresh=true and a fetcher exists for the venue_key, on-demand fetch is attempted first, then the latest snapshot is retrieved.
  • When the account_type query param is provided, it is trim + uppercased. Example: ?account_type=usdt_future retrieves the USDT_FUTURE snapshot.
  • Latest-row identity is (venue_key, asset, account_type). Spot treasury and USDT-M Futures inventory are distinct snapshot/reservation pools even for the same asset.
  • If on-demand fetch fails, falls back to the last stored snapshot.
  • If no snapshot exists, returns 200 OK with an empty balances array.
  • SPOT / USDT_FUTURE are the primary account types linked to Binance capital-transfer reservations. Balance-only account types such as FUNDING (e.g. OKX) / FUND (e.g. Bybit) may also be returned as snapshot/filter values.

Auth

  • Purpose: read

26. POST /v3/capital-transfers

Creates a provider-internal treasury/capital transfer. The current Binance v1 implementation is master-signed universalTransfer-based, and the source/destination account type is a first-class QTG contract field.

Request Body — CapitalTransferCreateRequest

Request Example — master Spot -> sub USDT-M Futures

Request Example — sub USDT-M Futures -> master Spot

Response — CapitalTransferView

201 Created for a new semantic request. 200 OK when the same idempotency_key is replayed with a semantically identical request.

Response meaning

Binance v1 topology notes

  • Binance master binding is Spot treasury only: if either side is master, that side’s account type must be SPOT.
  • Sub-account bindings can move between SPOT and USDT_FUTURE under supported topologies:
    • master SPOT -> sub SPOT or USDT_FUTURE
    • sub SPOT or USDT_FUTURE -> master SPOT
    • same sub SPOTUSDT_FUTURE
  • Cross-sub transfers are intentionally rejected in v1.
  • Same-account no-op transfers are rejected: for the same sub-account, source_account_type and destination_account_type must differ.
  • Master-to-master topology is not a supported capital-transfer operation.

Error Responses

Auth

  • Purpose: capital_transfer

27. GET /v3/capital-transfers

Retrieves capital transfers, ordered newest first.

Query Parameters

Response — CapitalTransferListResponse

Auth

  • Purpose: capital_transfer

28. GET /v3/capital-transfers/

Retrieves a single capital transfer. The response schema is identical to CapitalTransferView.

Path Parameters

Auth

  • Purpose: capital_transfer

29. POST /v3/capital-transfers//actions/resolve

Manually resolves a capital transfer in PENDING_PROVIDER_CONFIRMATION or MANUAL_INTERVENTION state to a terminal/holding state.

Request Body — CapitalTransferResolveRequest

Auth

  • Purpose: capital_transfer

Additional endpoint families

Beyond the movement / template / registry / route-catalog / capital-transfer lane documented in detail above, the following families are registered in src/qtg/interfaces/api/routes/. They are summarized here; consult the cited source files for full request/response schemas.

Identity and audit

GET /v3/audit/events supports filters: time_range_start, time_range_end, actor_client_id, actor_key_id, entity_type, entity_key, action, request_id, outcome, namespace, cursor, limit (max 500).

Agent authorities and template proposals

Agent wallet top-ups / funding envelopes

The agent-wallet top-up lane is dedicated and must not be driven through generic movement creation — see the Agent Wallet Top-Up rules in CLAUDE.md.

CCIP registry, verify-drift, and Hyperliquid

Dashboard BFF (/dashboard/*)

The dashboard backend-for-frontend is mounted at the root outside the /v3 HMAC scope (build_dashboard_router() in routes/dashboard/, prefix /dashboard). Dashboard writes use per-operator HMAC v3 (nonce + timestamp): the browser holds a non-extractable CryptoKey and signs each write. The old bearer-token-in-bundle model is gone — MG_DASHBOARD_WRITE_TOKEN / VITE_DASHBOARD_WRITE_TOKEN have been removed, and main.py hard-fails startup if MG_DASHBOARD_WRITE_TOKEN is set (even to empty). Localhost binding is now defense-in-depth, not the primary control. The expected writer-key count defaults to dashboard_writer_expected_key_count = 8. See runbooks/dashboard-auth-v1-to-hmac.md.