> ## Documentation Index
> Fetch the complete documentation index at: https://docs.atum.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Using the CLI

> Send your first testnet payment through the Payment Gateway CLI.

## Overview

Send a cross-rail payment using the Payment Gateway CLI. The CLI wraps the same prepare → sign → submit flow you would use with the SDK.

<Note>
  This guide walks through one example corridor — USDC on Arbitrum Sepolia → USDC on Celo Sepolia. Atum supports many source and destination combinations across Arbitrum, Base, Solana, Tron, and more. See [Supported networks](/get-started/reference/supported-networks) for the full list.
</Note>

<Steps>
  <Step title="Approve token spend">One-time setup for your wallet on the source chain — not before every payment.</Step>
  <Step title="Run send-payment">The CLI builds, signs, and submits your payment in one command.</Step>
  <Step title="Gateway routes your payment to a settlement provider">Settlement providers compete to fulfill your payment — no action needed from you.</Step>
  <Step title="Funds move across both rails">Settlement completes. Poll `/status` to confirm.</Step>
</Steps>

<Note>
  New to tokenized currency payments? Read [Atum infrastructure](/get-started/architecture/payment-rails-primer) first — about five minutes.
</Note>

You do not need to run any Atum backend services, only the hosted testnet gateway and your wallet.

For SDK integration in your own app, see [Build with the SDK](/get-started/start-building/build-with-the-sdk).

## Prerequisites

The following is required to use Atum to send a payment:

<AccordionGroup>
  <Accordion title="Node.js 20+ and npm">
    ```bash theme={null}
    brew install node
    ```

    Verify (npm is included with Node.js):

    ```bash theme={null}
    node -v   # v20.x.x or newer
    npm -v
    ```
  </Accordion>

  <Accordion title="Access to Atum's Payment Gateway">
    Confirm you can reach the hosted testnet API from your machine:

    ```bash theme={null}
    curl -sS https://payment-gw.production-testnet.atum.xyz/v1/defaults | python3 -m json.tool
    ```

    A successful check returns HTTP **200** and one entry per chain the gateway serves, keyed by CAIP-2 id:

    ```json theme={null}
    {
      "eip155:421614": {
        "chain_id": "eip155:421614",
        "quote_selector": "0xd0f080E23F95571D26eEb1FAD24a5F3d66835195",
        "escrow_contract": "0x0F875601504C9179562506AFa34b2D084268869b",
        "fulfillment_proxy": "0x1F1F8FA642bc5F530ba37F1Db3a656E9eB8BaFeb",
        "fulfillment_verifier": {
          "account": "0x27050EE47befC43F4dF7807a7AA8073039e5fa89",
          "endpoint": "https://veri-fill.production-testnet.atum.xyz"
        }
      },
      "eip155:11142220": { "chain_id": "eip155:11142220", "…": "…" }
    }
    ```

    This tells you two things at once: the gateway is up, and **which chains are enabled** — not which source→destination pairs you can use. A chain that isn't fully configured is left out of this listing, so what comes back is what you can actually pay on. Fetch these addresses rather than hardcoding them; they change when a chain's contracts are redeployed. A non-200 is a gateway-side answer, not a network problem: **404** means it resolved no chains and **503** means a dependency is down — in both cases the gateway is up and you have nothing to fix locally.

    The Payment Gateway is an **HTTP API**, not a blockchain network.
  </Accordion>

  <Accordion title="A funded testnet wallet">
    This example uses an EVM wallet funded on Arbitrum Sepolia. Any EVM-compatible wallet works (MetaMask, Coinbase Wallet, Rainbow, etc.).

    You will need:

    * Testnet ETH for the one-time token approval on the source chain ([settlement gas](/settle-payments/integration-guide#4-fund-your-settler-wallets) is the settlement provider's)
    * A small amount of testnet USDC on the source chain

    You can use the same `0x` address for sender and receiver. See [Testnet wallet setup](/get-started/reference/setup-testnet-wallet) if you need help getting funded.
  </Accordion>
</AccordionGroup>

## 1. Install the CLI

```bash theme={null}
npm install -g @atumlabs/payment-gateway-client
send-payment --help
```

This installs five commands: `send-payment`, `payment-status`, `request-quote`, `quote-status` and `approve-permit2`. Skip the `-g` and use `npx send-payment …` if you would rather not install globally.

## 2. Configure addresses and key

Create `.env` in the directory you'll run the commands from.

You will need your wallet's private key and address (in this example, we use the same address for sender and receiver):

Example (replace with your own values):

```
PRIVATE_KEY=0xYOUR_PRIVATE_KEY
SENDER=0xYOUR_SENDER_ADDRESS
RECEIVER=0xYOUR_RECEIVER_ADDRESS
```

<AccordionGroup>
  <Accordion title="Using a MetaMask wallet?">
    Your MetaMask **address and private key belong to the account**, not to a network.

    The network dropdown (Ethereum, Arbitrum Sepolia, etc.) only changes which chain you are **viewing** — not your credentials.

    Balances change by network; your `0x` address and key do not.

    #### Copy your address

    Click the **copy icon** next to your default Metamask address.

    Paste that value into `SENDER` and `RECEIVER` in your `.env`

    #### Export your private key

    1. In MetaMask, click the **three dots (⋮)** next to your account name.
    2. Click **Account details**.
    3. Copy your account private key (it's a long hex string starting with `0x`).
    4. Enter your MetaMask password and confirm.

    Paste it into `PRIVATE_KEY` in your `.env`.
  </Accordion>
</AccordionGroup>

<Warning>
  Add `.env` to `.gitignore` and **never commit** this file.
</Warning>

Load the variables before running scripts to avoid putting the key in shell history:

```bash theme={null}
set -a && source .env && set +a
```

## 3. Approve USDC spend

<Note>This step is required for **EVM and Tron** source chains. Solana uses a different token model and does not need this approval.</Note>

Before Atum can move funds from your wallet during settlement, you must **approve spending once** on the source chain. This is separate from signing each payment — do it one time per wallet and token, then send as many test payments as you like.

Without this step, the gateway may accept your payment request, but on-chain settlement will fail.

<AccordionGroup>
  <Accordion title="Technical name: Permit2">
    Atum uses the standard [Permit2](https://github.com/Uniswap/permit2) contract on EVM chains — the same approval pattern many DeFi apps use. The network pulls your USDC into a secure hold on the source chain only after you sign each payment.

    Details: [Permit2 and approvals](/get-started/concepts/permit2-and-approvals).
  </Accordion>
</AccordionGroup>

The package ships a command for this. It reads the current allowance first, so an already-approved wallet never has to produce a key to learn there was nothing to do:

```bash theme={null}
set -a && source .env && set +a

approve-permit2 $SENDER eip155:421614/erc20:0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d \
  --rpc-url https://sepolia-rollup.arbitrum.io/rpc
```

It takes the same CAIP-19 asset string as `send-payment`. The private key comes from `PRIVATE_KEY` or an interactive prompt and is never accepted as a flag, which would put it in shell history and in the process list. Add `--check` to report whether an approval is needed without sending anything or needing a key at all.

An approval is unlimited by default, so later payments need no further transaction. `--amount` bounds it instead, at the cost of renewing it as Permit2 consumes it.

<Note>
  This is the one command that spends gas. Everything else the CLI does builds and signs offline.

  If the transaction is broadcast but not confirmed before the timeout, the command exits non-zero and prints `{"unconfirmed": true, "txHash": …}`. That is not a revert — the approval may still land. Look that transaction up before re-running, rather than sending a second approval.
</Note>

### Approving by hand

You don't have to use Atum's command. The approval is an ordinary ERC-20 `approve` of the Permit2 contract — nothing Atum-specific — so any tool that can call it works.

<AccordionGroup>
  <Accordion title="From a block explorer or Remix">
    | Tool                                          | Link                                                                                                                                |
    | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
    | **Arbiscan** (Write as Proxy on testnet USDC) | [USDC contract → Write as Proxy](https://sepolia.arbiscan.io/address/0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d#writeProxyContract) |
    | **Remix**                                     | [remix.ethereum.org](https://remix.ethereum.org) — connect your wallet on the source chain and call `approve` on the USDC contract  |

    Use **spender** `0x000000000022D473030F116dDEE9F6B43aC78BA3` (the standard Permit2 contract) and an **amount** in 6-decimal units (for example `1000000000` = 1000 USDC). Circle testnet USDC is a proxy — on Arbiscan use **Write as Proxy**, not **Write Contract**.
  </Accordion>

  <Accordion title="From your own script">
    Save as `accept-permit2.ts` and run it once with `npx tsx accept-permit2.ts`:

    ```typescript theme={null}
    import { ethers } from 'ethers';

    async function main() {
      const privateKey = process.env.PRIVATE_KEY;
      if (!privateKey) throw new Error('Set PRIVATE_KEY in .env');

      const wallet = new ethers.Wallet(
        privateKey,
        new ethers.JsonRpcProvider('https://sepolia-rollup.arbitrum.io/rpc'),
      );
      const usdc = new ethers.Contract(
        '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d',
        ['function approve(address spender, uint256 amount) returns (bool)'],
        wallet,
      );

      const tx = await usdc.approve('0x000000000022D473030F116dDEE9F6B43aC78BA3', 1_000_000_000n);
      await tx.wait();
      console.log(`Spending approval confirmed: ${tx.hash}`);
    }

    main().catch((err) => { console.error(err); process.exit(1); });
    ```

    Three things are going on: connect to the **source** chain, call **`approve`** on the USDC contract, and name **Permit2** as the spender allowed to pull USDC when a payment settles. No money moves — it is a permission slip, and you still sign each payment separately. `1_000_000_000n` is a 1000 USDC ceiling for test runs.
  </Accordion>
</AccordionGroup>

For token approval details, see [Permit2 and approvals](/get-started/concepts/permit2-and-approvals).

## 4. Send your first payment

With USDC spend approved, you are ready to submit a payment.

Run the following to load your .env file first:

```bash theme={null}
set -a && source .env && set +a
```

Once your `.env` is loaded, run:

```shell title="send-payment" theme={null}
send-payment \
  --gateway https://payment-gw.production-testnet.atum.xyz \
  $SENDER \
  10000 \
  eip155:421614/erc20:0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d \
  $RECEIVER \
  eip155:11142220/erc20:0x01C5C0122039549AD1493B8220cABEdD739BC44E
```

<AccordionGroup>
  <Accordion title="What's in this command?">
    `send-payment` takes positional arguments in this order: **sender**, **amount**, **source asset**, **receiver**, **destination asset**.

    | Argument          | Example                                          | Definition                                                                                                                       |
    | ----------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
    | `--gateway`       | `https://payment-gw.production-testnet.atum.xyz` | Payment Gateway base URL. Fixed to the hosted testnet for this guide.                                                            |
    | `--request-id`    | *(omitted here)*                                 | Optional. Your idempotency key — see the tip below.                                                                              |
    | Sender            | `$SENDER`                                        | Depositor wallet — signs the payment and holds source funds. Set in `.env`.                                                      |
    | Amount            | `10000`                                          | Payment size in atomic units (6 decimals for USDC). `10000` = 0.01 USDC. Change as needed — see **Amount (atomic units)** below. |
    | Source asset      | `eip155:421614/erc20:0x75faf1…`                  | **Asset ID** for USDC on Arbitrum Sepolia (source chain in this example).                                                        |
    | Receiver          | `$RECEIVER`                                      | Account that receives funds on the destination chain. Can be the same `0x` as the sender.                                        |
    | Destination asset | `eip155:11142220/erc20:0x01C5C012…`              | **Asset ID** for USDC on Celo Sepolia (destination chain in this example).                                                       |

    ### What are the `eip155:…` strings?

    Those are **asset IDs** — one string that identifies an exact token on an exact chain. The CLI does not accept symbols like `USDC`; it needs the full ID. Different corridors use different asset IDs — copy them from [Supported assets](/get-started/reference/supported-assets).

    ```
    eip155:{chainId}/erc20:{tokenContractAddress}
    ```

    | Piece                 | Source asset (this example) | Destination asset (this example) |
    | --------------------- | --------------------------- | -------------------------------- |
    | `eip155`              | Prefix for EVM chains       | Same                             |
    | `421614` / `11142220` | Arbitrum Sepolia chain ID   | Celo Sepolia chain ID            |
    | `erc20:0x…`           | USDC on Arbitrum Sepolia    | USDC on Celo Sepolia             |

    <AccordionGroup>
      <Accordion title="Technical name: CAIP-19">
        Asset IDs follow the [CAIP-19](https://chainagnostic.org/CAIPs/caip-19) standard. Chain prefixes use [CAIP-2](https://chainagnostic.org/CAIPs/caip-2).

        Full catalog: [Supported assets](/get-started/reference/supported-assets). Concept page: [Corridors and assets](/get-started/concepts/corridors-and-assets).
      </Accordion>
    </AccordionGroup>

    ### Why are the asset IDs already filled in?

    This quickstart pins one route so you can focus on the payment flow — not hunting token addresses at runtime.

    * **`GET /v1/defaults`** lists which **chains** are enabled and returns their on-chain contract addresses — not which tokens or asset IDs to use, and not "USDC on Arbitrum Sepolia."
    * Token contract addresses come from [Supported assets](/get-started/reference/supported-assets).

    In production, store supported source/destination asset pairs in your own config. For other testnet routes, copy asset IDs from that catalog.
  </Accordion>
</AccordionGroup>

<Tip>
  The command above omits `--request-id`, so `send-payment` generates a new random one (`pmt-gw-cli-<timestamp>-<random>`) every time you run it — each run is a **new** payment.

  If you re-run the exact same command to retry a submission that failed or timed out, pass the **same** `--request-id` explicitly so the gateway recognizes it as the same payment instead of charging a second one. See [Idempotency](/payment-protocols/idempotency).
</Tip>

<AccordionGroup>
  <Accordion title="Amount (atomic units)">
    The third argument to `send-payment` is **not** dollars — it is USDC in **6-decimal atomic units** (smallest on-chain increment):

    | You send  | CLI value |
    | --------- | --------- |
    | 0.01 USDC | `10000`   |
    | 0.10 USDC | `100000`  |
    | 1.00 USDC | `1000000` |

    Use a **small test amount** (for example `10000`) to conserve testnet USDC. If settlement fails with `NO_QUOTES_RECEIVED`, verify asset IDs against [Supported assets](/get-started/reference/supported-assets) before retrying with a larger amount.
  </Accordion>
</AccordionGroup>

Your terminal should output two streams back-to-back:

| Stream     | Content                                              |
| ---------- | ---------------------------------------------------- |
| **stderr** | Signed `PaymentRequest` JSON (for debugging)         |
| **stdout** | `PaymentResponse` JSON — copy `payment_id` from here |

<Tabs>
  <Tab title="stderr (PaymentRequest)">
    The client logs the signed request with `console.error` before submitting:

    ```json theme={null}
    {
      "version": "1.0",
      "source": {
        "asset_identifier": "eip155:421614/erc20:0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d",
        "account": "0xYOUR_SENDER_ADDRESS"
      },
      "fulfillment_amount": "100000",
      "sender_auth": { "message_scheme": "EVM_PERMIT2" }
    }
    ```

    Followed by the recovery line. Copy the `request_id` from here if you need to retry:

    ```
    request_id: pmt-gw-cli-1789559493-a1b2c3  (re-run with --request-id pmt-gw-cli-1789559493-a1b2c3 to retry this same payment)
    ```
  </Tab>

  <Tab title="stdout (PaymentResponse)">
    The gateway response is printed with `console.log`:

    ```json theme={null}
    {
      "payment_id": "0x...",
      "status": "pending",
      "idempotent_replay": false
    }
    ```

    A `payment_id` means the gateway **accepted** your signed request — not that settlement finished. `idempotent_replay: false` means this call just created the payment; `true` would mean you got back a payment an earlier retry already created — see [Idempotency](/payment-protocols/idempotency).
  </Tab>
</Tabs>

Continue to [Check payment status](#5-check-payment-status) with the `payment_id` from **stdout**.

By default, the CLI **does not poll** after submit — `send-payment` may pause for up to **\~30 seconds** while the gateway finds a route and price before returning, then prints the `PaymentResponse` above and exits.

Pass `--wait` if you'd rather have the CLI poll for you: `send-payment` then blocks on `/status` itself and prints the payment JSON when settlement reaches `completed` or `failed`, or when `--wait-seconds` expires. Add `--wait-seconds <n>` to change the default 60-second cap — giving up does not affect the payment itself, it's a convenience bound only.

The exit code distinguishes the three endings, so don't branch on "zero or not" — a wait that runs out its budget is not a failure:

| Code | Meaning                                                        | What to do                                                         |
| ---- | -------------------------------------------------------------- | ------------------------------------------------------------------ |
| `0`  | Settled.                                                       | Nothing.                                                           |
| `2`  | Accepted, still settling.                                      | Re-run with the **same** `--request-id`, or poll `/status`.        |
| `3`  | Terminal failure.                                              | A fresh attempt needs a **new** `--request-id`.                    |
| `1`  | No outcome obtained — rejected, unreachable, or bad arguments. | Treat as *unknown*, not "nothing happened": recover like code `2`. |

### Review before you send

`send-payment` in one shot builds, signs and submits. You can also split it in two and read the signed request first:

```bash theme={null}
# Build and sign, print the request, submit nothing:
send-payment --prepare-only \
  --gateway https://payment-gw.production-testnet.atum.xyz \
  $SENDER \
  10000 \
  eip155:421614/erc20:0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d \
  $RECEIVER \
  eip155:11142220/erc20:0x01C5C0122039549AD1493B8220cABEdD739BC44E \
  > payment.json

# Send one that was prepared earlier:
send-payment --submit payment.json

# Or skip the file — "-" reads stdin, so the two pipe together:
send-payment --prepare-only \
  --gateway https://payment-gw.production-testnet.atum.xyz \
  $SENDER \
  10000 \
  eip155:421614/erc20:0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d \
  $RECEIVER \
  eip155:11142220/erc20:0x01C5C0122039549AD1493B8220cABEdD739BC44E \
  | send-payment --submit -
```

What `--prepare-only` prints is byte-for-byte what that run would have POSTed, so it can be reviewed or approved before any money moves.

<Warning>
  Treat the file like a signed cheque, not a draft — it is a signed authorization and `--submit` needs no private key. Delete it once the payment is sent or abandoned.

  A prepared request is submittable only while its quote window is open, and the default is **10 seconds**. Widen it with `--quote-deadline-seconds` if you intend to submit later, but by tens of seconds rather than minutes: that same window is how long the gateway waits before selecting a settlement quote, and quotes expire on their own, so an over-long window ends in a terminal `QUOTES_EXPIRED`.
</Warning>

`--prepare-only` still calls the gateway — that is where the corridor addresses come from — so it is not an offline signing mode. The exit codes above describe a *submitted* payment, so they don't apply to a `--prepare-only` run, where `0` means the request was built and signed and no payment exists. Don't chain a payment-conditional step off one.

### The other commands

| Command                         | What it does                                                                                                                                                                           |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `payment-status <paymentId>`    | Read a payment's current status. Same data as the `/status` endpoint in step 5.                                                                                                        |
| `request-quote`                 | Open a quote-request batch and collect price quotes for a corridor, without committing to a payment. Takes its own `--request-id` — see [Idempotency](/payment-protocols/idempotency). |
| `quote-status <quoteRequestId>` | Read a batch and the quotes collected for it so far.                                                                                                                                   |
| `approve-permit2`               | The one-time token approval from [step 3](#3-approve-usdc-spend).                                                                                                                      |

Every command takes `--gateway`, and both `send-payment` and `request-quote` take `--request-id` (or `--request-id-prefix` to change the generated one's prefix). Run any of them with `--help` for the full flag list.

## 5. Check payment status

This CLI flow talks to the Payment Gateway **directly**, so you poll `/status` after submit. That is the right pattern here.

For [x402](/payment-protocols/x402/overview) / [MPP](/payment-protocols/mpp/overview) HTTP payments, prefer [re-attempting the same purchase](/payment-protocols/idempotency) when settlement is still running; keep `/status` for dashboards and out-of-band reconciliation.

Copy `payment_id` from **stdout** in step 4, then poll every few seconds:

```bash theme={null}
PAYMENT_ID=0x...   # paste from send-payment stdout

curl -sS "https://payment-gw.production-testnet.atum.xyz/v1/payments/${PAYMENT_ID}/status" | python3 -m json.tool
```

Optional — event timeline for debugging:

```bash theme={null}
curl -sS "https://payment-gw.production-testnet.atum.xyz/v1/payments/${PAYMENT_ID}/timeline" | python3 -m json.tool
```

The `/status` response tells you whether settlement finished. Poll every few seconds until `status` is `completed` or `failed`, or until you are confident it is stuck.

| `status`    | Meaning                                                |
| ----------- | ------------------------------------------------------ |
| `pending`   | Accepted, settlement still running — keep polling      |
| `completed` | Payment fulfilled — settlement confirmed on both rails |
| `failed`    | Settlement did not complete — check `error.code`       |

<Note>
  `completed` and `failed` are the only two statuses that end a payment. Treat anything else as still in flight and keep polling — in particular, do not stop on a status you do not recognize, because giving up on a live payment is how a second charge happens.

  The client package declares one more, `finalizing`, for a payment whose delivery transaction is on chain but still gathering confirmations. Nothing emits it yet, and it is **not** terminal when it arrives. If you write an exhaustive `switch` on the SDK's `PaymentStatus` type, you need a branch for it.
</Note>

A `payment_id` from step 4 only proves **submit** succeeded. This step confirms **settlement**.

### Completed

USDC moved across both rails. The full response includes rail IDs, the source block number, and both tx hashes:

```json theme={null}
{
  "payment_id": "0x8329...c2c4",
  "status": "completed",
  "updated_at": "2026-07-06T23:38:30.785875Z",
  "fulfillment_confirmation": {
    "payment_id": "0x8329...c2c4",
    "request_id": "pmt-gw-cli-1783381062465-o9gj9c2",
    "source_chain_id": "eip155:421614",
    "source_tx_hash": "0x38d3b4de1227396586f7807d3543377ed22ce4b172e9bc6be81de70713a482d5",
    "source_block_number": 284870238,
    "destination_chain_id": "eip155:11142220",
    "destination_tx_hash": "0xf6b5226c1ef6b88d5a325bcda28420b828f1d9e891401fd1c80c7fe1f3a31122"
  }
}
```

View the source transaction on [Arbiscan](https://sepolia.arbiscan.io) and the destination transaction on the relevant chain explorer.

### Still settling

Settlement can take up to a few minutes on testnet. While `status` is `pending`, keep polling — do not resubmit the same payment under a new `request_id`.

### Failed (no route available)

If no settlement provider offers a price in time, you may see `NO_QUOTES_RECEIVED`. **Your integration still worked** — submit and signing succeeded. Settlement providers may skip payments they cannot route; check **destination asset IDs** first:

```json theme={null}
{
  "payment_id": "0x...",
  "status": "failed",
  "error": {
    "code": "NO_QUOTES_RECEIVED",
    "domain": "PAYMENT_GATEWAY",
    "message": "No settlement quotes were received within the auction window. This request_id is spent, so a new attempt needs a new one."
  }
}
```

<Warning>
  `NO_QUOTES_RECEIVED` is **not** a spending approval or signing error. A **wrong destination asset ID** is a common cause — copy asset IDs verbatim from [Supported assets](/get-started/reference/supported-assets). See [Troubleshooting](/get-started/support/troubleshooting#no_quotes_received).
</Warning>

### Troubleshooting

| Symptom                              | Likely cause                                         | What to do                                                                                                                                   |
| ------------------------------------ | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `payment_id` on stdout               | Submit succeeded                                     | Poll `/status`                                                                                                                               |
| `status: completed` + both tx hashes | Payment fulfilled                                    | Done                                                                                                                                         |
| `NO_QUOTES_RECEIVED`                 | Wrong asset ID or no settlement                      | Verify asset IDs in [Supported assets](/get-started/reference/supported-assets); see [Troubleshooting](/get-started/support/troubleshooting) |
| Stuck `pending`                      | Settlement in progress                               | Keep polling up to a few minutes                                                                                                             |
| Submit errors / no `payment_id`      | Wallet setup (USDC spend approval, balance, signing) | Re-check [Prerequisites](#prerequisites) and steps 2–3                                                                                       |

## Success criteria

You completed this quickstart when:

* [ ] `GET /v1/defaults` returns chain defaults for the testnet gateway
* [ ] Approve USDC spend confirmed on Arbitrum Sepolia
* [ ] `send-payment` returns a `payment_id` on stdout
* [ ] `GET /v1/payments/{payment_id}/status` returns `"status": "completed"` with `source_tx_hash` and `destination_tx_hash`

The first three items confirm your **integration**. The last item confirms **settlement** — on testnet, verify asset IDs if you see `NO_QUOTES_RECEIVED`; that does not invalidate the first three.

## Next steps

| Topic                                | Guide                                                                |
| ------------------------------------ | -------------------------------------------------------------------- |
| SDK integration in your app          | [Build with the SDK](/get-started/start-building/build-with-the-sdk) |
| Payment Gateway API reference        | [Payment Gateway API](/api-reference/payment-gateway/introduction)   |
| When settlement fails on testnet     | [Troubleshooting](/get-started/support/troubleshooting)              |
| Testnet chains and asset IDs         | [Supported networks](/get-started/reference/supported-networks)      |
| Key terms (Permit2, asset IDs, etc.) | [Core concepts](/get-started/architecture/core-concepts)             |
| Who does what in a payment           | [Who owns what](/get-started/support/who-owns-what)                  |
