> Solana Developer Platform docs, served as Markdown. The [docs index](https://platform-beta.solana.com/docs.md) lists every page and [llms.txt](https://platform-beta.solana.com/docs/ai/llms.txt) the key ones; [SKILL.md](https://platform-beta.solana.com/SKILL.md) has the rules for building on SDP.

# Basic payment

Send one token transfer from a custody wallet to a Solana address, from the dashboard or the API.

A basic payment moves tokens from a custody wallet you control to a Solana wallet address. Send it from **Payments** in the dashboard or with `POST /v1/payments/transfers`.

Pass `sourceCustodyWalletId` as the wallet's SDP `id` (`cwlt_…` from `GET /v1/wallets`). Pass `destination` as the recipient's wallet (owner) address, **not** a token account: for an SPL token, SDP derives the recipient's associated token account and creates it if it is missing.

## Prerequisites

- A custody wallet that holds the tokens you want to send, with a custody signer configured so SDP can sign for it.
- In the dashboard: a role that can send payments, and a contact to pay. Add contacts under **Payments** > **Contacts**.
- With the API: a key holding the `transfers.execute` grant whose [wallet access](/docs/developing-with-sdp/manage-api-keys#wallet-access) includes the source wallet. SDP checks the grant at the route and the key's access to the wallet again when it signs.

## How it works

<HowItWorks>

<Step number={1} title="Set up the transfer">

Specify the source, destination, token, and amount.

<StepPanel>

**UI**

Go to **Payments** in the sidebar. The overview shows your **Available balance**, recent **Activity**, and the **Move money** actions.

![Payments overview with the available balance, the Move money actions and recent activity](/images/payments/payments-overview.png)

Click **Pay**. The **Single** tab opens on the **Details** step, "Who are you paying?". Fill in:

- **Contact**: the person or business you are paying.
- **Destination**: one of the contact's Solana addresses. Click **Add a Solana address** to add one here.
- **Source wallet**: the wallet the payment leaves from.
- **Amount**: in whole-token units, such as `0.5`, with the token picked beside it.
- **Memo** (optional): a note SDP stores with the transfer. It is not written on chain.

![Details step with the source wallet list open](/images/payments/pay-source-wallet.png)

![Details step with every field filled in](/images/payments/pay-details-filled.png)

**API**

Build the request body:

```typescript title="transfer-request.ts"
const body = {
  sourceCustodyWalletId: "cwlt_123",    // `id` from GET /v1/wallets
  destination: "7xKXz...9fGh",         // recipient wallet (owner) address
  token: "9aBCd...4eEf",               // mint address, SOL, or a symbol such as USDC
  amount: "100.00",                    // whole-token units, as a string
  memo: "Payment for invoice #1234",
};
```

</StepPanel>

</Step>

<Step number={2} title="Execute the transfer">

Submit the transfer. SDP signs with the source custody wallet and submits the transaction.

<StepPanel>

**UI**

Click **Continue to review**, check the **Review transfer** summary, then click **Send transfer**.

The page waits while Solana finalizes the transfer, then shows the outcome: **Transfer sent** with its status and signature, **Transfer submitted** while it is still settling, or **Transfer failed** with the reason.

**API**

**curl**

```bash title="Terminal"
curl -X POST https://api.solana.com/v1/payments/transfers \
  -H "Authorization: Bearer sf_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: payment-invoice-1234" \
  -d '{
    "sourceCustodyWalletId": "cwlt_123",
    "destination": "7xKXz...9fGh",
    "token": "9aBCd...4eEf",
    "amount": "100.00",
    "memo": "Payment for invoice #1234"
  }'
```

**TypeScript**

```typescript title="execute-transfer.ts"
const response = await fetch(
  "https://api.solana.com/v1/payments/transfers",
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer sf_...",
      "Content-Type": "application/json",
      "Idempotency-Key": "payment-invoice-1234",
    },
    body: JSON.stringify({
      sourceCustodyWalletId: "cwlt_123",
      destination: "7xKXz...9fGh",
      token: "9aBCd...4eEf",
      amount: "100.00",
      memo: "Payment for invoice #1234",
    }),
  }
);
const { data } = await response.json();
// data.transfer is the transfer record: id, status, signature
```

**Java**

```java title="ExecuteTransfer.java"
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/payments/transfers"))
    .header("Authorization", "Bearer sf_...")
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", "payment-invoice-1234")
    .POST(HttpRequest.BodyPublishers.ofString("""
        {
          "sourceCustodyWalletId": "cwlt_123",
          "destination": "7xKXz...9fGh",
          "token": "9aBCd...4eEf",
          "amount": "100.00",
          "memo": "Payment for invoice #1234"
        }"""))
    .build();
```

A successful request waits for finality and returns the transfer at `finalized`. A `processing` answer means it is still settling: don't send it again, read it instead (step 3).

</StepPanel>

</Step>

<Step number={3} title="Verify the transfer">

Check that the transfer settled.

<StepPanel>

**UI**

Click **View on explorer** to open the transaction on Solana Explorer. The transfer also appears under **Payments** > **Transactions** with its status, such as **Finalized**.

Click a row to open the transaction's record, which has its own **View on explorer** link.

**API**

```typescript title="verify-transfer.ts"
// data.transfer.status: "finalized" once Solana finalizes the transaction
// data.transfer.signature: the transaction signature
```

If the response said `processing`, read the transfer until it settles:

```bash
curl 'https://api.solana.com/v1/payments/transfers/xfr_example' \
  -H "Authorization: Bearer $SDP_API_KEY"
```

</StepPanel>

</Step>

</HowItWorks>

## Pay a counterparty

When `destination` is a saved counterparty's Solana address, pass that account's id (`cpacct_…`) as `counterpartyAccountId`. SDP ties the transfer to the counterparty, as it does for a [transfer batch](/docs/payments/send-payouts#native-transfer-batches) recipient: the transfer carries `counterpartyId`, and `GET /v1/payments/transfers?counterpartyId=` lists it. The dashboard's Pay flow does this for you.

The account must be an active `crypto_wallet` account in the same project, and
`destination` must be its address. An unknown or inactive account returns `404`,
any other mismatch returns `400`, and no transfer is created.

Only fields documented for your deployment are accepted. Unknown fields return
`400` during validation, before looking up a wallet or transfer.

## SOL and the rent-exempt minimum

A Solana wallet holds either nothing or at least the rent-exempt minimum (about 0.00089 SOL). SDP refuses a native SOL transfer with `409 AMOUNT_BELOW_RENT_MINIMUM` if it would fund an empty address below that minimum or leave the source with a nonzero balance below it. Nothing is written or sent.

`details.recipientIndexes` (`[0]`) or `details.sourceRemainder` identifies the problem; `details.minimumAmount` gives the minimum. Fund the address, send at least the minimum, or send the source wallet's whole balance. The `409` is not stored under the `Idempotency-Key`, so the same request can run after the balance changes. Token transfers are unaffected: SDP sponsors rent for a missing token account.

## Insufficient funds

When the source wallet holds less SOL, or less of the token in its token account, than the amount, the transfer fails with `400 TRANSACTION_FAILED` and nothing is sent. The transfer is recorded `failed`, and its `error` says the wallet does not have enough funds. Fund the wallet, then send the payment again with a new `Idempotency-Key`: the failed key replays the same error.

A source wallet that has never held the token has no token account for it, so the request is refused earlier with `400 BAD_REQUEST` ("Source wallet has no token account for this mint").

## Deduplication

`POST /v1/payments/transfers` requires an `Idempotency-Key`; without one it returns `400 IDEMPOTENCY_KEY_REQUIRED`. Use one stable key per payment and reuse it for retries. A same-key retry returns the original transfer. A changed request, wallet, or credential returns `422 IDEMPOTENCY_KEY_REUSED`; an in-flight request returns `409 IDEMPOTENCY_KEY_IN_FLIGHT` with `Retry-After`.

A recorded failure (such as `400 TRANSACTION_FAILED`, or `422 CUSTODY_POLICY_REFUSED` when the wallet's policy at its custody provider refused to sign) replays the same error; use a new key to try again. Replays are snapshots, so read the transfer with `GET` for its current status. See [Idempotency](/docs/developing-with-sdp/idempotency) for the full contract.

Persist the returned `Transfer.id` as your local primary key. Once populated, the on-chain `signature` is also unique across SDP transfers and can deduplicate records across SDP and another chain indexer.

See [Payouts and disbursements](/docs/payments/send-payouts#dedup-and-retries-for-single-transfers) for the same pattern applied to a run of transfers.

## Related

- [Payment with memo](/docs/payments/send-payment-with-memo): carry an order reference in `memo`.
- [Payouts and disbursements](/docs/payments/send-payouts): pay many recipients in one batch.
- [Verifying a payment](/docs/payments/accept-verification): transfer statuses and finality.
