Route / developers

CCTP testnet integration

Validate your Route integration with test tokens before making mainnet payments. Your backend uses the same hosted API and the same transaction-signature and execution endpoints. Route prepares the provider transactions, retrieves attestations and verifies settlement; your application supplies the payment details and wallet signatures.

Released path: native USDC from Ethereum Sepolia → Stellar testnet, using Circle CCTP Standard or Fast. Reverse transfers and other testnet pairs are not available through this public contract. This onboarding path does not determine which network or adapter your application uses on mainnet. See mainnet execution coverage for supported production paths.

Use the complete payment starter for a runnable backend example with persistent state, sign-only transaction review and restart recovery. Use the normalized next-action API and SDK to follow saved payment state, and the recovery playbook for ambiguous outcomes. Transaction responses expose authorizationPrepared; save the transaction authorization with POST /payments/{id}/transaction before requesting its signature.

Connect#

  1. Sign in at route.click, choose a project, then open API keys → Testnet. Create or select a Testnet policy, set test-USDC amount and protocol-fee limits, and enable Circle in the provider-readiness step.
  2. In API keys, create a key bound to that policy (rt_test_…) with catalog:read, quotes:read, quotes:write, payments:read and payments:write. Keep the key on your backend. Testnet keys reject mainnet requests, including catalog reads.
  3. Use https://route.click/v1. Send these headers on every request:
Authorization: Bearer YOUR_ROUTE_KEY
Route-Network: testnet
Content-Type: application/json

Content-Type is needed for JSON mutations. Quote, payment and transaction creation, and mint renewal, also require a UUID Idempotency-Key saved with the corresponding request. A missing or non-UUID key returns 400 IDEMPOTENCY_REQUIRED and changes nothing. Reuse that key and identical input after a timeout.

The header selects the network and must match the key. Omitting Route-Network selects mainnet; a testnet key rejects that request with KEY_NETWORK_MISMATCH. Testnet assets, quotes, payments and transaction authorizations have separate records. Select Mainnet in API keys to create a separate rt_live_… key when moving to mainnet. Existing rt_sandbox_… keys retain their original access and restrictions.

Test tokens have no USD valuation. Leave USD key limits blank for a testnet-only key; USD-restricted keys are rejected. Provider, network, asset and recipient restrictions still apply, including restrictions inherited from the keys used earlier in the payment. New Testnet policies enforce test-USDC amount and protocol-fee limits. Read or update the assigned policy with GET /policy or PUT /policy using the corresponding scope. Legacy shared USD policies retain their existing behavior; USD values are not converted into test-token limits. Quote and payment records are accessible only to keys on the same policy. This flow validates connectivity and the payment lifecycle. It does not validate mainnet liquidity, production prices or saved USD and arrival policy limits.

See routing policies for policy versioning and key rotation.

Destination delivery#

Use destinationExecution: "managed" when preparing a new testnet payment. Route sends the attested CCTP mint function to OpenZeppelin Stellar Channels, which builds the Stellar transaction, submits it and sponsors its XLM fee. Your application signs only the Ethereum Sepolia approval and burn actions. It does not need a destination signing key or a relayer server.

Managed delivery is the default when no customer fee payer is supplied. Omit destinationFeePayer and destinationFeeBudget in this mode. Read GET /adapters to confirm that managed delivery is available. Route still verifies the exact destination transaction and native-USDC receipt before reporting completion; a Channels acceptance response alone is not settlement.

Prepare wallets#

  • Sender: an Ethereum Sepolia wallet with test ETH for gas and 1–10 native test USDC. Use the token identity returned by GET /assets and the Circle faucet.
  • Recipient: a Stellar testnet G account with the official test USDC trustline. The catalog returns the issuer and asset identity. Production Stellar accounts and trustlines do not carry over to testnet.
  • Optional customer-funded destination fee payer: a Stellar testnet G account able to sign the mint transaction. It can be the recipient. Fund it with Stellar Friendbot. This is needed only for destinationExecution: "customer". It needs at least 0.1 spendable test XLM after reserves; Route simulates the actual mint fee later. Managed delivery is the default and uses OpenZeppelin Channels sponsorship.

The source wallet builder supports ordinary Ethereum accounts. Customer-funded minting requires a Stellar G account with an authorized master signer; its wallet builder does not support multisig, muxed accounts or customer-supplied fee-bump envelopes. Managed Channels receipts can include a provider-sponsored fee-bump envelope, which Route verifies independently. Route never requests a private key.

Payment lifecycle#

  1. Discover: GET /assets and GET /adapters. Use the returned asset IDs and directional capabilities.
  2. Quote: POST /quotes with a new UUID Idempotency-Key:
{
  "sourceAssetId": "ethereum-sepolia:usdc",
  "destinationAssetId": "stellar-testnet:usdc",
  "amount": "1"
}

Standard and Fast are separate candidates. Fees come from Circle's sandbox API. The quote does not include complete gas or arrival estimates, so selected is null. Choose an available status: "quoted" candidate whose decision is not "excluded" explicitly; this is not a certified cheapest or fastest recommendation.

Quote responses still default to view=compact, retaining testnet limits, checks, amounts and expiry. Use POST /v1/quotes?view=full or GET /v1/quotes/{id}?view=full for the previous detailed response. Explicit best and list views are accepted, but testnet has no automatic recommendation: quote or recommendedCandidateId is null, candidates is empty for list, and nextAction is null. Read compact/full to choose Standard or Fast. Testnet payment creation still requires candidateId; quote-ID-only acceptance is mainnet-only. Changing views preserves the same saved quote; reads and identical idempotent replays do not request a new provider quote or create a payment. Invalid or repeated view values return 400 INVALID_QUOTE_VIEW. See quote response views.

Fast requires sufficient global Circle allowance. Route reads the allowance again during preparation; the quote does not reserve it. Circle's lastUpdated records the last allowance change, not an expiry, so an unchanged timestamp does not by itself make Fast unavailable. See Circle's allowance guide. If allowance is depleted during processing, Circle can complete the transfer at Standard speed.

  1. Create the payment: POST /payments with a different saved UUID Idempotency-Key:
{"mode":"prepare","quoteId":"RETURNED_QUOTE_ID","candidateId":"circle:standard"}
  1. Prepare the first action: POST /payments/{id}/prepare with the current payment version:
{
  "version": 1,
  "sender": "ETHEREUM_SEPOLIA_PUBLIC_ADDRESS",
  "recipient": "STELLAR_TESTNET_G_ACCOUNT",
  "destinationExecution": "managed",
  "maxFee": "0.01"
}

Route verifies the networks, contracts, token identity, trustline, balances, allowance and fee cap, and simulates the action. maxFee is the maximum CCTP fee in test USDC; test ETH gas is additional. Managed destination XLM fees are sponsored; customer mode pays its own mint fee. The per-action test ceilings for customer-signed actions are 0.001 test ETH and 1 test XLM. These are test guardrails, not production fee promises. New customer mint transactions include a 20% resource-fee margin plus 1,000 stroops in the signed XLM budget, subject to the same ceiling and spendable-balance check. The final submission check cannot exceed that budget.

  1. Get the signable transaction: POST /payments/{id}/transaction with a new UUID Idempotency-Key and {"version": CURRENT_PAYMENT_VERSION}. The response contains transaction, a payment_transaction object. Save its id as the authorization ID. Its nested transaction is the wallet action; that action's transaction is the exact EVM payload or Stellar {xdr, networkPassphrase, hash}. Display the network, payer and fees. Honor signingEnabled and expiresAt.
  2. Sign: have the specified wallet sign those exact bytes without broadcasting. EVM signatures must produce a serialized legacy transaction with the returned chain ID, nonce and gas terms. Stellar signatures must use the returned testnet passphrase and XDR. Send POST /payments/{id}/transaction/signature with {"authorizationId":"RETURNED_TRANSACTION_ID","signedTransaction":"SIGNED_BYTES"}. Route verifies the signer, exact transaction and fresh readiness. Signed bytes are immutable and are never included in read responses.
  3. Execute: POST /payments/{id}/execute with {"authorizationId":"RETURNED_TRANSACTION_ID"}. Route saves the submission attempt before broadcasting once. A 202 response means submission was accepted for tracking; it is not proof of settlement. Alternatively, an EVM wallet may use the explicit wallet submission handoff. Reserve it with /transaction/broadcast before sending, then report the hash at /transaction/broadcast-confirm; retain Route-Network: testnet on every call. If the wallet changed the nonce, fees or transaction type, confirmation returns 409 WALLET_TRANSACTION_REVIEW_REQUIRED; the transaction's lastError becomes that evidence object, paymentState is REQUIRES_ATTENTION and nextStep is review-required. Any other transaction the wallet signed that you report there, or to handoff recovery or cancellation, and that is neither the saved transaction nor the zero-value self-transfer cancelling the saved nonce, is recorded the same way with reason: "unclassified-sender-transaction", and one the wallet did not verifiably sign (a relayed or sponsored execution, an unsigned or unknown envelope, another key) with reason: "unclassified-transaction". Every reported hash is recorded for the payment before Route reads it; one the network does not show yet, or that it cannot read, stays recorded as reported-transaction and blocks until a later check with the same hash. A signed authorization without a handoff can report a hash at /transaction/broadcast-recover too: it is inspected and recorded, and nothing closes. So can an action from /prepare whose authorization was never requested: /transaction/broadcast-recover inspects and records the hash, /transaction/broadcast-confirm returns 409 WALLET_TRANSACTION_REVIEW_REQUIRED with the recorded report, and /transaction is then refused while it is open. A pending cancellation clears no report; only a finalized one does. Evidence on any action blocks the whole payment: /prepare and /transaction no longer prepare the next action (409 WALLET_TRANSACTION_REVIEW_REQUIRED), so a copy of the burn reported against the approval blocks the burn. NEAR-style operator review applies: testnet evidence has no attribution path. Once such evidence is on record (or the same payment sent or queued at another nonce), /execute and a new handoff of any action of the payment are refused, whatever the account nonces show, with the same 409 and the evidence in error.details before any network read; nothing is sent. An existing handoff is still returned, but with walletBroadcast.method: null and empty params, and the transaction reports walletBroadcastAvailable, signingEnabled and submissionEnabled as false (an approval keeps its handoff and params, but is not submitted). The evidence stays the lastError after a later submission outcome, which is nested under it as submission. While a handoff recovery or cancellation check is reading the network, /execute and a new handoff return 409 WALLET_EVIDENCE_CHECK_IN_PROGRESS with details.retryable: true; retry once it returns. Never resend; see wallet handoff recovery. Stellar mint signing continues to use the sign-only flow. The legacy /submission endpoint remains unavailable on the hosted service.
  4. Track: POST /payments/{id}/refresh with {}. Poll with backoff, starting at 15 seconds, then 30 and 60 seconds with jitter; honor a longer Retry-After. GET /payments/{id} reads the saved state without querying the chains.
  5. Continue: after one successful canonical approval block, status becomes approval_confirmed. approvalFinalized may still be false; approval confirmation does not mean the burn or mint is complete. Call /prepare with only {"version": CURRENT_PAYMENT_VERSION} for the burn, then repeat transaction → signature → execute → refresh. Review the fresh burn fees and expiry after approval; do not reuse the approval's review deadline or assume the initial quote stayed valid during confirmation. See source wallet readiness. After the burn is finalized and Circle supplies the matching attestation, Route submits the managed Stellar mint automatically. Continue /refresh while status is destination_pending; destinationExecution reports relay progress and customerSignatureRequired: false. For customer mode, select destinationExecution: "customer" and supply destinationFeePayer on first preparation; awaiting_destination then requires the same prepare/sign/execute flow with that wallet.
  6. Complete: Route marks the payment completed only after verifying the exact successful Stellar transaction and native USDC credit to the saved recipient. Stellar USDC uses seven decimals; the CCTP message uses six. The received amount accounts for the protocol fee.

Ethereum finality can take substantially longer than Circle's Fast attestation. This testnet flow waits for source finality before submitting or preparing the Stellar mint and does not represent production Fast-transfer speed.

Managed delivery sponsors mint transaction fees only. Recipient account activation and trustline reserves remain the recipient’s responsibility. Sponsorship is subject to Channels availability and fee quota. A relay that Channels reports failed or expired, or whose first response was lost before Route saved its relay ID (submission-unknown), makes the payment REQUIRES_ATTENTION with nextStep: "review-required". It requires reconciliation without another source burn. For a first relay with no saved relay ID, or one Channels reported failed or expired, destinationExecution.lastError.retryAllowed is true: after lastError.remintAfter (15 minutes after the attempt) Route re-mints the same saved call once, only if the CCTP nonce is still unused at a fresh Stellar testnet ledger (closed within two minutes) and the attestation and call still simulate without new authorization; the payment status stays destination_pending until then. There is no second automatic re-mint, and the earlier attempt is listed in destinationExecution.relayAttempts. Completion follows whichever recorded relay minted the message; a duplicate that failed because the nonce was used is not reported as destination_failed. If Channels resubmits the relay under a new hash, Route verifies the latest hash and lists every reported hash in destinationExecution.relayHashes.

New CCTP testnet burns and customer-signed Stellar mints have a five-minute review window; token approvals have 15 minutes. Read the returned expiresAt, which includes preparation/check time. Quote observations have their own shorter expiration. Saved actions keep their original deadline and are rechecked before signature acceptance and submission. A longer review does not guarantee unchanged fees or extend a signed transaction.

Responses and recovery#

GET /payments lists the key's testnet payments within its policy, newest first, limit per page (1–100, default 50). While hasMore is true, pass nextCursor as cursor with the same view to read older payments; when hasMore is false, no older payments remain. Cursors are bound to the project, policy, view and testnet. Testnet payments are never archived, so view=active and view=all return the same payments and view=archived returns none.

GET /payments/{id}/recovery uses the testnet key, Route-Network: testnet and payments:read. It returns the shared PaymentRecoveryReport shape for saved evidence, without a provider/chain refresh or managed mint. Its reservation groups have observed: false and empty items; this means those groups were not inspected, not that no reservations exist. The report includes the latest saved testnet action, while the transaction endpoint retains the detailed action history. Use SDK getRecovery() or the recovery guide.

GET /payments/{id}/transaction returns the current transaction and action history. Transaction states are ready, signed, submitting, submitted, submission-unknown and expired for a superseded action. The enclosing payment carries the verified settlement status. Testnet /prepare and /preparation return the testnet payment view with its current version; consult the separate testnet OpenAPI contract for response differences from mainnet.

  • Preserve quote, payment and transaction IDs and idempotency keys. A timeout does not authorize a replacement payment.
  • Repeating signature or execution requests for the same action does not create another broadcast. If submission is uncertain, refresh the existing payment using its saved hash. Route does not automatically retry, replace or raise fees.
  • An expired unsigned action may be prepared again with the current payment version if its original nonce and action kind are still valid. Its old authorization becomes unusable. A signed action cannot be replaced automatically, because Ethereum transaction bytes have no protocol expiry. Keep its references for reconciliation.
  • If a mint fails, a submitted action is uncertain, an external transaction consumes the nonce, or a message is minted outside Route, operator reconciliation is required. An expired, never-submitted customer Stellar mint may instead expose renew-mint; follow mint renewal. Do not start another burn as a recovery step.
  • Managed testnet payments are also checked automatically after their source burn is submitted. Customer-funded testnet minting remains client-driven. The dashboard's payment list currently shows mainnet records; use the testnet API for testnet history. Lifecycle changes enter the project outbox with payload.network: "testnet". Outbound webhook delivery still requires an approved HTTPS origin and is not enabled by selecting testnet.

JavaScript client#

The server-side Route client example accepts network: "testnet" and baseUrl: "https://route.click/v1". Use assets, adapters, quote, createDraft, prepareAction, prepareProtocolAction, saveTransactionSignature, executePayment, getPayment, listPayments and refreshPayment. Provider SDKs and provider credentials do not belong in the client.

For support, include the request ID, payment ID, network, endpoint and error code. Keep API keys, wallet secrets and signatures out of logs.

References#

Expired customer-signed mint#

If a customer-signed Stellar mint expires before Route records a submission, preserve the payment, authorization and burn hash and request recovery. When the current action offers renew-mint, use the renewal endpoint to prove its expiry and unused sequence, then sign the replacement mint. Do not create or fund another source payment to recover the same delivery. A fee margin in newly prepared envelopes does not change an existing signature or prove destination completion.

Operator reconciliation can complete the already-attested mint through Channels and verify the resulting receipt without changing the expired signature or repeating the source burn. This is separate from the public API: there is no self-service endpoint to switch an existing customer-mode payment to managed delivery. Choose managed delivery on first preparation for the standard sponsored flow.