Basic payment

Send a single token transfer with SDP, using either Execute or Prepare mode.

SDP handles token transfers through its payments API. A basic payment moves tokens from a custody wallet you control to any Solana wallet address. The source field on the request is an SDP custody wallet ID (wal_…), resolved server-side from the caller's wallet list; destination is the recipient's on-chain wallet (owner) address — for SPL transfers, SDP derives the associated token account (ATA) from this address and creates it if needed, so pass the wallet pubkey, not a token-account address.

Transfers are available through the Payments dashboard and the API.

Prerequisites

  • A deployed token with minted supply
  • A custody wallet to use as the source. Execute mode additionally requires the wallet to have a custody signer configured so SDP can sign and submit; Prepare mode doesn't (you'll sign client-side).
  • An API key with both payments:write and wallets:read permissions, where the key's payments:write scope includes the source wallet — both endpoints (POST /v1/payments/transfers and POST /v1/payments/transfers/prepare) are gated by that pair at the route layer, and the source wallet is then re-checked against the key's payments:write wallet scope.

How it works

Set up the transfer

Specify the source, destination, token, and amount.

Go to Payments in the sidebar. The overview shows your total SDP balance, recent transactions, and Send / Receive actions.

Payments overview showing balance, Send and Receive buttons, and recent transactions

Click Send and select Wallet transfer to send from an SDP wallet to a Solana address.

Send page showing Wallet transfer and Off-ramp options

On the Enter transfer details form, select a source wallet from the dropdown.

Transfer details form with source wallet dropdown open

Fill in:

  • Source — a wallet you control in SDP
  • Amount — human-readable units (e.g., 0.5)
  • Asset — select from your deployed tokens
  • Destination address — any valid Solana wallet address
  • Memo — optional note recorded onchain

Transfer details form with all fields filled in

Prepare the request body:

const body = {
  source: "wal_example",              // SDP custody wallet ID
  destination: "7xKXz...9fGh",         // recipient wallet (owner) address
  token: "9aBCd...4eEf",               // onchain mint address
  amount: "100.00",                    // human-readable units
  memo: "Payment for invoice #1234",
};

token is the mint address returned by the deploy step, not the SDP token ID. destination is the wallet pubkey — SDP derives the associated token account from it.

Execute the transfer

Submit the transfer. In Execute mode SDP signs and submits. In Prepare mode you receive an unsigned transaction to sign client-side. See Prepare vs Execute for guidance.

Click Review to see the transfer summary, then Confirm to submit.

The transfer status updates to Confirmed once the transaction is finalized.

Execute mode

curl -X POST https://api.solana.com/v1/payments/transfers \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "source": "wal_example",
    "destination": "7xKXz...9fGh",
    "token": "9aBCd...4eEf",
    "amount": "100.00",
    "memo": "Payment for invoice #1234"
  }'
const response = await fetch(
  "https://api.solana.com/v1/payments/transfers",
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer sk_test_...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      source: "wal_example",
      destination: "7xKXz...9fGh",
      token: "9aBCd...4eEf",
      amount: "100.00",
      memo: "Payment for invoice #1234",
    }),
  }
);
const { data } = await response.json();
// data.transfer.id, data.transfer.status — transfer record under the `transfer` envelope key
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/payments/transfers"))
    .header("Authorization", "Bearer sk_test_...")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("""
        {
          "source": "wal_example",
          "destination": "7xKXz...9fGh",
          "token": "9aBCd...4eEf",
          "amount": "100.00",
          "memo": "Payment for invoice #1234"
        }"""))
    .build();

Prepare mode — append /prepare and add "options": { "simulate": true } for a dry-run.

curl -X POST https://api.solana.com/v1/payments/transfers/prepare \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "source": "wal_example",
    "destination": "7xKXz...9fGh",
    "token": "9aBCd...4eEf",
    "amount": "100.00",
    "memo": "Payment for invoice #1234",
    "options": { "simulate": true }
  }'
const response = await fetch(
  "https://api.solana.com/v1/payments/transfers/prepare",
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer sk_test_...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      source: "wal_example",
      destination: "7xKXz...9fGh",
      token: "9aBCd...4eEf",
      amount: "100.00",
      memo: "Payment for invoice #1234",
      options: { simulate: true },
    }),
  }
);
const { data } = await response.json();
// data.preparedTransaction.serialized — sign and submit
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/payments/transfers/prepare"))
    .header("Authorization", "Bearer sk_test_...")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("""
        {
          "source": "wal_example",
          "destination": "7xKXz...9fGh",
          "token": "9aBCd...4eEf",
          "amount": "100.00",
          "memo": "Payment for invoice #1234",
          "options": { "simulate": true }
        }"""))
    .build();

Verify the transfer

Confirm the transfer completed and balances updated.

The transfer appears in Payments history with a confirmed status and an onchain transaction signature.

Payments overview showing confirmed inbound transaction

Click the record to see full details including the Solana Explorer link.

// data.transfer.status    — "confirmed"
// data.transfer.signature — onchain tx signature

Poll status for async confirmations:

curl https://api.solana.com/v1/payments/transfers/txn_abc123 \
  -H "Authorization: Bearer sk_test_..."

Deduplication

Unlike the issuance endpoints, payments transfer endpoints do not currently honor an Idempotency-Key header — retrying a failed transfer request will produce a new Transfer record. To guarantee at-most-once execution from the client side:

  1. Persist the SDP Transfer.id returned by the first successful response before retrying. On retry, skip submission if you already have a transfer ID for that logical payment.
  2. Rely on the on-chain signature (UNIQUE across SDP's transfers table) as the cross-source dedup key once it is populated on the record. signature is filled in when the transaction is submitted to the network, which can lag behind the initial processing status — treat its presence (not the status value) as the trigger for signature-based dedup.

See Payouts and disbursements for the same pattern applied to batched outbound flows.

Is this page helpful?