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/transfersRun in dashboard

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

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

IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST 'https://api-beta.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"
  }'
// Reuse this key when you retry this request.
const idempotencyKey = crypto.randomUUID();
const res = await fetch("https://api-beta.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"
  })
});
import os, requests, uuid

# Reuse this key when you retry this request.
idempotency_key = str(uuid.uuid4())
r = requests.post("https://api-beta.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
{
  "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
{
  "error": {
    "code": "INSUFFICIENT_PERMISSIONS",
    "message": "Missing action: transfers.execute",
    "details": {
      "action": "transfers.execute"
    }
  },
  "meta": {
    "requestId": "req_example"
  }
}

Error responses use the standard error shape.