V3 API Endpoints Reference
Source files:This document covers the QTG v3 HTTP API. All v3 endpoints are mounted undersrc/qtg/interfaces/api/routes/,src/qtg/interfaces/api/schemas/
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
WhenMG_AUTH_ENABLED=true, all /v3/** requests except /healthz require inbound HMAC authentication.
Required headers:
X-QTG-Key-IdX-QTG-TimestampX-QTG-NonceX-QTG-Signature
Role axis (admin / operator / agent)
Authorization is a 3-role axis, not the legacy 6-purpose model. Eachapi_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_OPSset).agent— agent only.- All roles — admin + operator + agent (the
_ALLset).
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 aSee the document below for canonical string construction, signing examples, and the key seed procedure:Purpose:scope per endpoint (e.g.read,capital_transfer). Those are legacy labels — the live authorization control is the role axis above. Treat a legacyreadas “all roles”,approval/operate/capital_transferas “operator”,admin/writeas documented byROLE_ALLOWED_ROUTES.
docs/reference/infrastructure/auth-rollout.md— HMAC authentication enablement procedure and validation record
401— missing header / invalid key / signature mismatch / replay / timestamp out of range403— key is valid but role not allowed for the route (or agent namespace mismatch)503— route role mapping missing (fail-closed)
Rate Limiting
WhenMG_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 (
agentrole): 300 RPM (defaultMG_RATE_LIMIT_READ_RPM) - write-class (
admin/operatorroles): 60 RPM (defaultMG_RATE_LIMIT_WRITE_RPM) - When the limit is exceeded:
429 {"detail": "rate limit exceeded"}+Retry-Afterheader /healthzand/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 returns200 OKwith the existing resource - same
idempotency_key+ materially different request returns409 Conflict - pending provider outcomes remain in
PENDING_PROVIDER_CONFIRMATIONwith 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 OK2. POST /v3/plan-templates
Registers a transfer plan template. Multiple versions can be registered for the sametemplate_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 thePENDING_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
- Look up the plan version by
template_key+template_version - If
executable_in_v3_0 == false, return 409 Conflict - Load nodes/edges, then finalize executor/signer bindings via
resolve_bindings() - Determine DAG root nodes via
current_frontier_node_keys() - Generate the hash via
build_compiled_snapshot()+compute_compiled_plan_hash() - Create MovementRequest (state:
PENDING_APPROVAL, expires afterapproval_ttl_seconds) - 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; supportsstate, 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 toREADY state and execution begins.
Path Parameters
Request Body — MovementApproveRequest
Response — MovementActionResponse
200 OK
Internal processing flow
- Check that request state is
PENDING_APPROVAL - Verify
compiled_plan_hashmatch - Verify availability of executor/signer bindings for frontier nodes
approval_status->approved,reservation_status->held- Transition frontier nodes
BLOCKED->READY - Transition request state to
APPROVED - Enqueue
request_approvedcallback
Error Responses
9. POST /v3/movements//reject
Rejects a movement request.Request Body — MovementRejectRequest
Response — MovementActionResponse
200 OK (request_state: REJECTED)
Internal processing
- Check that request state is
PENDING_APPROVAL approval_status->rejected- Transition request state to
REJECTED - Enqueue
request_rejectedcallback
Error Responses
10. POST /v3/movements//actions/resume
Resumes a request inWAITING_MANUAL_ACTION or MANUAL_INTERVENTION state.
Request Body
None (empty POST).Response — MovementActionResponse
200 OK (request_state: EXECUTING)
Internal processing
- Check that request state is
WAITING_MANUAL_ACTIONorMANUAL_INTERVENTION - Transition request state to
EXECUTING - Enqueue
request_resumedcallback
Error Responses
11. POST /v3/movements//actions/retry
Finds and retries nodes inFAILED or UNKNOWN state.
Request Body
None (empty POST).Response — MovementActionResponse
200 OK (request_state: EXECUTING)
Internal processing
- Search
FAILEDorUNKNOWNnodes ordered byupdated_atascending - Transition the node to
READY - Set
current_frontierto that node’snode_key - Transition request state to
EXECUTING - Enqueue
request_retriedcallback
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
- Check that the plan version has an
on_canceledge - Transition request state to
CANCELLED - If there are completed nodes with side effects, keep
reservation_statusasheld - If no side effects, set
reservation_statustoreleased - Enqueue
request_cancelledcallback
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 OKError 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 OKError 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 suppliedprevious_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. Theclient_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
WhenMG_AUTO_APPROVE_ENABLED=true and a POST /v3/movements request contains strategy_id:
- Extract
client_idfrom HMAC auth - Look up the matching policy set by
client_id + strategy_idinauto_approve_policies - Five-stage evaluation:
is_active->asset->template->amount->binding_available - If all checks pass:
PENDING_APPROVAL->APPROVEDauto transition, frontier nodesREADY - On failure: stay in
PENDING_APPROVAL, record the reason inauto_approve_result
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_executableexecutor_unavailableamount_below_minamount_above_max
Error Responses
Auth
Purpose:read
MovementActionResponse common response
approve, reject, resume, retry, and cancel all use the same response schema.Authentication model
WhenMG_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 OKAuth
- 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=trueand a fetcher exists for thevenue_key, on-demand fetch is attempted first, then the latest snapshot is retrieved. - When the
account_typequery param is provided, it is trim + uppercased. Example:?account_type=usdt_futureretrieves theUSDT_FUTUREsnapshot. - 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 OKwith an emptybalancesarray. SPOT/USDT_FUTUREare the primary account types linked to Binance capital-transfer reservations. Balance-only account types such asFUNDING(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-signeduniversalTransfer-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
SPOTandUSDT_FUTUREunder supported topologies:- master
SPOT-> subSPOTorUSDT_FUTURE - sub
SPOTorUSDT_FUTURE-> masterSPOT - same sub
SPOT↔USDT_FUTURE
- master
- Cross-sub transfers are intentionally rejected in v1.
- Same-account no-op transfers are rejected: for the same sub-account,
source_account_typeanddestination_account_typemust 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 toCapitalTransferView.
Path Parameters
Auth
- Purpose:
capital_transfer
29. POST /v3/capital-transfers//actions/resolve
Manually resolves a capital transfer inPENDING_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 insrc/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.
Related documents
- Data Model Reference — table details
- Bootstrap & Configuration — settings and initialization
- Runtime Workers — per-worker behavior details