> 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.

# Execute transfer (custody)

Executes an on-chain transfer using the exact SDP Wallet ID (id from /v1/wallets) and server-side custody signing.

`POST /v1/payments/transfers`

Requires an Idempotency-Key: a retry with the same key and request returns the original transfer instead of sending a second one. A 200 may return a processing transfer with its signature when broadcast or confirmation is still being reconciled; do not create a replacement transfer for that payment. A native SOL transfer is refused with 409 `AMOUNT_BELOW_RENT_MINIMUM`, before anything is written or sent, when it pays less than the rent-exempt minimum to an address with no account or would leave the source wallet holding more than nothing but less than the minimum. When the source wallet's policy at its custody provider refuses to sign, the transfer fails with 422 `CUSTODY_POLICY_REFUSED` and nothing is signed or sent; the message names the provider whose policy to review. 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 is recorded `failed` with that reason in `error`; a retry with the same Idempotency-Key returns the same error, so fund the wallet and send again with a new key. A source wallet with no token account for the token is refused earlier with 400 `BAD_REQUEST`.

Requires an [API key](/docs/developing-with-sdp/authentication) with the `transfers.execute` action.

## Headers

- **`x-project-id`** · `string` · min length 1 · example `prj_example`

  Required for dashboard sessions. API keys use their assigned project.

- **`Idempotency-Key`** · `string` · required

  Use a unique 1–255 printable ASCII key per operation. Reuse it with the same request on retries. An in-flight attempt returns `409`; a changed request returns `422`. [Retry and recovery rules](/docs/developing-with-sdp/idempotency).

## Request body

`application/json` · required

Create transfer request payload for a custody-managed source wallet. This endpoint does not provision wallets.

- **`projectId`** · `string` · min length 1 · example `prj_example`

  Project identifier for the transfer context.

- **`sourceCustodyWalletId`** · `string` · required · min length 1 · example `cwlt_example`

  Exact SDP Wallet ID (`id` from `GET /v1/wallets`).

- **`destination`** · `string` · required · example `7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU`

  Destination wallet address.

- **`counterpartyAccountId`** · `string` · min length 1 · example `cpacct_example`

  The counterparty's active Solana `crypto_wallet` account that `destination` belongs to, as a transfer batch recipient names it. The transfer record is tied to that counterparty. Its address must equal `destination`.

- **`token`** · `string` · required · example `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`

  Token to transfer. Pass `SOL` for the native token, a well-known token symbol (e.g. `USDC`) resolved to the configured cluster's mint, or a base58 SPL mint address. Custom tokens must be specified by their on-chain mint.

- **`amount`** · `string` · required · example `100.00`

  Token amount in UI units (decimal string).

- **`memo`** · `string` · max length 256

  Optional memo for the transfer.

## Responses

- **`200`** Transfer executed

  - **`data`** · `object` · required

    Transfer response payload.

    - **`transfer`** · `object` · required

      Transfer details.

      - **`fireblocks`** · `object`

        Native Fireblocks transfer tracking. A processing payment may await approval in Fireblocks; network fees are paid by its source vault. A missing transaction ID means submission is still being reconciled. Do not create a replacement payment while its outcome is unknown.

        - **`transactionId`** · `string | null` · required

        - **`status`** · `string` · required

        - **`subStatus`** · `string | null` · required

      - **`id`** · `string` · required · min length 1 · example `xfr_example`

        Transfer identifier (SDP record ID, not the on-chain signature).

      - **`organizationId`** · `string` · required · min length 1 · example `org_example`

        Organization identifier.

      - **`custodyWalletId`** · `string | null` · required · example `cwlt_example`

        Exact SDP Wallet ID associated with the persisted transfer, or null for unresolved legacy and observed rows.

      - **`providerWalletId`** · `string` · required · min length 1 · example `privy_wallet_123`

        Provider wallet ID retained as evidence; not an execution selector.

      - **`projectId`** · `string` · min length 1 · example `prj_example`

        Project identifier for the transfer.

      - **`type`** · `string` · required · example `transfer`

        Transfer type.

      - **`kind`** · `string` · required

        Dashboard transaction kind derived from the transfer type and originating Payments resource.

      - **`direction`** · `string` · required · one of `inbound`, `outbound` · example `outbound`

        Transfer direction.

      - **`status`** · `string` · required · example `confirmed`

        Transfer status.

      - **`signature`** · `string | null` · required · example `sig_example`

        Solana transaction signature (tx id/hash).

      - **`serializedTx`** · `string | null` · required · min length 1 · example `base64_tx_example`

        Base64-encoded transaction payload, if available. Always null for transfer batch chunks, whose signed bytes stay server-side.

      - **`slot`** · `integer | null` · required · example `123456`

        Slot number, if confirmed.

      - **`blockTime`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`

        Block time, if confirmed.

      - **`confirmedAt`** · `string` · format `date-time` · example `2025-01-01T00:00:00.000Z`

        When SDP confirmed the transfer on chain.

      - **`fee`** · `integer | null` · required · example `0`

        Network fee the source wallet paid, in lamports: 0 when SDP sponsored the transaction, null when not known.

      - **`error`** · `string | null` · required · example `Signature failed`

        Error message if the transaction failed.

      - **`initiatedBy`** · `object`

        Initiator that triggered the transfer.

        - **`type`** · `string` · required · one of `api_key`, `user`, `system` · example `api_key`

          Initiator type.

        - **`id`** · `string`

          Initiator identifier if applicable.

        - **`display`** · `string`

          Human-friendly label for the initiator.

      - **`source`** · `string` · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`

        Source wallet address.

      - **`destination`** · `string` · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`

        Destination wallet address.

      - **`memo`** · `string` · max length 256

        Optional memo for the transfer.

      - **`token`** · `string`

        Token symbol or mint address.

      - **`amount`** · `string` · example `100.00`

        Token amount in UI units (decimal string).

      - **`counterpartyId`** · `string` · example `cpty_example`

        Counterparty tied to the transfer record, when available.

      - **`counterpartyDisplayName`** · `string` · example `Acme Studio`

        Current display name of the counterparty tied to the transfer.

      - **`risk`** · `object`

        Optional risk evaluation for the transfer.

        - **`provider`** · `string` · required · example `trm`

          Risk scoring provider.

        - **`score`** · `string` · required · example `0.12`

          Provider-specific risk score.

        - **`level`** · `string` · required · one of `low`, `medium`, `high`, `unknown` · example `low`

          Risk level classification.

        - **`evaluatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`

          Timestamp when risk was evaluated.

      - **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`

        Creation timestamp.

      - **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-02T00:00:00.000Z`

        Last update timestamp.

  - **`meta`** · `object` · required

    Response metadata.

    - **`requestId`** · `string` · required · min length 1 · example `req_example`

      Request identifier for tracing.

    - **`timestamp`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`

      ISO 8601 timestamp.

- **`400`** Bad Request

  - **`error`** · `object` · required

    Standard error payload.

    - **`code`** · `string` · required

      Machine-readable error code.

    - **`message`** · `string` · required

      Human-readable error message.

    - **`details`** · `object`

      Optional error details for debugging.

      - **`*`** · `any`

  - **`meta`** · `object`

    - **`requestId`** · `string` · min length 1 · example `req_example`

      Request identifier for tracing.

- **`401`** Unauthorized

  Same fields as `400`.

- **`403`** Forbidden. Requires the `transfers.execute` action: an API key must hold it (a module's `manage` or `execute` also covers its `read`), and a signed-in member's organization role must include it. Without it the error is INSUFFICIENT_PERMISSIONS and `error.details.action` names the action. Any other 403 names its own reason.

  Same fields as `400`.

- **`404`** Not Found

  Same fields as `400`.

- **`409`** Conflict

  Same fields as `400`.

- **`422`** Unprocessable Entity

  Same fields as `400`.

- **`500`** Internal Server Error

  Same fields as `400`.

- **`502`** Bad Gateway

  Same fields as `400`.

- **`503`** Service Unavailable

  Same fields as `400`.

## Example request

```bash tab="cURL"
IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST 'https://api.solana.com/v1/payments/transfers' \
  -H "Authorization: Bearer $SDP_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "prj_example",
    "sourceCustodyWalletId": "cwlt_example",
    "destination": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
    "token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    "amount": "100.00"
  }'
```

```ts tab="TypeScript"
// Reuse this key when you retry this request.
const idempotencyKey = crypto.randomUUID();
const res = await fetch("https://api.solana.com/v1/payments/transfers", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SDP_API_KEY}`,
    "Idempotency-Key": idempotencyKey,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    "projectId": "prj_example",
    "sourceCustodyWalletId": "cwlt_example",
    "destination": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
    "token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    "amount": "100.00"
  })
});
```

```python tab="Python"
import os, requests, uuid

# Reuse this key when you retry this request.
idempotency_key = str(uuid.uuid4())
r = requests.post("https://api.solana.com/v1/payments/transfers",
  headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
  json={
    "projectId": "prj_example",
    "sourceCustodyWalletId": "cwlt_example",
    "destination": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
    "token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    "amount": "100.00"
  })
```

## Example responses

```json tab="200"
{
  "data": {
    "transfer": {
      "fireblocks": {
        "transactionId": "transaction_example",
        "status": "example",
        "subStatus": "example"
      },
      "id": "xfr_example",
      "organizationId": "org_example",
      "custodyWalletId": "cwlt_example",
      "providerWalletId": "privy_wallet_123",
      "projectId": "prj_example",
      "type": "transfer",
      "kind": "example",
      "direction": "outbound",
      "status": "confirmed",
      "signature": "sig_example",
      "serializedTx": "base64_tx_example",
      "slot": 123456,
      "blockTime": "2025-01-01T00:00:00.000Z",
      "confirmedAt": "2025-01-01T00:00:00.000Z",
      "fee": 0,
      "error": "Signature failed",
      "initiatedBy": {
        "type": "api_key"
      },
      "source": "So11111111111111111111111111111111111111112",
      "destination": "So11111111111111111111111111111111111111112",
      "amount": "100.00",
      "counterpartyId": "cpty_example",
      "counterpartyDisplayName": "Acme Studio",
      "risk": {
        "provider": "trm",
        "score": "0.12",
        "level": "low",
        "evaluatedAt": "2025-01-01T00:00:00.000Z"
      },
      "createdAt": "2025-01-01T00:00:00.000Z",
      "updatedAt": "2025-01-02T00:00:00.000Z"
    }
  },
  "meta": {
    "requestId": "req_example",
    "timestamp": "2025-01-01T00:00:00.000Z"
  }
}
```

```json tab="403"
{
  "error": {
    "code": "INSUFFICIENT_PERMISSIONS",
    "message": "Missing action: transfers.execute",
    "details": {
      "action": "transfers.execute"
    }
  },
  "meta": {
    "requestId": "req_example"
  }
}
```

Error responses use the [standard error shape](/docs/reference/conventions/errors).
