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

# Submit a withdrawal

Submits a withdrawal transaction signed by the customer's wallet.

> **Earn is under active development.** Features, API endpoints, and docs may change.

`POST /v1/earn/external-wallet/withdrawals`

It works like a deposit submit: SDP checks every signature against the exact built message, records the movement before broadcast, requires an Idempotency-Key, and accepts each built transaction once, with the same `409 CONFLICT` answers. Submit the original signed bytes even after the blockhash expires, since the wallet may already have broadcast them. After an uncertain response, retry with the same key and body, then read the movement instead of building a new withdrawal. Withdrawals stay available while Earn is disabled.

Requires an [API key](/docs/developing-with-sdp/authentication) with the `earn.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 · pattern `^[\x20-\x7e]{1,255}$` · example `idempotency_example_12345`

  Idempotency key for safely retrying mutating requests. 1-255 printable ASCII characters; echoed back on the response.

## Request body

`application/json` · required

Submit the signed bytes back. Requires the Idempotency-Key header; a retry with the same key resolves the original movement or withdrawal request instead of acting twice.

- **`transactionId`** · `string` · required · min length 1 · max length 128 · example `earn_external_wallet_transaction_e7a88258-1d19-48f3-8d0e-81fefa4bf3b3`

  The built transaction being submitted; single-use.

- **`signedTransaction`** · `string` · required · min length 1 · max length 1700 · pattern `^[A-Za-z0-9+/]+={0,2}$` · example `AUyx/sEc49/1l6jxo3nbViPAIUqdrXCo8Qx5SfDvIbtUI0zex3Bi6Cyhhn1QTVU8zWDzXTRyIX2aCmq9v7yk5AOAAQABAvSV602SJ67iQCUGLwHzVtPnlMbZqL79wnV/nb+Mi9SJBUpTWpkpIQZNJOhxYNo4fHw1td28kruB5B+oQEEFRI0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAAtzZHAtZXhhbXBsZQA=`

  Base64 wire bytes of the signed transaction. They must be byte-for-byte the built message with only signatures added.

## Responses

- **`200`** The recorded withdrawal

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

    - **`withdrawal`** · `object` · required

      One recorded external-wallet vault movement.

      - **`settlementOutcome`** · `string` · one of `delivered`, `cancelled`

      - **`movementId`** · `string` · required · example `earn_movement_a6aed3f5-4f89-4dc6-9d4b-0864021bd80d`

      - **`positionId`** · `string` · required · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`

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

      - **`providerReference`** · `string` · required · example `7uib8xGAwkaPz4ZGCA6t8sSEid5Yp9ty13PHUweTypx`

      - **`direction`** · `string` · required · one of `deposit`, `withdrawal`

      - **`status`** · `string` · required · one of `requested`, `submitted`, `confirmed`, `finalized`, `failed`

        The movement's status. `requested` means SDP recorded the signed transaction but has not yet seen it on the network. `confirmed` is optimistic and can still be rolled back; only `finalized` and `failed` are final.

      - **`settlement`** · `string` · required · one of `atomic`, `provider_order`

        How the movement settles. `atomic` completes in one transaction, so you may show it as done at `confirmed` while SDP tracks finality. `provider_order` pays out later through the provider, so chain confirmation alone is not payout: the movement stays `confirmed` until the provider completes it, and only `finalized` means it is done.

      - **`signature`** · `string` · required

        The transaction signature, for explorers.

      - **`ownerAddress`** · `string` · required · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`

        The customer's wallet address.

      - **`amount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`

        Requested quantity, denominated in `denomination`.

      - **`denomination`** · `string` · required · example `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`

        Token mint for a deposit; share mint for a withdrawal.

      - **`tokenMint`** · `string` · required · example `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`

        The position's deposit-token mint. Show `tokenAmount` in these units; `amount` and `denomination` carry the onchain quantity, which is shares on a withdrawal.

      - **`tokenAmount`** · `string | null` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`

        Quantity in `tokenMint` units. Kamino deposits and all withdrawals require a finalized observation; null beforehand or when the actual debit/payout cannot be observed. Other deposits report their fixed input amount.

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

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

        ISO 8601 timestamp.

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

        ISO 8601 timestamp.

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

        ISO 8601 timestamp.

      - **`replayed`** · `boolean`

        True when the idempotency key already produced this movement and nothing was re-sent.

  - **`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 `earn.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`.

- **`429`** Too Many Requests

  Same fields as `400`.

- **`500`** Internal Server Error

  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/earn/external-wallet/withdrawals' \
  -H "Authorization: Bearer $SDP_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transactionId": "earn_external_wallet_transaction_0ebe7320-4b8e-4d1a-a5f0-6c2d9e8b1f47",
    "signedTransaction": "AUyx/sEc49/1l6jxo3nbViPAIUqdrXCo8Qx5SfDvIbtUI0zex3Bi6Cyhhn1QTVU8zWDzXTRyIX2aCmq9v7yk5AOA"
  }'
```

```ts tab="TypeScript"
// Reuse this key when you retry this request.
const idempotencyKey = crypto.randomUUID();
const res = await fetch("https://api.solana.com/v1/earn/external-wallet/withdrawals", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SDP_API_KEY}`,
    "Idempotency-Key": idempotencyKey,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    "transactionId": "earn_external_wallet_transaction_0ebe7320-4b8e-4d1a-a5f0-6c2d9e8b1f47",
    "signedTransaction": "AUyx/sEc49/1l6jxo3nbViPAIUqdrXCo8Qx5SfDvIbtUI0zex3Bi6Cyhhn1QTVU8zWDzXTRyIX2aCmq9v7yk5AOA"
  })
});
```

```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/earn/external-wallet/withdrawals",
  headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
  json={
    "transactionId": "earn_external_wallet_transaction_0ebe7320-4b8e-4d1a-a5f0-6c2d9e8b1f47",
    "signedTransaction": "AUyx/sEc49/1l6jxo3nbViPAIUqdrXCo8Qx5SfDvIbtUI0zex3Bi6Cyhhn1QTVU8zWDzXTRyIX2aCmq9v7yk5AOA"
  })
```

## Example responses

```json tab="200"
{
  "data": {
    "withdrawal": {
      "movementId": "earn_movement_f671de6c-e6f9-4b6f-b390-147eb942cea4",
      "positionId": "earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b",
      "provider": "kamino",
      "providerReference": "C7N2fqV2GvhFghDf5PJSwstyjedjLNCY1cm2Ak7TeU83",
      "direction": "withdrawal",
      "status": "submitted",
      "settlement": "atomic",
      "signature": "3dhkSPLiQZQPXL96fWzMdWJtdbtAffkBKXw2xhsYbV3VNSnYrVAcdx1czsnLsE1ou4SvLecGdSYe4QpAuZdfjJAf",
      "ownerAddress": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
      "amount": "10",
      "denomination": "CeirL1mAehaM6g2EccgMUQwohNfnuyicskVQ4r9ekXk6",
      "tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
      "tokenAmount": null,
      "failureReason": null,
      "createdAt": "2026-11-06T18:21:04.120Z",
      "confirmedAt": null,
      "settledAt": null,
      "replayed": false
    }
  },
  "meta": {
    "requestId": "req_example",
    "timestamp": "2025-01-01T00:00:00.000Z"
  }
}
```

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

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