# 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](protocol-execution.md) for supported production paths.

Use the [complete payment starter](payment-starter.md) for a runnable backend example with persistent state, sign-only transaction review and restart recovery. Use the [normalized next-action API and SDK](next-actions.md) to follow saved payment state, and the [recovery playbook](recovery.md) 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](https://route.click/auth/), 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:

```http
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](routing-policies.md) 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](https://docs.openzeppelin.com/relayer/1.5.x/guides/stellar-channels-guide), 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](https://faucet.circle.com/).
- **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](https://friendbot.stellar.org/). 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`:

```json
{
  "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](customer-api.md#request-a-quote).

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](https://developers.circle.com/cctp/howtos/get-fast-transfer-allowance). If allowance is depleted during processing, Circle can [complete the transfer at Standard speed](https://help.circle.com/support/en/what-is-the-fast-transfer-allowance?id=kb_article_view&sysparm_article=KB0011087).

3. **Create the payment:** `POST /payments` with a different saved UUID `Idempotency-Key`:

```json
{"mode":"prepare","quoteId":"RETURNED_QUOTE_ID","candidateId":"circle:standard"}
```

4. **Prepare the first action:** `POST /payments/{id}/prepare` with the current payment version:

```json
{
  "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.

5. **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`.
6. **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.
7. **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](next-actions.md#evm-wallet-submission). 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](recovery.md#wallet-handoff-with-a-missing-response). Stellar mint signing continues to use the sign-only flow. The legacy `/submission` endpoint remains unavailable on the hosted service.
8. **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.
9. **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](source-wallet-readiness.md). 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.
10. **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](recovery.md#read-saved-recovery-evidence).

`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](testnet-openapi.json) 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](recovery.md#renew-an-expired-customer-stellar-mint). 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](/docs/examples/route-client.mjs) 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

- [Circle Stellar contracts](https://developers.circle.com/cctp/references/stellar-contracts)
- [Circle Stellar recipient encoding and minting](https://developers.circle.com/cctp/references/stellar)
- [Circle USDC contract identities](https://developers.circle.com/stablecoins/usdc-contract-addresses)

### 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](recovery.md#renew-an-expired-customer-stellar-mint) 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.
