# Execute a payment

Documentation revision **2026-10-07**. Mainnet contract **1.44.0-preview**; testnet contract **0.10.7-preview**.

Use `GET /v1/payments/{id}/next-action` to decide what your app should present next. The action table is the shared control flow; provider references define exact payloads. The same response envelope covers NEAR, direct protocol payments, CCTP testnet and supported composed workflows. It identifies the next step, the responsible actor and the existing Route operation to call.

This is an **observation of saved state**. Reading it does not refresh chain evidence, allocate a deposit, request a signature or submit funds. Every mutation still checks current wallet, policy, fee and receipt evidence. A displayed action may become stale before the user acts.

## Save a payment

Choose an unexpired candidate from [Compare routes](customer-api.md). Persist your business order, network, quote/candidate IDs and a UUID request key before creating the payment.

### Create the draft

Choose a candidate from the quote and send:

```json
{"mode":"prepare","quoteId":"QUOTE_UUID","candidateId":"EXACT_CANDIDATE_ID"}
```

`POST /payments` requires a separate UUID `Idempotency-Key`. It returns a draft with its `id`, `status`, `version`, saved quote evidence, events and `readiness`. Quotes with unknown checks may be saved for review; quotes with failed checks are rejected. Expired observations, changed provider settings and changed policies require a new quote. A saved draft can subsequently expose newly expired evidence or changed settings in `readiness.blockers`.

`GET /payments/{id}` retrieves that same record. `status` is currently `draft` or `cancelled`. `executionEnabled` reports whether the selected path is released; `readiness.ready` remains false for an unsigned draft. `POST /payments/{id}/cancel` takes `{ "version": 1 }`. Cancellation only cancels a draft before a provider preparation is reserved. Once a preparation exists, cancellation returns `409 PREPARATION_EXISTS`: cancelling a database record cannot revoke an allocated deposit address or undo a transfer.


## Action table

| `action.type` | Actor | Your app's behavior |
| --- | --- | --- |
| `prepare-payment` | `app` | Prepare or resume the existing payment with the indicated version. Supply `paymentIntent` when listed; collect sender, recipient, fee budget and any path-specific fields |
| `request-intent-approval` | `app` | Request the NEAR wallet intent challenge for the saved preparation |
| `sign-intent` | `wallet` | Review and sign the exact intent challenge, then confirm that proof. This does not transfer tokens |
| `prepare-transaction` | `app` | Persist a request key, then create the saved transaction authorization. An allowance transaction may precede the source transfer |
| `sign-transaction` | `wallet` | Independently review the exact payload and obtain a sign-only transaction signature, then save it for the indicated authorization |
| `submit-transaction` | `app` | After explicit payment authorization, submit the already saved signed transaction for that authorization |
| `confirm-wallet-broadcast` | `app` | Recover the hash from the wallet and confirm the reserved action. Never automatically repeat a wallet send |
| `renew-source` | `app` | NEAR: check an expired NEAR Stellar, Solana or Fogo source against current chain and provider evidence, renew the same deposit, then obtain fresh intent consent and a transaction signature. Mainnet Stellar-source protocol payments (Circle CCTP, LayerZero, Axelar): prove that the expired, never-signed burn or transfer has no receipt, its time bound has passed on a closed ledger and its wallet sequence is unused, then prepare a fresh action at the same sequence (with fresh terms) for a new signature; see [Stellar source renewal](recovery.md#renew-or-close-an-expired-stellar-burn-or-transfer). If the wallet already broadcast the saved transaction itself, renewal is refused and Route [tracks that transaction or records it for review](recovery.md#a-wallet-broadcast-the-saved-stellar-action-itself). Both use `POST /payments/{id}/transaction/renew` with `authorizationId` and a persisted UUID `Idempotency-Key`. |
| `renew-mint` | `app` | Check expiry and unused sequence, then prepare the same customer Stellar mint for a fresh signature. `POST /payments/{id}/transaction/renew` requires an `authorizationId` and UUID `Idempotency-Key`. |
| `renew-approval` | `app` | Mainnet Stellar CCTP only. Prove that the expired approval has no receipt, its time bound has passed on a closed ledger and its wallet sequence is unused. Then prepare a fresh action at the same sequence (one exact approval, or the burn when permission already covers it) for a new signature. `POST /payments/{id}/transaction/renew` requires an `authorizationId` and a persisted UUID `Idempotency-Key`. No funds move. See [approval renewal](recovery.md#renew-an-expired-stellar-cctp-approval). |
| `close-payment` | `app` | Mainnet Stellar-source protocol payments only, when the current expired Stellar action cannot be renewed: a signed, submitted or uncertain burn or transfer that was never included, or an approval or never-signed burn or transfer whose renewal cannot run because execution is paused or the payment's saved policy or provider settings changed. Prove the same expiry and unused sequence, then close the unfunded standalone payment: it becomes `cancelled`, and its wallet sequence, API-key holds and credit hold are released. `POST /payments/{id}/transaction/close` requires an `authorizationId` and a persisted UUID `Idempotency-Key`. Nothing is sent; a new payment needs a fresh quote. `closeEnabled` also marks an expired approval or unsigned transfer the app may choose to close instead of renewing. See [closing an expired Stellar payment](recovery.md#close-an-expired-stellar-payment). |
| `refresh-review` | `app` | Renew a supported review on the same payment/action; inspect the new checks and any renewed intent approval |
| `track` | `app` | Refresh the existing transfer, respecting backoff. This POST can progress an authorized managed destination mint. Do not sign or create a replacement payment |
| `continue-leg` | `app` | Read the active child's next-action endpoint and follow that transfer. Preserve the parent and child IDs |
| `advance` | `app` | Prepare the next composed transfer after the verified intermediate receipt. This can allocate the next provider deposit and needs a saved request key |
| `reconcile` | `app` | Reconcile a captured provider preparation using the same payment; it does not resend funds |
| `completed` | `none` | Show verified final delivery and stop the execution loop |
| `refunded` | `none` | Show verified final refund; do not count this as a delivered payment |
| `cancelled` | `none` | Close the unused payment. This does not imply a previously submitted transfer was reversed |
| `review-required` | `operator` | Stop automatic signing/submission, display the message and blockers, and follow the recovery playbook. It includes a transaction reported for any action of the payment that Route has not shown to be that action's own transaction or a recognized cancellation (`lastError.reason` `reported-transaction`, `unclassified-transaction`, `unclassified-sender-transaction`, `equivalent-execution`, or a wallet-altered copy): Route then submits, hands off and prepares no action of the payment. For a managed Stellar mint whose `destinationExecution.lastError.retryAllowed` is `true`, Route itself re-mints once after `lastError.remintAfter` if the CCTP message is still unminted; background tracking or `/refresh` continues it. Never repeat the source transfer |

For an `advance` action, apps can first call `previewAdvance` and explicitly review the returned bounds without allocating a child. Pass that review to `advance`, then obtain a separate wallet signature. See [continuation recovery](composed-payments.md#review-an-interrupted-route).

An `app` actor identifies responsibility, **not permission to run unattended**. Preparation can reserve a wallet slot or allocate provider instructions. Submission and composed advancement must remain within the authorization your user or account owner granted. See [unattended payments](unattended-payments.md).

## Read the snapshot

```http
GET /v1/payments/PAYMENT_UUID/next-action
Authorization: Bearer YOUR_ROUTE_KEY
Route-Network: testnet
```

Required scope: `payments:read`. Use the matching environment key. Testnet requires `Route-Network: testnet`; omit the header or use `mainnet` for mainnet. The result is the `payment_next_action` object directly, without a wrapper.

| Field | How to use it |
| --- | --- |
| `schemaVersion` | Currently `1`. Pause automatic execution on an unsupported version |
| `network`, `paymentId` | Check they match the saved business order and API environment |
| `activePaymentId` | The active child transfer for a composed workflow; otherwise the payment itself |
| `flow`, `adapter` | Identify the underlying payment family; detailed payloads and evidence remain specific to that family |
| `paymentVersion`, `preparationVersion` | Separate concurrency versions. Preserve both; do not interchange them |
| `authorizationId` | The saved transaction authorization, when one exists |
| `status` | The payment's normalized `paymentState`, such as `AWAITING_ACTION` or `REQUIRES_ATTENTION` (see [payment states](recovery.md#payment-states)); use `action.type` for the next step. Detailed saved states remain on the payment: draft `status`, composed `workflow.state` and testnet `status` |
| `observedAt`, `expiresAt` | Snapshot time and relevant review deadline. Neither reserves liquidity nor proves that signed bytes are unusable after expiry |
| `action.type`, `action.actor` | What should happen and whether it belongs to your app, a wallet, an operator, or nobody |
| `action.operation` | Existing endpoint, method, saved request fields, missing caller input and request-key requirement; null for terminal/operator-only steps |
| `action.payload` | The exact intent challenge or unsigned transaction to review for wallet steps; otherwise null. This is not an accepted signature |
| `action.walletRequirements` | Optional initial composed-preparation address fields and their networks. Collect customer-controlled intermediate wallets with ETH for their fees; see the [BTC → Base → Stellar example](composed-payments.md#prepare-the-route) |
| `checks`, `blockers`, `tracking` | Saved evidence in its existing flow-specific shape. An empty array does not establish fresh readiness |

Operation paths already begin with `/v1`. Resolve them against your trusted Route origin, not against `/v1` again. Never send credentials to a host supplied by an untrusted object. The SDK below uses explicit methods rather than a generic operation dispatcher.

For initial composed preparation, `walletRequirements` describes the addresses to collect within `paymentIntent`. The operation still has `requiredInput: ["paymentIntent"]`, `body: {"version": <saved version>}` and `payload: null`. Merge the collected intent fields into the top-level `/prepare` body; do not send a nested `paymentIntent` object. This metadata is optional and is not permission to fund or sign.

## Keep the three approvals separate

1. **Spending permission:** an on-chain token allowance transaction, only when required: ERC-20 approval on EVM, or native-USDC approval for Stellar-source CCTP. It has its own signature and receipt. EVM approval can continue after canonical block inclusion; Stellar approval requires the exact allowance event in a finalized ledger. Sufficient existing allowance skips this step; on Stellar, that allowance must also stay live through the burn’s expiry. An expired Stellar approval offers `renew-approval` rather than a review refresh. See [Stellar CCTP approval](protocol-execution.md#stellar-source-cctp-approval). The actual transfer keeps its normal finality requirements.
2. **Payment intent proof:** NEAR's wallet signature of the reviewed payment terms. It is separate from ERC-20 allowance and cannot itself broadcast the funding transaction.
3. **Transaction signature:** a signature over the exact saved transaction. Saving it is separate from Route submitting it.

Testnet transaction responses now expose `authorizationPrepared`. A prepared unsigned action can exist before the signing authorization is persisted. Until `authorizationPrepared: true`, `signingEnabled` is false and the next action is to prepare the transaction, not ask the wallet to sign. Read `GET /payments/{id}/transaction` after a lost creation response to recover that authorization.

## Use the JavaScript SDK

Download these files together into the same server-side directory:

- [route-sdk.mjs](examples/route-sdk.mjs) and [route-sdk.d.mts](examples/route-sdk.d.mts)
- [route-client.mjs](examples/route-client.mjs)
- [route-wallet-compatibility.mjs](examples/route-wallet-compatibility.mjs) and [route-wallet-compatibility.d.mts](examples/route-wallet-compatibility.d.mts)

```js
import {createRouteSDK} from './route-sdk.mjs';

const route = createRouteSDK({
  baseUrl: 'https://route.click/v1',
  apiKey: process.env.ROUTE_API_KEY,
  network: 'testnet',
});

const next = await route.getNextAction(savedPaymentId);
switch (next.action.actor) {
  case 'wallet':
    // Present the exact payload for independent review; do not auto-sign.
    showWalletReview(next);
    break;
  case 'operator':
    showRecovery(next.action.message, next.blockers);
    break;
  case 'none':
    showOutcome(next.action.type);
    break;
  case 'app':
    // Dispatch only an explicitly supported, authorized business operation.
    showNextStep(next.action.type, next.action.operation);
    break;
}
```

`savedPaymentId`, `showWalletReview`, `showRecovery`, `showOutcome` and `showNextStep` belong to your application. This snippet reads guidance; it is not a payment execution loop. The [complete testnet starter](payment-starter.md) demonstrates durable operation storage and restart recovery for its specified CCTP path.

For troubleshooting, `await route.getRecovery(savedPaymentId)` returns a typed, allowlisted report with the saved outcome, required evidence and reservation observations. It makes one GET and never refreshes or performs a recovery action. `INVALID_RECOVERY_REPORT` means the response failed version, identity, safe-field or nested-type validation; preserve the existing payment and request review. See the [recovery report contract](recovery.md#read-saved-recovery-evidence).

The SDK validates the normalized result returned by `getNextAction()` and throws `INVALID_NEXT_ACTION` for an unsupported or inconsistent snapshot. Stop automatic execution, retain the payment and read its detailed evidence; do not fall back to guessing an operation. Other mutation responses retain their existing flow-specific envelopes; follow the detailed contract when inspecting those results, or read a fresh next-action snapshot afterwards. The SDK performs no wallet calls, automatic mutations, retries or chain broadcasts on construction or on reads.

| Operation | SDK method |
| --- | --- |
| Prepare payment | `preparePayment(id, {version, ...intent}, key)`; mainnet requires a key, testnet uses the payment version without one |
| Request / confirm NEAR intent | `requestIntentApproval(id, version, key)` / `confirmIntentApproval(id, approvalId, signature)` |
| Prepare transaction | `prepareTransaction(id, {version, approvalId?}, key)` |
| Save exact signature / submit | `saveSignature(id, authorizationId, signedTransaction)` / `submit(id, authorizationId)` |
| Renew supported review | `refreshReview(id, {version?, authorizationId?}, key)` |
| Track / reconcile | `refresh(id, destinationHash?)`, or `refresh(id, {refundHash})` for a known SwapKit refund / `reconcile(id)` |
| Close an unfunded payment | `closePayment(id, authorizationId, key, cancellationHash?)`; the hash is required for an expired unsigned EVM action |
| Preview composition continuation | `previewAdvance(id)` |
| Advance composition | `advance(id, key, review?)` |

`paymentIntent` in `requiredInput` means the documented fields for that preparation; it is not a new nested API field. Merge those fields with `operation.body` without replacing the saved version, approval or authorization IDs. Mainnet NEAR review renewal uses the preparation version and optional authorization ID; protocol approval renewal uses its authorization ID. Use the supplied operation body and the underlying endpoint contract instead of guessing from a status name.

## Supply the preparation intent

`paymentIntent` is a reminder to supply the fields for the chosen flow, not a nested request property. Preserve `operation.body.version` and merge the applicable fields:

| Flow | Initial input and complete contract |
| --- | --- |
| NEAR mainnet | `sender`, `recipient`, `refundTo`, `refundDeadline`; minimum received and slippage inherit the saved quote when omitted. [NEAR preparation](customer-api.md#prepare-a-near-deposit-and-follow-its-outcome) |
| Direct protocol mainnet | `sender`, `recipient`. Circle → Stellar customer mode also needs `destinationFeePayer` and `destinationFeeBudget` in XLM; managed mode uses Route's configured sponsorship. [Protocol preparation](protocol-execution.md#2-prepare-the-payment) |
| CCTP testnet | `sender`, `recipient`, `maxFee` in test USDC and destination execution mode; customer mode needs its Stellar fee payer. Later actions reuse the saved intent. [Testnet lifecycle](testnet-api.md#payment-lifecycle) |
| Composed mainnet | Original sender, final recipient and customer-controlled `intermediate` wallet, with the path's minimum/slippage/refund terms. Resume with the original saved intent and request identity. [Composed preparation](composed-payments.md#prepare-the-route) |

## Check wallet compatibility early

The SDK also exports the pure `checkWalletCompatibility` helper. It checks your declared account type and capabilities; it performs no wallet or network calls and cannot prove that an extension will sign a real action.

```js
import {checkWalletCompatibility} from './route-sdk.mjs';

const result = checkWalletCompatibility({
  family: 'evm',
  flow: 'protocol',
  accountType: 'eoa',
  capabilities: {signOnly: true, evmLegacyTransaction: true},
});
// result.status: compatible | unsupported | unknown
// result.evidence: declared-capabilities-only
```

Populate capabilities from your reviewed wallet integration; do not hard-code `true` merely because an account connected. Missing capability information returns `unknown`. NEAR additionally needs the matching wallet intent proof capability. Inspect `requirements`, `reasons` and `nextStep`, and read the [wallet compatibility matrix](compatibility.md#wallet-compatibility-gate). EVM wallets that sign and send use the explicit handoff below.

## EVM wallet submission

Choose one submission mode for each saved action. The default is sign-only → `saveSignature` → `submit`, where Route broadcasts. Alternatively, a supported EVM wallet can submit the action and your backend reports its hash. This works for NEAR EVM deposits, Circle/Allbridge/LayerZero/Axelar EVM actions and CCTP Ethereum Sepolia actions. Quote-only adapters gain no execution coverage from this option.

1. Prepare the payment and transaction as usual. NEAR still requires its separate signed intent approval. Read the saved transaction and require `walletBroadcastAvailable: true`.
2. Before opening the wallet, call `POST /payments/{id}/transaction/broadcast` with `{"authorizationId":"SAVED_ACTION_ID"}`. Persist the response and your intent to open the wallet. The handoff reserves this action for wallet submission, reruns readiness and owner limits, and sends nothing to the network. Retrying this API request returns the same handoff. Once review evidence for any action of this payment is on record (a wallet-altered copy of a call, see step 4, the same payment sent or queued at another nonce, any other transaction reported for it that is neither the action's saved transaction nor a recognized cancellation, whoever signed it, or a reported hash Route could not read yet, from any recovery, confirmation, cancellation or close request), a **new** handoff returns `409 WALLET_TRANSACTION_REVIEW_REQUIRED` with that evidence in `error.details` before any network read: it reserves nothing and returns no `params`. An **existing** handoff is kept and replayed, and nothing is sent, but the response no longer presents the call to send: `walletBroadcast.method` is `null`, `params` is empty and `notice` asks for review. The saved transaction then reports `walletBroadcastAvailable`, `signingEnabled` and `submissionEnabled` as `false`; mainnet protocol actions and GET NEAR transaction add `actionBlocker.code: "WALLET_TRANSACTION_REVIEW_REQUIRED"`. Do not open the wallet; review the recorded hash. While a recovery, cancellation or close check for the action is still reading the network, a new handoff returns `409 WALLET_EVIDENCE_CHECK_IN_PROGRESS` with `details.retryable: true`; retry after that request returns. An altered spending approval moves no funds: its handoff is not refused and keeps its `params`, but Route never submits its saved signature. Any other recorded transaction refuses an approval's handoff too.
3. With explicit user approval, the app calls its wallet's `eth_sendTransaction` using `walletBroadcast.params`. Never open the wallet when `params` is empty. Verify the connected account/network first. Pass the supplied fields unchanged, including `type: "0x0"`, nonce, gas and gasPrice. If the wallet lowers the gas limit, Route can recognize that broadcast within the original gas ceiling. The gas price, sender, chain, nonce, recipient, value and calldata must still match exactly. Increased gas limits, fee-price editing, nonce rewriting, EIP-1559 conversion and smart-account wrapping are unsupported: Route never attaches such a transaction to the payment (see step 4).
4. Persist the returned hash immediately. Call `POST /payments/{id}/transaction/broadcast-confirm` with `{"authorizationId":"SAVED_ACTION_ID","transactionHash":"0x..."}`. Route checks the transaction's cryptographic signature, network, sender, nonce, amount, destination, calldata and fees. A missing RPC transaction returns `409 WALLET_TRANSACTION_NOT_FOUND`: retry **confirmation with the same hash**, never the wallet send. Route records every reported hash for the payment before it checks it, so until a retry verifies it the payment reads `review-required`; the wallet's exact original clears that record when it registers. If the wallet changed the nonce, fees or transaction type but kept the exact payment call (sender, contract, calldata, value and chain), confirmation returns `409 WALLET_TRANSACTION_REVIEW_REQUIRED`. Its `error.details` is the recorded evidence: `transactionHash`, the saved `nonce`, `observedNonce`, `observedType`, observed gas and fee fields, `checkedAt` and `automaticAttachmentAllowed: false`. Route keeps the evidence permanently and as the action's `lastError`, never attaches it, and keeps the saved nonce and every reservation. `paymentState` becomes `REQUIRES_ATTENTION`, `next-action` returns `review-required`, and mainnet emits `payment.requires_attention`. For a burn or transfer, that transaction may have moved the funds: a cancellation can no longer close the payment, and confirming the exact original later does not clear the review. An altered spending approval moves no funds; it is flagged the same way, but a finalized cancellation of its saved nonce can still close an otherwise unfunded payment. Do not send replacement funds; review the recorded hash. Other `WALLET_TRANSACTION_MISMATCH` or `WALLET_TRANSACTION_CONFLICT` responses also require reconciliation, never a new send.
5. Read `next-action` and continue normal receipt tracking. Spending approval still needs one successful canonical inclusion before the next action. Funding and destination settlement retain their existing finality checks.

The SDK exposes `beginWalletBroadcast(paymentId, authorizationId)` and `confirmWalletBroadcast(paymentId, authorizationId, transactionHash)`. These are backend API methods; neither connects to a wallet nor requests a signature. For `checkWalletCompatibility`, set `submissionMode: "wallet"` and explicitly declare `evmSignAndSend` and `evmLegacyTransaction`; NEAR also needs `evmTypedDataV4`.

After the handoff, `/transaction/signature`, `/execute` and review renewal are disabled for that action. Until confirmation, its state is `submission-unknown`. With no recorded review evidence, the normalized next action is `confirm-wallet-broadcast`. Once a hash is recorded as `reported-transaction`, `next-action` returns `review-required` with no automatic operation. Retry recovery with the same `authorizationId` and `transactionHash` used in the original report: payment-wide evidence may come from an earlier action, and a pending cancellation must be checked through recovery rather than payment confirmation. This means the wallet **may** have submitted; Route has not. API timeouts, wallet rejection, closing the tab or review expiry do not release the nonce or payment/credit holds. Recover the existing hash from wallet activity and confirm it. Use `recoverWalletBroadcast(paymentId, authorizationId, transactionHash?)` to check the saved nonce and recover an included original transaction. A verified finalized cancellation can close an unfunded standalone payment through `cancelWalletBroadcast`; see [wallet handoff recovery](recovery.md#wallet-handoff-with-a-missing-response). For a supported composed first source action, verified cancellation closes the unfunded parent and releases unused payment holds; see [first-transfer recovery](composed-payments.md#close-a-cancelled-first-transfer). For a supported composed intermediate action, the same endpoints can close only that cancelled attempt; then review continuation on the parent as described in [intermediate cancellation recovery](composed-payments.md#continue-after-an-intermediate-cancellation). For supported standalone payments whose signature is already saved, including backend signing, see [signed-source cancellation](recovery.md#close-a-cancelled-signed-source-transfer). Never create a replacement payment while the outcome remains uncertain.

Hash confirmation remains available after the review expires, API-key rotation or execution pause, provided the current key has access to the payment. This supports reconciliation of an existing send; it does not extend permission to make a late send. A verified hash records submission, not recipient delivery. Credits and processed USD totals retain their normal verified-outcome rules.

## Recovery, versions and request keys

Persist the order, environment, payment ID, active child ID, both versions, authorization ID, operation body and request key **before** mutating. The snapshot does not replace that durable record.

- Use `operation.body.version` as supplied. Mainnet `POST /prepare` uses `paymentVersion`; mainnet `POST /transaction` and NEAR intent approval use `preparationVersion`. Testnet preparation and transaction creation both use `paymentVersion`. They are not interchangeable counters.
- When `idempotencyKeyRequired` is true, save a new UUID for a genuinely new operation. Reuse that key and the identical body if its response is lost. A fresh snapshot does not permit changing an in-flight operation's body under its old key.
- Signature acceptance and execute bind to an existing authorization. Preserve the exact signed bytes privately. A calculated `sourceHash` does not prove broadcast.
- `submitting`, `submitted` and `submission-unknown` require tracking the same transfer. Do not interpret review expiry as permission to create another deposit or broadcast from the wallet.
- On `VERSION_CONFLICT`, reread payment state and recover existing progress. On `401`/`403`, correct key access without replacing the payment. Honor `Retry-After` on `429`; use bounded 15/30/60-second polling with jitter when tracking. Unknown actions or schemas require a pause.

Use the [recovery playbook](recovery.md) for lost responses, failed readiness checks, expired signatures and operator escalation. Garden is paused: new quotes and execution are unavailable, including Garden-backed SwapKit paths. SwapKit executes supported simple EVM and native Bitcoin deposits; consult the candidate’s exact execution coverage. Testnet remains the documented CCTP path. Auto-route selects among policy-compatible paths and uses its separate 4× pricing; it does not authorize wallet signatures or turn this snapshot into an autonomous payer.
## Approval is specific to the selected route

An EVM token allowance belongs to a particular **source chain, token contract, owner wallet and spender contract**. It must cover the required amount. There is no blanket “wallet approved for Route” state. Changing the route, token, sender, chain or amount can change the required allowance. See the [ERC-20 allowance specification](https://eips.ethereum.org/EIPS/eip-20#allowance).

Use the exact approval action returned by Route. Do not guess the spender, use a destination address as the spender, or approve every adapter in advance. Route checks the current allowance, skips unnecessary approvals and requests the exact required amount. If an insufficient nonzero allowance needs resetting, `approval-reset` and `approval` are separate transactions with separate signatures and confirmation waits. Route does not request unlimited approval.

The source wallet also needs enough native currency for approval and transfer fees. An approval grants spending permission; it does not send the cross-chain payment or prove recipient delivery. It still costs network fees.

## Confirmation and reorganizations

Across every supported EVM chain, for mainnet `approval` and `approval-reset`, `canContinue: true` means the verified permission transaction is usable for requesting the next action. It is independent of `receipt.finalized`. The preparation returns `nextAction: "prepare-transaction"`. Do not use the action's submitted state or a wallet's hash alone as the continuation signal.

Route rechecks unfinalized prerequisite approvals before building the next action, saving its signature and submitting it. It also rechecks the current allowance and simulates the exact action. If an approval disappears from the canonical chain, continuation is blocked with `APPROVAL_CONFIRMATION_PENDING`; call `/refresh` and track the existing hash. An approval event mismatch or unavailable network verification also blocks progress. Route does not automatically replace or resend transactions. If the transfer was already submitted, track that transfer; do not initiate another payment.

Approval gas is included in subsequent fee checks even before finality. This rule changes only token permissions: burn, bridge and destination-settlement verification retain their finality requirements. Wallets with sufficient existing allowance skip approval as before.

## Not every transfer needs a token allowance

NEAR's current EVM deposit builder transfers tokens directly from the sender to the provider deposit address; it does not use an ERC-20 spender allowance. Its [wallet intent approval](wallet-approvals.md) is an off-chain confirmation of payment details, followed by a separate transaction signature. Do not add a token-approval transaction to that flow.

Native-asset transfers and the supported Stellar, Bitcoin and Solana-family flows have their own wallet checks and signatures. Do not impose an EVM allowance requirement on them. Stellar recipients may need the exact asset trustline; source wallets still need sufficient asset balance and network fees. For composed routes, repeat readiness checks for each leg, including the intermediate wallet's approval and gas needs.

Hosted CCTP testnet uses a different continuation endpoint: after `approval_confirmed`, call `/prepare` with the current **payment version** for the burn, then request its transaction. Follow the [testnet lifecycle](testnet-api.md); do not substitute the mainnet preparation-version sequence.


## Mainnet protocol sequence

After an approval returns `canContinue: true`, use the current preparation version and a new saved action key to request the next protocol transaction. Route refreshes terms before the transfer review. [Protocol execution](protocol-execution.md#3-request-sign-and-submit-each-action) contains the exact request/sign/submit sequence.

## Expiry and pending transactions

Honor the action's current `expiresAt`, signing and submission flags. Submitted or uncertain transactions keep their existing payment and hash; review expiry never authorizes another deposit. Use [Track and recover](recovery.md) for renewal, polling and unresolved outcomes.

## Saved SwapKit quote review

A `review-required` response can identify a saved provider allocation with changed terms. NEAR EVM- or Aptos-source payments with `quoteReviewAvailable: true` support [explicit review of the saved allocation](payment-transactions.md). Acceptance sets a new standalone minimum and cost ceiling, or retains the original composed final minimum, before separate wallet consent.

For SwapKit: Read [`GET /payments/{id}/quote-review`](providers.md#reviewing-changed-swapkit-terms), show the revised receipt and fees, and obtain explicit payer acceptance before posting the review hash and minimum. This is an interactive decision, not an automatic retry. After acceptance, `next-action` can return `prepare-payment` for the same channel while its deadline remains valid. A validated SwapKit channel whose deposit instructions were not attached, for example after a failed wallet check, also returns `prepare-payment` with the same wallets until `providerAllocation.expiresAt`. After that time it returns `review-required`.

### Expired Stellar source actions

`renew-source` also applies to an eligible unsigned Stellar protocol burn or transfer. A signed expired Stellar source may instead offer `close-payment`, an explicit application action at `POST /payments/{id}/transaction/close` with `authorizationId` and a UUID idempotency key. It verifies non-execution and closes the unfunded standalone payment, preserving evidence and releasing unused holds. Never infer permission from expiry alone. See [Stellar protocol recovery](recovery.md#recover-an-expired-stellar-protocol-source).
