# Solana Developer Platform
> Full public documentation for Solana Developer Platform — all page content for AI and agent ingestion.
## Canonical URLs
- Docs: https://platform.solana.com/docs
- API: https://api.solana.com
- API reference: https://platform.solana.com/docs/reference/api
- OpenAPI: https://api.solana.com/openapi.json
- AI guide: https://platform.solana.com/docs/reference/docs-for-ai
## Docs
### What is SDP?
Source: https://platform.solana.com/docs/introduction
> One API for building on Solana with your chosen providers, from devnet to mainnet.
Solana Developer Platform (SDP) gives your app one API for the Solana services it uses. Build and test on devnet in the dashboard, then call the API with a key you create there.
## Why SDP
Institutional assets are moving onchain. In October 2026, RWA.xyz tracked [about $39.5 billion in tokenized real-world assets](https://app.rwa.xyz/), including [about $5.2 billion on Solana](https://app.rwa.xyz/networks/solana). [Tokenized stocks](https://app.rwa.xyz/stocks) are one part of that growth.
Assets at this scale need custody, enforceable compliance, and auditable controls. SDP puts the services your deployment supports behind one API, so you can build on Solana without wiring up every provider yourself.
## How it fits together
- **Devnet and mainnet** each have their own keys, wallets, and settings. Build on devnet with test tokens, then [go to mainnet](/docs/guides/go-to-mainnet) once SDP approves your organization.
- **Providers** supply the services behind SDP. You choose the providers you use; your app calls the same SDP API either way.
- **Access** is explicit. An API key holds only the actions you grant it, and sensitive operations leave an audit trail.
## What you can build
This deployment publishes these modules:
| Module | What it does |
| --- | --- |
| **Custody** | Provision and operate Solana wallets through interchangeable custody providers |
| **Payments** | Move funds onchain: transfers, batches, recurring payments, and payment requests |
| **Earn** | Discover DeFi and RWA strategies for customer-wallet deposits and withdrawals |
[Get started](/docs) on devnet, or [browse the API reference](/docs/reference/api).
---
### Get started
Source: https://platform.solana.com/docs
> Build on Solana with one dashboard and REST API, from devnet to mainnet.
---
### Go to mainnet
Source: https://platform.solana.com/docs/guides/go-to-mainnet
> Request mainnet access, connect your own custody account, and go live.
You start with a Sandbox project on devnet. The SDP team must approve your
organization before enabling Production on mainnet.
## 1. Request mainnet access
Sign in to the [dashboard](https://platform.solana.com/dashboard) as an
**organization admin**. Open the account menu at the bottom of the sidebar, then
**Settings**. Under **Mainnet access**, click **Request mainnet access**. If the
section is missing, ask an admin to submit.
[](/docs/images/go-to-mainnet/01-request-access.png)
Select your organization's registered **Country or region** and click
**Continue**. Contact the SDP team to change it after submission.
[](/docs/images/go-to-mainnet/02-jurisdiction.png)
Read the **Terms and Conditions** to the end, accept them on your organization's
behalf, and click **Send request**. **Request sent** confirms submission;
Settings shows **In review** until approval.
[](/docs/images/go-to-mainnet/request-sent.png)
If you see **Request not sent**, click **Send request again**; your details are
saved. If requests aren't open, contact the SDP team.
## 2. Switch to Production
After approval, refresh **Settings**. When **Mainnet access** shows **Approved**,
click your organization's name at the top of the sidebar and select your
production project (by default, **Default Production Project**).
Production uses `mainnet-beta`. Wallets, provider connections, and API keys are
separate from Sandbox and must be set up again.
## 3. Connect your custody provider account
Production runs on your own provider account. Open **Wallets**, click
**Create wallet**, and choose a provider.
- [Connect Fireblocks](/docs/wallet-operations/fireblocks): Connect your own Fireblocks workspace for production custody.
For Privy, keep or rename the **Credential label**. Enter your **Privy app ID** and
**Privy app secret** from the [Privy dashboard](https://dashboard.privy.io),
then a **Wallet label**. Click **Connect and create wallet** to verify the
connection and create your first Privy wallet.
[](/docs/images/go-to-mainnet/04-connect-privy.png)
For more wallets, pick this connection under **Whose Privy account holds the
keys?** See [Create a wallet](/docs/guides/setup-wallets).
## 4. Create a production API key
In Production, open **API keys** and click **Create new API key**. Name it,
grant the actions it needs, choose which wallets it can sign with, and review
the summary. Click **Create key**.
[](/docs/images/go-to-mainnet/05-production-api-key.png)
Copy the key to your secret manager; it's shown once. Replace your backend's
sandbox key. The API URL stays `https://api.solana.com`; the key's project
determines the network. See
[API key grants and wallet access](/docs/developing-with-sdp/manage-api-keys).
## 5. Fund your wallet with mainnet USDC
Send a small amount of **USDC on Solana mainnet** to your wallet's address.
Devnet tokens have no value on mainnet. Circle's
[mainnet USDC mint](https://developers.circle.com/stablecoins/usdc-contract-addresses)
is `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`.
Use `"token": "USDC"` in transfer requests so SDP selects the mint for your network.
Replace any hardcoded devnet mint addresses.
SDP sponsors network fees and recipient token-account rent for transfers it
signs from your Privy wallet. Fund it with USDC; no SOL is needed for those fees.
Sponsorship limits apply. If sponsorship is unavailable, SDP refuses the transfer
instead of charging your wallet.
Send a [small first payment](/docs/payments/send-basic-payment) and
[verify its status](/docs/payments/accept-verification) before increasing volume.
---
## Learn
### Tutorials
Source: https://platform.solana.com/docs/tutorials
> Follow a complete SDP workflow, from setup to a finished request.
Choose a workflow to build from start to finish.
- [Send a payment](/docs/tutorials/end-to-end-payment-flow): Move from wallet setup to settlement and reconciliation.
---
### Core concepts
Source: https://platform.solana.com/docs/developing-with-sdp
> The interfaces, environments, and conventions you build against.
SDP has two interfaces over the same API:
- **Dashboard:** the browser UI. Sign in with email, Google, or GitHub.
- **REST API:** programmatic access for backend integrations. Requests authenticate with an API key in the `Authorization` header.
Both call the same API, and each route requires the same action whoever calls it. A signed-in member holds actions through their organization role; an API key holds the grants you give it. Some routes are for signed-in members only, such as managing API keys, members, and the organization, or setting up a custody provider. An API key is refused them.
Build against the API for automation and integrations. Use the dashboard to set up your organization, inspect state, and run one-off operations.
## Environments
An environment belongs to the **project**, not to individual requests. Every tenant resource and API key belongs to a project and inherits its environment.
| Environment | Network |
| ------------ | ------------ |
| `sandbox` | devnet |
| `production` | mainnet-beta |
A key's format doesn't name its environment; the project it belongs to does.
Both environments expose the same route shapes. Build and test against your sandbox project, then move to production with a production project's API key. [Go to mainnet](/docs/guides/go-to-mainnet) covers requesting access and setting up your production wallets and API key.
## Response conventions
Every successful response wraps its payload in a `data` envelope with request metadata:
```json
{
"data": { ... },
"meta": { "requestId": "req_...", "timestamp": "2026-07-31T12:00:00.000Z" }
}
```
Errors return a machine-readable `code` alongside a human-readable `message`:
```json
{
"error": { "code": "INVALID_API_KEY", "message": "Invalid API key" },
"meta": { "requestId": "req_..." }
}
```
Include the `requestId` when you report an issue. It links your request to SDP's traces.
## Execution model
For a custody operation, SDP builds the transaction, signs it with the configured custody wallet, and submits it.
## Read next
- [Authentication](/docs/developing-with-sdp/authentication) covers project keys and access checks.
- [API keys](/docs/developing-with-sdp/manage-api-keys) covers grants and wallet scope.
- [Idempotency](/docs/developing-with-sdp/idempotency) covers safe retries.
- [API conventions](/docs/reference/conventions) covers errors, pagination, limits, and versioning.
---
## Modules
### Wallets
Source: https://platform.solana.com/docs/wallet-operations
> Create a custody wallet, check balances, and scope access.
Create a custody wallet, check its balances, and use API key grants to control what an integration can do with it.
## Start here
- [Create a wallet](/docs/guides/setup-wallets): choose a provider and provision your first wallet.
- [Wallet balances](/docs/wallet-operations/balances): read token balances and optional USD valuations.
- [Manage API keys](/docs/developing-with-sdp/manage-api-keys): grant only the actions and wallets an integration needs.
- [Connect Fireblocks](/docs/wallet-operations/fireblocks): Connect your own Fireblocks workspace for production custody.
See the [Wallets API reference](/docs/reference/api/wallets) for request and response details.
---
### Payments
Source: https://platform.solana.com/docs/payments
> Send, accept and reconcile Solana payments from your SDP custody wallets.
Send SOL and SPL tokens from your custody wallets, pay many recipients in one batch, schedule recurring payments, and match what moved to your own records. If you have no custody wallet yet, start with [Set Up Wallets](/docs/guides/setup-wallets).
## What's in this section
- **[How payments work on SDP](/docs/payments/concepts)**: wallets, tokens, fees and the transfer record.
- **Send payments**
- [Basic payment](/docs/payments/send-basic-payment): one transfer, from the dashboard or the API.
- [Payment with memo](/docs/payments/send-payment-with-memo): tag a transfer with your order or invoice reference.
- [Payouts and disbursements](/docs/payments/send-payouts): pay many recipients and reconcile the run.
- **Accept payments**
- [Accept overview](/docs/payments/accept-overview): payment links, shared addresses, and finding each payment.
- [Verifying a payment](/docs/payments/accept-verification): transfer statuses and when a payment is final.
- [Indexing and reconciliation](/docs/payments/accept-indexing): list transfers and match them to your ledger.
Wallet balances and controls are in [Wallet operations](/docs/wallet-operations).
## Underlying API
Payments endpoints live under `/v1/payments`: transfers, transfer batches, recurring payments and wallet balances. Each page here links the endpoints it uses, and the [Payments API reference](/docs/reference/api/payments) lists them all.
## Related
- [End-to-end payment flow](/docs/tutorials/end-to-end-payment-flow): wallet setup through settlement and reconciliation.
---
## Earn
### Earn
Source: https://platform.solana.com/docs/earn
> Offer stablecoin yield in your app, from your customers' own wallets, through one API.
> **Earn is under active development.** Features, API endpoints, and docs may change.
Earn lets your customers earn yield on stablecoins without handing over their
keys. Your backend builds each deposit and withdrawal through SDP, your
customer's wallet signs it, and SDP broadcasts it and tracks it to settlement.
[Start the quickstart](/docs/earn/quickstart)
[API reference](/docs/reference/api/earn)
## What you can build
| Capability | What Earn gives you |
| --- | --- |
| [Strategies](/docs/earn/strategies) | A catalog of DeFi and real-world-asset strategies with live rates, deposit tokens, and withdrawal terms. |
| [Deposits](/docs/earn/deposits) | Live quotes, unsigned transactions for your customer's wallet, and tracking from submit to finality. |
| [Balances and earnings](/docs/earn/balances) | Live positions, recorded movements, and exact earnings for each customer wallet. |
| [Withdrawals](/docs/earn/withdrawals) | Instant withdrawals, plus provider orders, solver queues, and operator redemptions that pay out later. |
Returns, losses, fees, and withdrawal timing depend on the strategy. An APY is
a rate, not a promise.
## How it works
- **Customer's wallet**
Signs a deposit of the strategy's deposit token.
- **Strategy**
Issues shares or a yield-bearing token to the customer's wallet.
- **Customer's wallet**
Receives the payout when a withdrawal settles.
SDP never holds your customer's keys or pools their funds in an SDP account.
Funds sit with the strategy's protocol or provider, whose custody and
withdrawal risks still apply. [How Earn works](/docs/earn/concepts) explains
shares, signing, and settlement.
## Start building
Run the [Quickstart](/docs/earn/quickstart) to make your first sandbox deposit
through the Earn API, then follow [Embedded Yield](/docs/guides/embedded-yield)
to ship the full integration. Every request field and response is in the
[Earn API reference](/docs/reference/api/earn).
---
### Quickstart
Source: https://platform.solana.com/docs/earn/quickstart
> Deposit test USDC into a strategy from a sandbox wallet, then read the position, in about ten minutes.
> **Earn is under active development.** Features, API endpoints, and docs may change.
Everything here runs in your sandbox project on devnet, with a local test
wallet standing in for your customer's wallet. Nothing touches real funds.
## Before you begin
Create the project folder. Run the remaining commands from it:
```bash title="Terminal"
mkdir earn-quickstart && cd earn-quickstart
```
| You need | How to get it |
| --- | --- |
| An API URL for a beta or experimental deployment | Use the API URL supplied with your SDP deployment. Stable deployments do not include Earn. |
| A sandbox API key with `earn.execute` | [Create an API key](/docs/developing-with-sdp/manage-api-keys) in your sandbox project. Keep it on your backend. |
| The Solana CLI | [Install it](https://solana.com/docs/intro/installation) for `solana-keygen` and `solana airdrop`. |
| A test wallet | `solana-keygen new --no-bip39-passphrase -o test-wallet.json` creates one. It plays your customer's wallet. |
| Devnet SOL | `solana airdrop 1 $(solana-keygen pubkey test-wallet.json) --url devnet`, or the [Solana faucet](https://faucet.solana.com). The wallet pays network fees and the rent for its new accounts, under 0.004 SOL. |
| Devnet USDC | The [Circle faucet](https://faucet.circle.com) on Solana Devnet. Most sandbox strategies take this mint: `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`. The examples deposit 25, so request enough or lower `amount`. |
| Node.js 20.18 or later | For the TypeScript steps. |
Your organization also needs the strategy's provider activated. If a preview
returns `403` with `provider_not_entitled`, ask your SDP contact.
Install the TypeScript tools, then set your deployment URL and key:
```bash title="Terminal"
npm init -y && npm pkg set type=module
npm install @solana/kit@8 && npm install -D tsx
export SDP_API_URL="https://YOUR_SDP_API_HOST"
export SDP_API_KEY="sf_..."
```
Then add the test wallet helper. It signs exactly the bytes SDP builds, which
is what your customer's wallet will do in production:
```typescript title="test-wallet.ts"
import { readFile } from "node:fs/promises";
import {
createKeyPairFromBytes,
getAddressFromPublicKey,
getBase64EncodedWireTransaction,
getBase64Encoder,
getTransactionDecoder,
partiallySignTransaction,
} from "@solana/kit";
// Sandbox only: a local keypair stands in for your customer's wallet.
const secretKey = new Uint8Array(JSON.parse(await readFile("test-wallet.json", "utf8")));
const keyPair = await createKeyPairFromBytes(secretKey);
export const ownerAddress = await getAddressFromPublicKey(keyPair.publicKey);
/** Signs SDP's unsigned base64 transaction without changing it. */
export async function signWithTestWallet(transactionBase64: string): Promise {
const transaction = getTransactionDecoder().decode(getBase64Encoder().encode(transactionBase64));
// Partial: it leaves any other signer's slot, such as your fee payer's, empty.
const signed = await partiallySignTransaction([keyPair], transaction);
return getBase64EncodedWireTransaction(signed);
}
```
## Make the deposit
Call the Earn API with a small request helper, then write the deposit as one
file, `deposit.ts`.
### 1. Add the request helper
```typescript title="earn-api.ts"
const SDP_API_URL = process.env.SDP_API_URL;
if (!SDP_API_URL) throw new Error("Set SDP_API_URL to your beta or experimental API URL");
const SDP_API_KEY = process.env.SDP_API_KEY;
if (!SDP_API_KEY) throw new Error("Set SDP_API_KEY to your sandbox API key");
/** Calls the Earn API: GET without a body, POST with one. Returns `data`. */
export async function earnRequest(path: string, body?: unknown, idempotencyKey?: string) {
const response = await fetch(`${SDP_API_URL}${path}`, {
method: body === undefined ? "GET" : "POST",
headers: {
Authorization: `Bearer ${SDP_API_KEY}`,
...(body === undefined ? {} : { "Content-Type": "application/json" }),
...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
},
body: body === undefined ? undefined : JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`SDP returned ${response.status}: ${await response.text()}`);
}
return (await response.json()).data;
}
/** Polls a movement until it is final: every second for a minute, then every five seconds. */
export async function waitForMovement(movementId: string, timeoutMs = 300_000) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
const { movement } = await earnRequest(
`/v1/earn/external-wallet/movements/${encodeURIComponent(movementId)}`,
);
if (movement.status === "finalized" || movement.status === "failed") return movement;
const delayMs = Date.now() - startedAt < 60_000 ? 1_000 : 5_000;
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
throw new Error(`Movement ${movementId} is not final yet; keep tracking it`);
}
```
### 2. Choose a strategy, then build
This example picks an active, fundable strategy whose first deposit mint is
devnet USDC and needs no slippage floor.
[Strategies](/docs/earn/strategies) explains each check.
```typescript title="deposit.ts"
import { randomUUID } from "node:crypto";
import { earnRequest, waitForMovement } from "./earn-api";
import { ownerAddress, signWithTestWallet } from "./test-wallet";
type Strategy = {
id: string;
status: string;
fundable: boolean;
depositMints: string[];
depositSlippage: unknown;
withdrawalSlippage: { defaultToleranceBps: number } | null;
};
const { strategies }: { strategies: Strategy[] } = await earnRequest(
"/v1/earn/strategies?pageSize=100",
);
const strategy = strategies.find(
(s) => s.fundable && s.status === "active" && s.depositSlippage === null
&& s.depositMints[0] === "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
);
if (!strategy) throw new Error("No devnet USDC strategy is open for deposits");
const amount = "25"; // in the deposit token, depositMints[0]
const preview = await earnRequest("/v1/earn/vault-deposit-previews", {
strategyId: strategy.id,
amount,
});
if (preview.blockingIssues.length > 0) {
throw new Error(`Preview blocked: ${JSON.stringify(preview.blockingIssues)}`);
}
const { transaction: built } = await earnRequest(
"/v1/earn/external-wallet/deposit-transactions",
{ strategyId: strategy.id, ownerAddress, amount },
);
```
The build is an unsigned transaction. Nothing has moved yet.
### 3. Sign and submit
```typescript title="deposit.ts (continued)"
const signedTransaction = await signWithTestWallet(built.transaction);
// In production, save the idempotency key with the signed transaction before you send it,
// and retry an uncertain response with both unchanged.
const idempotencyKey = randomUUID();
const { deposit } = await earnRequest(
"/v1/earn/external-wallet/deposits",
{ transactionId: built.transactionId, signedTransaction },
idempotencyKey,
);
```
SDP checks the signature against the exact bytes it built, records the
deposit, then broadcasts it.
Persist `deposit.positionId` with the selected strategy's `withdrawalSlippage`
in your backend. Save `null` explicitly when no floor is supported. Keep this
record for withdrawals even if the strategy leaves the catalog.
### 4. Track it and read the position
```typescript title="deposit.ts (continued)"
const movement = await waitForMovement(deposit.movementId);
if (movement.status === "failed") throw new Error(movement.failureReason ?? "Deposit failed");
const { positions } = await earnRequest(
`/v1/earn/external-wallet/positions?${new URLSearchParams({ ownerAddress })}`,
);
console.log(JSON.stringify(positions, null, 2));
```
Run it:
```bash title="Terminal"
npx tsx deposit.ts
```
## See the result
The movement usually reaches `finalized` within 30 seconds, and the wallet
holds shares in the strategy. Your strategy and its `label` may differ:
```json title="Output (trimmed)"
[
{
"id": "earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b",
"ownerAddress": "Af89Mkuvo6AcLDbs7Fxs1FPsnnbD9KrY9NfmaFXHqmPo",
"provider": "kamino",
"label": "Kamino Vault USDC",
"tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"shareMint": "CeirL1mAehaM6g2EccgMUQwohNfnuyicskVQ4r9ekXk6",
"shares": "25",
"withdrawableShares": "25",
"tokenValue": "25"
}
]
```
`tokenValue` is the position's live value in the deposit token. Share and
token amounts are equal here only because this vault's shares are still worth
one USDC each.
## Sign in your customer's wallet
In production, the signing step runs in your app, not on your backend. With a
[Wallet Standard](https://github.com/wallet-standard/wallet-standard) wallet
and `@solana/react` (`npm install @solana/react@8 @solana/kit@8` in your app),
sign the same base64 transaction and check that the wallet returned it
unchanged:
```typescript title="use-sign-in-wallet.ts"
import { useWalletAccountTransactionSigner } from "@solana/react";
import {
getBase64EncodedWireTransaction,
getBase64Encoder,
getTransactionDecoder,
} from "@solana/kit";
type WalletAccount = Parameters[0];
/** Signs SDP's base64 transaction in the customer's connected wallet. */
export function useSignInWallet(account: WalletAccount) {
// Use "solana:mainnet" for a production project.
const signer = useWalletAccountTransactionSigner(account, "solana:devnet");
return async (transactionBase64: string): Promise => {
const unsigned = getTransactionDecoder().decode(getBase64Encoder().encode(transactionBase64));
// Wallet Standard signers may modify before signing, so check the result below.
const [signed] = await signer.modifyAndSignTransactions([unsigned]);
if (!signed) throw new Error("The wallet returned no signed transaction");
const unchanged =
signed.messageBytes.length === unsigned.messageBytes.length &&
signed.messageBytes.every((byte, index) => byte === unsigned.messageBytes[index]);
if (!unchanged) throw new Error("The wallet changed the transaction; build a new one");
return getBase64EncodedWireTransaction(signed);
};
}
```
Send the signed transaction to your backend, which submits it. Some wallets
add instructions before they sign. SDP refuses a changed transaction with
`400`, so check first and ask for a fresh build. Never broadcast it from the
wallet: SDP broadcasts after it records the deposit.
## Next steps
| Next | Where |
| --- | --- |
| Handle an uncertain submit | [Retry safely](/docs/earn/integration-details#retry-safely) |
| Show balances and earnings | [Balances and earnings](/docs/earn/balances) |
| Withdraw the position | [Withdraw funds](/docs/earn/withdrawals) |
| Ship the full integration | [Embedded Yield](/docs/guides/embedded-yield) |
---
### How Earn works
Source: https://platform.solana.com/docs/earn/concepts
> Strategies, positions and shares, who signs, and how each deposit and withdrawal settles.
> **Earn is under active development.** Features, API endpoints, and docs may change.
Earn moves your customer's stablecoins into a strategy and back out again.
This page explains the pieces you work with and the rules that keep balances
honest.
## Key terms
| Term | Meaning |
| --- | --- |
| Strategy | A yield source in the catalog: a lending market, a vault, or a yield-bearing token. Each takes one deposit token. |
| Position | One customer wallet's holding in one strategy. |
| Shares | The units a position holds. Withdrawals spend shares, not dollars. |
| Movement | SDP's record of one deposit or withdrawal, from submit to finality. |
| Withdrawal request | SDP's record of a withdrawal that pays out later, through a solver queue or an operator redemption. |
| External wallet | A wallet SDP holds no key for, such as your customer's own wallet. The `/external-wallet/` routes and `ownerAddress` refer to it. |
## Strategies
| Source | Where yield comes from |
| --- | --- |
| DeFi (`sourceKind: "defi"`) | Onchain lending markets and vaults. Returns move with protocol activity. |
| Real-world assets (`sourceKind: "rwa"`) | Tokenized investments backed by assets such as US Treasuries. Returns and redemptions follow the issuer's terms. |
`sourceKind` says where yield comes from, not how a withdrawal settles. Read
each position's [withdrawal options](/docs/earn/withdrawals#1-choose-a-route)
to see which routes it supports. [Strategies](/docs/earn/strategies) covers
the catalog and how to choose.
## Positions and shares
Three amounts describe a position:
| Amount | Field | What it tells you |
| --- | --- | --- |
| Shares | `shares` | How much of the strategy the wallet owns, in share units. |
| Share value | `tokenValue` | What those shares are worth now, in the deposit token. |
| Withdrawable shares | `withdrawableShares` | How many shares can leave right now, after locks and liquidity. |
A withdrawal names a share amount, and its payout depends on the strategy's
live state. Quote it with a
[withdrawal preview](/docs/earn/withdrawals#2-withdraw-instantly) instead of
multiplying by an old rate. A missing live value means unavailable, never
zero.
## Who signs
SDP never holds your customer's key. Every deposit and withdrawal follows the
same four steps:
1. Your backend asks SDP to **build** an unsigned transaction.
2. Your customer's wallet **signs** it, without changing or broadcasting it.
3. Your backend **submits** the signed transaction to SDP.
4. SDP verifies it, records a movement, broadcasts it, and tracks it.
Keep your API key on your backend. It can read every position your project
records, so your backend decides which customer may act on which wallet. See
[Embedded Yield](/docs/guides/embedded-yield#authenticate-on-your-backend).
## How a movement settles
| Status | Meaning |
| --- | --- |
| `requested` | SDP recorded the signed transaction and has not seen it onchain yet. |
| `submitted` | SDP broadcast the transaction. |
| `confirmed` | The network confirmed it. Final for display only when settlement is `atomic`. |
| `finalized` | The transaction is final. |
| `failed` | It did not land. `failureReason` says why. |
A movement's `settlement` decides when funds have really moved:
- **`atomic`**: the deposit or withdrawal happens in one transaction. You may
show it as done at `confirmed` while SDP tracks it to `finalized`.
- **`provider_order`**: the transaction places an order that the provider
completes later. Confirmation proves the order, not the payout: the
movement stays `confirmed` until the provider completes it, and only
`finalized` means it is done.
Withdrawals through a solver queue or an operator redemption also pay out
later. Track them as [withdrawal requests](/docs/earn/withdrawals#track-a-request),
and credit funds only when the payout arrives.
## Where funds go
A vault or lending deposit exchanges the deposit token for shares in the
customer's wallet. A token strategy acquires the yield-bearing token itself.
Either way, the deposited stablecoins go to the protocol or provider, never to
an SDP-owned account.
**warn**
Never send tokens to a strategy's `providerReference`. It identifies the
instrument, not a deposit address. Deposit only through a transaction SDP
builds.
Holding shares in the customer's own wallet does not remove the provider's
custody, liquidity, or withdrawal risks. A position's value can rise or fall
with returns, losses, and fees.
## Sandbox and production
Your API key's project decides the network: a sandbox project runs on devnet
with test tokens, and a production project runs on mainnet with real funds.
The API host and every request stay the same. See
[Go to mainnet](/docs/guides/go-to-mainnet) when you are ready to move.
Next, [make your first deposit](/docs/earn/quickstart) or
[choose a strategy](/docs/earn/strategies).
---
### Strategies
Source: https://platform.solana.com/docs/earn/strategies
> Find a strategy your project can deposit into, then check its slippage policy, rate, eligibility, and withdrawal terms.
> **Earn is under active development.** Features, API endpoints, and docs may change.
Every deposit names a strategy. List the catalog, keep the strategies your
project can fund, and check each one's terms before you offer it to
customers.
Set `SDP_API_URL` and `SDP_API_KEY` as shown in the
[Quickstart](/docs/earn/quickstart#before-you-begin).
## List the catalog
[`GET /v1/earn/strategies`](/docs/reference/api/earn/list-earn-strategies)
returns the strategies your project can see, largest first. It defaults to
your project's cluster: devnet for sandbox, mainnet for production.
```bash title="Terminal"
curl --fail-with-body -sS "${SDP_API_URL:?Set SDP_API_URL}/v1/earn/strategies?pageSize=100" \
-H "Authorization: Bearer $SDP_API_KEY"
```
```json title="Response (trimmed)"
{
"data": {
"strategies": [
{
"id": "earn_strategy_3b6a81ce-00b0-4c5a-9761-cb359932e97e",
"provider": "kamino",
"providerReference": "C7N2fqV2GvhFghDf5PJSwstyjedjLNCY1cm2Ak7TeU83",
"name": "Kamino Vault USDC",
"sourceKind": "defi",
"depositMints": ["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"],
"shareMint": "CeirL1mAehaM6g2EccgMUQwohNfnuyicskVQ4r9ekXk6",
"apyType": "variable",
"liquidityTerm": "instant",
"status": "active",
"depositSlippage": null,
"withdrawalSlippage": null,
"hostCluster": "devnet",
"fundable": true
}
],
"total": 6,
"page": 1,
"pageSize": 100
}
}
```
The catalog pages with `page` and `pageSize` (default 20, up to 100). Keep
reading pages until you have `total` strategies. Narrow the list with
`sourceKind`, `apyType`, or `liquidityTerm`.
## Check that it takes deposits
| Check | Pass when |
| --- | --- |
| `status` | `"active"`. Paused and deprecated strategies take no new deposits. |
| `fundable` | `true`. The strategy exists on your project's cluster. |
| `hostCluster` | Matches your project: `devnet` in sandbox, `mainnet-beta` in production. |
| Deposit token | `depositMints[0]` is the token a deposit takes. Fund the wallet with it. |
| Preview | A [deposit preview](/docs/reference/api/earn/create-earn-vault-deposit-preview) returns an empty `blockingIssues`. |
In sandbox, `?cluster=mainnet-beta` lists mainnet strategies for browsing.
They stay `fundable: false`: a sandbox project cannot deposit into them.
Your organization also needs each provider activated, once for all its
projects. Until it is, previews and builds return `403` with `FORBIDDEN` and
`details.reason` set to `provider_not_entitled`:
```json title="Response"
{
"error": {
"code": "FORBIDDEN",
"message": "Kamino requires manual activation for this organization.",
"details": { "reason": "provider_not_entitled" }
}
}
```
Ask your SDP contact to activate the provider for your organization.
## Read the slippage policy
`depositSlippage` and `withdrawalSlippage` say whether a transaction needs a
floor derived from a live quote:
| Policy | Deposit | Withdrawal |
| --- | --- | --- |
| `null` | Omit `minSharesOut`. | Omit `minAmountOut`. |
| `{ "quoteRequired": true, "defaultToleranceBps": 10 }` | Preview, then send `minSharesOut`. | Preview, then send `minAmountOut`. |
[Slippage floors](/docs/earn/integration-details#slippage-floors) shows how
to compute a floor exactly.
## Show rates and terms
| Field | How to show it |
| --- | --- |
| `currentApy` | A decimal rate: `"0.062"` is 6.2%. When absent, show "Unavailable", never 0%. |
| `apyType` | A `variable` rate moves with the strategy's returns; a `fixed` rate does not. |
| `liquidityTerm` | A label, not a promise. Read live [withdrawal options](/docs/earn/withdrawals#1-choose-a-route) before you offer a withdrawal route. |
| `redemptionDelayDays` | Expected timing, not a guaranteed deadline. |
| `riskMetadata` | Provider fees and risk facts, when the provider reports them. |
An APY describes a rate, not a guaranteed return. Holding a token in the
customer's own wallet does not guarantee principal, backing, or liquidity.
## Confirm eligibility
SDP access authorizes API calls, not investment. For restricted products,
confirm investor, jurisdiction, wallet, subscription, and redemption
requirements with the provider, for each customer and for the wallet that
will hold the position. One usable strategy does not make a provider
permissionless.
## Follow the funds
| Strategy shape | Money in | Money out |
| --- | --- | --- |
| Vault or lending market | One transaction exchanges the deposit token for shares. | Shares redeem under the provider's liquidity and withdrawal rules. |
| Yield-bearing token | One transaction acquires the token in the customer's wallet. | A sale depends on market liquidity; a redemption may need provider approval. |
| Provider order | A transaction pays into the provider's order. | The provider completes the order later. |
## Paused and deprecated strategies
Pausing or deprecating a strategy stops new deposits only. Existing positions
stay visible and withdrawable, subject to the provider's liquidity. See
[Withdraw funds](/docs/earn/withdrawals).
Next, [make your first deposit](/docs/earn/quickstart).
---
### Deposit funds
Source: https://platform.solana.com/docs/earn/deposits
> Preview, build, sign, submit, and track a deposit request by request, in cURL or TypeScript.
> **Earn is under active development.** Features, API endpoints, and docs may change.
A deposit takes six requests and one signature. This page shows each request
and its response, so you can build the flow in any language. For the shortest
path, start with the [Quickstart](/docs/earn/quickstart).
## Before you begin
You need the Quickstart's setup: `SDP_API_URL` for a beta or experimental
deployment, a sandbox API key with `earn.execute`, a funded test wallet in
`test-wallet.json`, and Node.js for signing. See
[Before you begin](/docs/earn/quickstart#before-you-begin). The cURL examples
also require jq 1.6 or later.
**cURL**
Work in the folder that holds `test-wallet.json`, with `@solana/kit` installed
for the signer:
```bash title="Terminal"
npm init -y && npm pkg set type=module && npm install @solana/kit@8
export SDP_API_KEY="sf_..."
export OWNER_ADDRESS="$(solana-keygen pubkey test-wallet.json)"
```
Save this signer as `sign.mjs` in the same folder. It signs SDP's transaction
without changing it and prints the signed transaction:
```javascript title="sign.mjs"
// Sandbox only: signs a base64 transaction with test-wallet.json.
import { readFile } from "node:fs/promises";
import {
createKeyPairFromBytes,
getBase64EncodedWireTransaction,
getBase64Encoder,
getTransactionDecoder,
partiallySignTransaction,
} from "@solana/kit";
const secretKey = new Uint8Array(JSON.parse(await readFile("test-wallet.json", "utf8")));
const keyPair = await createKeyPairFromBytes(secretKey);
const unsigned = getTransactionDecoder().decode(getBase64Encoder().encode(process.argv[2]));
const signed = await partiallySignTransaction([keyPair], unsigned);
process.stdout.write(getBase64EncodedWireTransaction(signed));
```
**TypeScript**
Use `earn-api.ts` and `test-wallet.ts` from the
[Quickstart](/docs/earn/quickstart#1-add-the-request-helper).
The steps below are one file, `deposit.ts`, that you run with
`npx tsx deposit.ts`.
## 1. Choose a strategy
List the catalog and keep an active, fundable strategy whose first deposit
mint is devnet USDC and needs no slippage floor.
[Strategies](/docs/earn/strategies) explains each check.
**cURL**
```bash title="Terminal"
curl --fail-with-body -sS "${SDP_API_URL:?Set SDP_API_URL}/v1/earn/strategies?pageSize=100" \
-H "Authorization: Bearer $SDP_API_KEY" > strategies.json
# The first active, fundable devnet USDC strategy that needs no deposit floor.
STRATEGY_ID="$(jq -er --arg mint "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU" '
[.data.strategies[] | select(.fundable and .status == "active"
and .depositSlippage == null and .depositMints[0] == $mint)][0].id
// error("No devnet USDC strategy is open for deposits")' strategies.json)" || exit 1
export STRATEGY_ID
export AMOUNT="25" # in the deposit token
echo "$STRATEGY_ID"
```
**TypeScript**
```typescript title="deposit.ts"
import { randomUUID } from "node:crypto";
import { earnRequest } from "./earn-api";
import { ownerAddress, signWithTestWallet } from "./test-wallet";
type Strategy = {
id: string;
status: string;
fundable: boolean;
depositMints: string[];
depositSlippage: unknown;
withdrawalSlippage: { defaultToleranceBps: number } | null;
};
const { strategies }: { strategies: Strategy[] } = await earnRequest(
"/v1/earn/strategies?pageSize=100",
);
const strategy = strategies.find(
(s) => s.fundable && s.status === "active" && s.depositSlippage === null
&& s.depositMints[0] === "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
);
if (!strategy) throw new Error("No devnet USDC strategy is open for deposits");
const amount = "25"; // in the deposit token
```
## 2. Preview the deposit
[`POST /v1/earn/vault-deposit-previews`](/docs/reference/api/earn/create-earn-vault-deposit-preview)
quotes the shares the deposit would receive right now. It moves nothing.
**cURL**
```bash title="Terminal"
curl --fail-with-body -sS "${SDP_API_URL:?Set SDP_API_URL}/v1/earn/vault-deposit-previews" \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -nc --arg strategyId "$STRATEGY_ID" --arg amount "$AMOUNT" \
'{strategyId: $strategyId, amount: $amount}')" > preview.json
jq -e '.data.blockingIssues == []' preview.json || echo "Resolve blockingIssues first" >&2
```
**TypeScript**
```typescript title="deposit.ts (continued)"
const preview = await earnRequest("/v1/earn/vault-deposit-previews", {
strategyId: strategy.id,
amount,
});
if (preview.blockingIssues.length > 0) {
throw new Error(`Preview blocked: ${JSON.stringify(preview.blockingIssues)}`);
}
```
```json title="Response (trimmed)"
{
"data": {
"strategyId": "earn_strategy_3b6a81ce-00b0-4c5a-9761-cb359932e97e",
"sharesOut": "25",
"shareDecimals": 6,
"blockingIssues": []
}
}
```
Resolve every `blockingIssues` entry before you build, even on a `200`. A
preview does not reserve the rate. For a strategy whose `depositSlippage` is
not null, use `sharesOut` to set `minSharesOut`, as
[Slippage floors](/docs/earn/integration-details#slippage-floors) shows.
Ignore the response's `feeSponsored`: it describes Treasury custody-wallet
movements, and your customer's wallet pays its own fees unless you set a
`feePayer`.
## 3. Build the transaction
[`POST /v1/earn/external-wallet/deposit-transactions`](/docs/reference/api/earn/create-earn-external-wallet-deposit-transaction)
returns an unsigned transaction for the customer's wallet.
**cURL**
```bash title="Terminal"
curl --fail-with-body -sS "${SDP_API_URL:?Set SDP_API_URL}/v1/earn/external-wallet/deposit-transactions" \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -nc --arg strategyId "$STRATEGY_ID" --arg ownerAddress "$OWNER_ADDRESS" \
--arg amount "$AMOUNT" \
'{strategyId: $strategyId, ownerAddress: $ownerAddress, amount: $amount}')" \
> build.json
```
**TypeScript**
```typescript title="deposit.ts (continued)"
const { transaction: built } = await earnRequest(
"/v1/earn/external-wallet/deposit-transactions",
{ strategyId: strategy.id, ownerAddress, amount },
);
```
```json title="Response (trimmed)"
{
"data": {
"transaction": {
"transactionId": "earn_external_wallet_transaction_b116f3ed-5f19-41b1-85cd-8afe6b37875d",
"transaction": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...",
"lastValidBlockHeight": "14846",
"sponsored": false,
"ownerAddress": "Af89Mkuvo6AcLDbs7Fxs1FPsnnbD9KrY9NfmaFXHqmPo",
"provider": "kamino",
"tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"amount": "25",
"minSharesOut": null
}
}
}
```
Keep `transactionId` and the base64 `transaction` together. The network
accepts the transaction until block height `lastValidBlockHeight`, about a
minute after the build, so sign and submit promptly. The customer's wallet
pays the network fee, plus rent for the accounts a first deposit creates,
unless you name a `feePayer`; see
[Fees and sponsorship](/docs/earn/integration-details#fees-and-sponsorship).
## 4. Sign it
The customer's wallet signs the exact bytes, without changing or broadcasting
them. Here the test wallet stands in. In production, sign in your app, as the
[Quickstart](/docs/earn/quickstart#sign-in-your-customers-wallet) shows.
**cURL**
```bash title="Terminal"
export TRANSACTION_ID="$(jq -r '.data.transaction.transactionId' build.json)"
export SIGNED_TRANSACTION="$(node sign.mjs "$(jq -r '.data.transaction.transaction' build.json)")"
test -n "$SIGNED_TRANSACTION" || echo "Signing failed: is @solana/kit installed here?" >&2
```
**TypeScript**
```typescript title="deposit.ts (continued)"
const signedTransaction = await signWithTestWallet(built.transaction);
```
## 5. Submit it
[`POST /v1/earn/external-wallet/deposits`](/docs/reference/api/earn/create-earn-external-wallet-deposit)
takes the signed transaction. SDP checks it against the build, records the
deposit, then broadcasts it. The `Idempotency-Key` header is required.
**cURL**
```bash title="Terminal"
# Save the idempotency key with the signed body before the first send; retry with both.
export IDEMPOTENCY_KEY="$(node -e 'console.log(crypto.randomUUID())')"
jq -nc --arg transactionId "$TRANSACTION_ID" --arg signedTransaction "$SIGNED_TRANSACTION" \
'{transactionId: $transactionId, signedTransaction: $signedTransaction}' > submit.json
curl --fail-with-body -sS "${SDP_API_URL:?Set SDP_API_URL}/v1/earn/external-wallet/deposits" \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
--data-binary @submit.json > deposit.json
```
**TypeScript**
```typescript title="deposit.ts (continued)"
// Save the idempotency key with the signed body before the first send; retry with both.
const idempotencyKey = randomUUID();
const { deposit } = await earnRequest(
"/v1/earn/external-wallet/deposits",
{ transactionId: built.transactionId, signedTransaction },
idempotencyKey,
);
```
Persist the returned `positionId` with the selected strategy's
`withdrawalSlippage` in your backend's position record. Keep `null` explicitly
when no floor is supported. A strategy can leave the catalog while its
positions remain withdrawable; use this saved policy when building an exit.
```json title="Response (trimmed)"
{
"data": {
"deposit": {
"movementId": "earn_movement_505a6c34-f804-4abd-b6a9-315d031bdf1f",
"positionId": "earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b",
"direction": "deposit",
"status": "submitted",
"settlement": "atomic",
"signature": "3dhkSPLiQZQPXL96fWzMdWJtdbtAffkBKXw2xhsYbV3VNSnYrVAcdx1czsnLsE1ou4SvLecGdSYe4QpAuZdfjJAf",
"amount": "25",
"tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"tokenAmount": null,
"replayed": false
}
}
}
```
On some strategies a deposit's `tokenAmount` stays null until `finalized`, so
read the deposited amount there. If the response is lost or times out, send
the same body with the same idempotency key.
SDP returns the original deposit, in its current state, with
`replayed: true` instead of depositing twice.
[Retry safely](/docs/earn/integration-details#retry-safely) covers the other
cases.
## 6. Track it to finality
Poll [`GET /v1/earn/external-wallet/movements/{movementId}`](/docs/reference/api/earn/get-earn-external-wallet-movement)
until the status is `finalized` or `failed`.
**cURL**
```bash title="Terminal"
MOVEMENT_ID="$(jq -r '.data.deposit.movementId' deposit.json)"
# Every second for a minute, then every five seconds, for up to three minutes.
for attempt in $(seq 84); do
curl --fail-with-body -sS "${SDP_API_URL:?Set SDP_API_URL}/v1/earn/external-wallet/movements/$MOVEMENT_ID" \
-H "Authorization: Bearer $SDP_API_KEY" > movement.json
jq -e '.data.movement.status | IN("finalized", "failed")' movement.json >/dev/null && break
if [ "$attempt" -le 60 ]; then sleep 1; else sleep 5; fi
done
jq '.data.movement | {status, settlement, tokenAmount, failureReason}' movement.json
```
**TypeScript**
```typescript title="deposit.ts (continued)"
import { waitForMovement } from "./earn-api";
const movement = await waitForMovement(deposit.movementId);
if (movement.status === "failed") throw new Error(movement.failureReason ?? "Deposit failed");
```
```json title="Response (trimmed)"
{
"data": {
"movement": {
"movementId": "earn_movement_505a6c34-f804-4abd-b6a9-315d031bdf1f",
"direction": "deposit",
"status": "finalized",
"settlement": "atomic",
"amount": "25",
"tokenAmount": "25",
"tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"failureReason": null
}
}
}
```
With `settlement: "atomic"`, you may show the deposit as done at `confirmed`
while you keep tracking to `finalized`. A `provider_order` deposit stays
`confirmed` until the provider completes it, so treat it as done only at
`finalized`; see
[How a movement settles](/docs/earn/concepts#how-a-movement-settles).
## 7. Read the position
[`GET /v1/earn/external-wallet/positions`](/docs/reference/api/earn/list-earn-external-wallet-positions)
returns the wallet's live positions.
**cURL**
```bash title="Terminal"
curl --fail-with-body -sS --get "${SDP_API_URL:?Set SDP_API_URL}/v1/earn/external-wallet/positions" \
-H "Authorization: Bearer $SDP_API_KEY" \
--data-urlencode "ownerAddress=$OWNER_ADDRESS"
```
**TypeScript**
```typescript title="deposit.ts (continued)"
const { positions } = await earnRequest(
`/v1/earn/external-wallet/positions?${new URLSearchParams({ ownerAddress })}`,
);
console.log(JSON.stringify(positions, null, 2));
```
```json title="Response (trimmed)"
{
"data": {
"ownerAddress": "Af89Mkuvo6AcLDbs7Fxs1FPsnnbD9KrY9NfmaFXHqmPo",
"positions": [
{
"id": "earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b",
"label": "Kamino Vault USDC",
"tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"shares": "25",
"withdrawableShares": "25",
"tokenValue": "25"
}
],
"hasMore": false,
"nextCursor": null
}
}
```
Show a missing `shares` or `tokenValue` as unavailable, never as zero.
[Balances and earnings](/docs/earn/balances) covers earnings, history, and
pagination.
## Errors
| Status | Code | When | What to do |
| --- | --- | --- | --- |
| `400` | `BAD_REQUEST` | The submit has no `Idempotency-Key`, or the signed transaction differs from the build, or a signature is missing or invalid. | Send a key. Otherwise build again and sign the exact bytes. |
| `403` | `INSUFFICIENT_PERMISSIONS` | The API key lacks the grant the route needs, named in `details.action`. | Use an API key with `earn.execute`. |
| `403` | `FORBIDDEN` | `details.reason` is `provider_not_entitled`: your organization is not activated for the provider. | Ask your SDP contact to activate it. |
| `404` | `NOT_FOUND` | SDP has no build with that `transactionId`. | Check the ID, or build again. |
| `409` | `CONFLICT` | The idempotency key was already used for a different build, or the same build arrives under a new idempotency key. | Retry with the original build and idempotency key. Use a new idempotency key only for a new deposit. |
These codes replace the ones on the shared
[Idempotency](/docs/developing-with-sdp/idempotency) page; see
[Retry safely](/docs/earn/integration-details#retry-safely).
Next, [withdraw funds](/docs/earn/withdrawals) or
[ship Embedded Yield](/docs/guides/embedded-yield).
---
### Balances and earnings
Source: https://platform.solana.com/docs/earn/balances
> Show each customer's live positions, movement history, and exact earnings, and never show a missing value as zero.
> **Earn is under active development.** Features, API endpoints, and docs may change.
Read balances from live positions and history from recorded movements. Each
per-wallet read takes the customer's wallet as `ownerAddress`, so your
backend decides which wallet a signed-in customer may see. The examples read
it from `OWNER_ADDRESS`, as in [Deposit funds](/docs/earn/deposits#before-you-begin).
Set `SDP_API_URL` and `SDP_API_KEY` as shown in the
[Quickstart](/docs/earn/quickstart#before-you-begin).
## Positions
[`GET /v1/earn/external-wallet/positions`](/docs/reference/api/earn/list-earn-external-wallet-positions)
returns the wallet's live positions.
```bash title="Terminal"
curl --fail-with-body -sS --get "${SDP_API_URL:?Set SDP_API_URL}/v1/earn/external-wallet/positions" \
-H "Authorization: Bearer $SDP_API_KEY" \
--data-urlencode "ownerAddress=$OWNER_ADDRESS"
```
| Field | Show it as |
| --- | --- |
| `id` | The position ID. Withdrawals name it. |
| `label` | The strategy's display name. |
| `shares` | The wallet's live share balance. |
| `withdrawableShares` | The shares that can leave right now. |
| `tokenValue` | The live value in the deposit token (`tokenMint`). Every Earn deposit token is a USD stablecoin, so at par this reads as dollars. |
| `unlockTimestamp` | When locked shares become withdrawable, in Unix seconds. Null or absent when nothing is locked. |
Positions come newest first, and repeat deposits into one strategy add to the
same position.
- **A missing live value is unavailable.** When the provider read fails,
`shares`, `withdrawableShares`, and `tokenValue` are left out. Show
"Unavailable" and disable withdrawals until a fresh read succeeds. Never
show zero or an old quote.
- **A wallet with no positions returns an empty list,** not `404`. A fully
withdrawn position drops out of the list.
- **An open withdrawal request holds shares outside the position.** They
leave `shares` and `tokenValue` until the request pays out or is canceled,
so show its `quotedAssets` as an expected payout.
- **`parIntermediate`** appears while the wallet holds an operator
redemption's intermediate token outside an open request. It keeps the
position open and counts toward earnings' `currentValue`. See
[Operator redemptions](/docs/earn/withdrawals#operator-redemptions).
## Movements
Each deposit and withdrawal is a movement. Read one with
[`GET /v1/earn/external-wallet/movements/{movementId}`](/docs/reference/api/earn/get-earn-external-wallet-movement),
or list a wallet's history, newest first, with
[`GET /v1/earn/external-wallet/movements`](/docs/reference/api/earn/list-earn-external-wallet-movements).
```bash title="Terminal"
curl --fail-with-body -sS --get "${SDP_API_URL:?Set SDP_API_URL}/v1/earn/external-wallet/movements" \
-H "Authorization: Bearer $SDP_API_KEY" \
--data-urlencode "ownerAddress=$OWNER_ADDRESS"
```
```json title="Response (trimmed)"
{
"data": {
"ownerAddress": "HM4yQMRMc3gd6TwgraU7r4f4zpJRZmDomzkSPDWmmfi8",
"movements": [
{
"movementId": "earn_movement_a6aed3f5-4f89-4dc6-9d4b-0864021bd80d",
"direction": "deposit",
"status": "finalized",
"settlement": "atomic",
"amount": "25",
"denomination": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"tokenAmount": "25",
"tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"createdAt": "2026-11-06T18:17:55.220Z",
"confirmedAt": "2026-11-06T18:17:55.374Z",
"settledAt": "2026-11-06T18:17:58.425Z"
}
],
"hasMore": false,
"nextCursor": null
}
}
```
Show `tokenAmount` in `tokenMint` units for both directions. On a withdrawal,
`amount` is the share quantity, not the payout, and `tokenAmount` stays null
until SDP observes the payout. `confirmedAt` is when the network confirmed
the movement, and `settledAt` is when it finalized. Label requested amounts
and expected queue payouts as such.
[How a movement settles](/docs/earn/concepts#how-a-movement-settles) explains
each status and `settlement`.
## Earnings
[`GET /v1/earn/external-wallet/earnings`](/docs/reference/api/earn/get-earn-external-wallet-earnings)
returns the wallet's balance and earnings for each deposit token.
```bash title="Terminal"
curl --fail-with-body -sS --get "${SDP_API_URL:?Set SDP_API_URL}/v1/earn/external-wallet/earnings" \
-H "Authorization: Bearer $SDP_API_KEY" \
--data-urlencode "ownerAddress=$OWNER_ADDRESS"
```
```json title="Response (trimmed)"
{
"data": {
"earnings": {
"ownerAddress": "HM4yQMRMc3gd6TwgraU7r4f4zpJRZmDomzkSPDWmmfi8",
"positionCount": 1,
"unavailablePositionCount": 0,
"totalsByToken": [
{
"tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"currentValue": "25",
"totalDeposited": "25",
"totalWithdrawn": "0",
"earned": "0"
}
]
}
}
}
```
`currentValue` is the sum of the positions' `tokenValue`, plus any
intermediate tokens. `earned` is
`currentValue + totalWithdrawn - totalDeposited`, and it appears only when it
is exact. It can be negative: right after a deposit, the strategy's share
rounding can leave it slightly below zero. When it is left out,
`earnedUnavailableReason` says why:
| Reason | Meaning |
| --- | --- |
| `live_value_unavailable` | A position's live value could not be read. |
| `movements_pending` | A deposit or withdrawal is still settling. |
| `withdrawals_pending` | An open withdrawal request holds shares that have not paid out yet. |
| `deposits_not_valued` | SDP did not observe a finalized deposit's amount. |
| `withdrawals_not_valued` | SDP did not observe a finalized withdrawal's payout. |
Show a dash, never `$0`, while `earned` is missing. Three limits apply:
- **Only held positions count.** A fully withdrawn position drops out of
earnings but stays in the movement history.
- **Outside transfers count.** The live value reads the wallet's whole balance
in the strategy, including shares acquired outside SDP, while deposits and
withdrawals come only from SDP's records.
- **It is not an accounting ledger.** It gives no lifetime realized returns or
daily accrual history. Reconcile against your own records for statements.
## Position totals
[`GET /v1/earn/external-wallet/positions/summary`](/docs/reference/api/earn/get-earn-external-wallet-position-summary)
returns live totals across every customer wallet in your project, by token and
by strategy. It takes no `ownerAddress`.
```bash title="Terminal"
curl --fail-with-body -sS "${SDP_API_URL:?Set SDP_API_URL}/v1/earn/external-wallet/positions/summary" \
-H "Authorization: Bearer $SDP_API_KEY"
```
Wallet addresses are personal data, so they are left out unless you pass
`includeOwnerAddresses=true`. Add `includePositions=true` to also get each
wallet's positions; it requires the address opt-in, or the request returns
`400`.
Never send this summary, or your API key, to a customer's browser.
## Paginate every list
| List | Paging |
| --- | --- |
| Strategies | `page` and `pageSize` (default 20, up to 100), until you have `total`. |
| Positions, movements, withdrawal requests | `limit` (default 20, up to 100) and `before`, set to the previous page's `nextCursor`, until `hasMore` is `false`. |
Treat a missing or repeated cursor as an error: a silently short list hides a
customer's money. Earn lists differ from the shared
[Pagination](/docs/reference/conventions/pagination) conventions: the catalog
returns `total`, `page`, and `pageSize` inside `data`, and the cursor lists
take their cursor as `before`.
---
### Withdraw funds
Source: https://platform.solana.com/docs/earn/withdrawals
> Choose a live withdrawal route, withdraw instantly or by request, and confirm the payout reached the customer's wallet.
> **Earn is under active development.** Features, API endpoints, and docs may change.
A withdrawal spends shares from a position and pays the deposit token back to
the wallet that owns it. Some routes pay in the same transaction; others pay
later. Read the live routes first, then follow the one your customer chooses.
Set `SDP_API_URL` and `SDP_API_KEY` as shown in the
[Quickstart](/docs/earn/quickstart#before-you-begin).
## 1. Choose a route
[`POST /v1/earn/external-wallet/withdrawal-options`](/docs/reference/api/earn/get-earn-external-wallet-withdrawal-options)
reads each route from live provider state.
```bash title="Terminal"
curl --fail-with-body -sS "${SDP_API_URL:?Set SDP_API_URL}/v1/earn/external-wallet/withdrawal-options" \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc"}'
```
```json title="Response (trimmed)"
{
"data": {
"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc",
"instant": true,
"providerOrder": false,
"queued": true,
"queueAsset": {
"assetMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"allowWithdrawals": true,
"secondsToMaturity": 3600,
"minimumSecondsToDeadline": 60,
"maximumSecondsToDeadline": 7776000,
"minimumDiscountBps": 0,
"maximumDiscountBps": 100,
"minimumShares": "0.000001",
"shareDecimals": 6
},
"parRedemption": null
}
}
```
| Route | When it is offered | Pays out |
| --- | --- | --- |
| Instant withdrawal | `instant: true` | In the same transaction. |
| Provider order | `providerOrder: true` | Later, when the provider completes the order. |
| Solver queue | `queued: true`, with live terms in `queueAsset` | Later, at a discount, when a solver fulfills the request. |
| Operator redemption | `parRedemption` is present | Later, at par, when the operator settles the request. |
A position offers an instant withdrawal or a provider order, never both, and
the same withdrawal endpoints build whichever it offers. When more than one
route is offered, let your customer choose, and keep their choice. Never infer
a route from the strategy's `liquidityTerm`. A failed read or missing terms
means the route is unavailable, not that it pays zero.
## 2. Withdraw instantly
An instant withdrawal works like a deposit: preview, build, sign, submit, and
track. Withdraw at most the position's live `withdrawableShares`: a preview
does not check the wallet's balance.
| Step | Request |
| --- | --- |
| Preview | [`POST /v1/earn/external-wallet/withdrawal-previews`](/docs/reference/api/earn/create-earn-external-wallet-withdrawal-preview) |
| Build | [`POST /v1/earn/external-wallet/withdrawal-transactions`](/docs/reference/api/earn/create-earn-external-wallet-withdrawal-transaction) |
| Submit | [`POST /v1/earn/external-wallet/withdrawals`](/docs/reference/api/earn/create-earn-external-wallet-withdrawal), with an `Idempotency-Key` |
| Track | [`GET /v1/earn/external-wallet/movements/{movementId}`](/docs/reference/api/earn/get-earn-external-wallet-movement) |
The example uses the Quickstart's `earn-api.ts` and `test-wallet.ts`, and
`floor.ts` from [Slippage floors](/docs/earn/integration-details#slippage-floors).
Call `withdraw` with the `positionId` and `withdrawalSlippage` saved in your
backend after the deposit, plus the share amount your customer chose:
```typescript title="withdraw.ts"
import { randomUUID } from "node:crypto";
import { earnRequest, waitForMovement } from "./earn-api";
import { floorForTolerance } from "./floor";
import { ownerAddress, signWithTestWallet } from "./test-wallet";
type SavedPosition = {
positionId: string;
withdrawalSlippage: { defaultToleranceBps: number } | null;
};
type Position = { id: string; withdrawableShares?: string };
export async function withdraw(savedPosition: SavedPosition, shares: string) {
let position: Position | undefined;
let before: string | null = null;
do {
const query = new URLSearchParams({ ownerAddress, limit: "100" });
if (before !== null) query.set("before", before);
const page: { positions: Position[]; nextCursor: string | null } = await earnRequest(
`/v1/earn/external-wallet/positions?${query}`,
);
position = page.positions.find((p) => p.id === savedPosition.positionId);
before = page.nextCursor;
} while (!position && before !== null);
if (!position || position.withdrawableShares === undefined) {
throw new Error("The saved position cannot withdraw right now");
}
const preview = await earnRequest("/v1/earn/external-wallet/withdrawal-previews", {
positionId: position.id,
shares,
});
if (preview.blockingIssues.length > 0) {
throw new Error(`Preview blocked: ${JSON.stringify(preview.blockingIssues)}`);
}
// Send a floor only when the saved policy takes one; never require a catalog listing to exit.
const policy = savedPosition.withdrawalSlippage;
const minAmountOut = policy
? floorForTolerance(preview.assetsOut, preview.assetDecimals, policy.defaultToleranceBps)
: undefined;
const { transaction: built } = await earnRequest(
"/v1/earn/external-wallet/withdrawal-transactions",
{ positionId: position.id, shares, minAmountOut },
);
const signedTransaction = await signWithTestWallet(built.transaction);
const { withdrawal } = await earnRequest(
"/v1/earn/external-wallet/withdrawals",
{ transactionId: built.transactionId, signedTransaction },
randomUUID(),
);
return waitForMovement(withdrawal.movementId);
}
```
```json title="Preview response"
{
"data": {
"positionId": "earn_position_e3053c7b-e41e-4f21-b4dc-e229ab95f40b",
"assetsOut": "10",
"assetDecimals": 6,
"blockingIssues": []
}
}
```
The preview quotes the payout; it does not reserve it. The build requires
`minAmountOut` when the strategy's `withdrawalSlippage` is not null, and
refuses it when the policy is null. The position supplies the vault and mints,
so a delisted strategy stays exitable. Keep the deposit-time policy with the
position; do not require the strategy to appear in today's catalog.
If the build reports that the floor policy changed, refresh the policy with
your SDP contact before retrying. Never guess a policy from a missing catalog
row.
At `finalized`, `tokenAmount` is the payout SDP observed. Check the customer's
token balance onchain if you need independent proof, then refresh
[balances](/docs/earn/balances).
## Provider orders
A provider order takes an exact share amount and pays out after the provider
prices and settles it. Its preview may return `501`, which does not block the
build. No floor can bound a later price, so omit `minAmountOut` when the
policy is null.
The withdrawal's `settlement` is `provider_order`. Confirmation proves the
order transaction, not the payout: the movement stays `confirmed` until the
provider completes the order. Show it as pending until it reaches `finalized`;
see [How a movement settles](/docs/earn/concepts#how-a-movement-settles).
## Asynchronous withdrawals
A solver queue or an operator redemption creates a withdrawal request. The
request transaction moves shares, never the payout, so track the request until
it pays out or is canceled.
### Solver queues
The request escrows shares. After `secondsToMaturity`, a solver may fulfill it
at the agreed discount until the deadline. Before the customer confirms, show
the live terms from `queueAsset`: minimum shares, the discount range, maturity,
and the deadline range.
Send `shares`, `discountBps`, and `deadlineSeconds`, each inside the live
terms; `mechanism` defaults to `solverQueue`. The deadline counts from
maturity, not from creation, and both run on the cluster's clock:
`maturityTimestamp` and `deadlineTimestamp` are onchain Unix times.
- **The discount is your customer's cost.** A larger `discountBps` makes a
solver more likely to fill the request, and the customer receives that much
less.
- **The deadline is how long solvers have.** A longer `deadlineSeconds` gives
them more time, but keeps the shares escrowed longer if none fills it.
- **Open requests leave the balance.** Escrowed shares drop out of the
position's `shares` and earnings' `currentValue`, and earnings report
`withdrawals_pending`. Show the request's `quotedAssets` as an expected
payout.
### Operator redemptions
The request converts shares into an intermediate token and authorizes the
operator to settle it for the deposit token at par. Read the live
`parRedemption` terms: minimum shares, `intermediateMint`, `assetMint`, and
whether cancellation is allowed.
Send `mechanism: "operatorRedemption"` with either `shares` or
`intermediateAmount` from an existing `parIntermediate` holding, never both.
A null `minimumShares` means the conversion rate is unavailable: the preview
reports a blocking issue.
### Create a request
| Step | Request |
| --- | --- |
| Preview | [`POST /v1/earn/external-wallet/queued-withdrawal-previews`](/docs/reference/api/earn/create-earn-external-wallet-queued-withdrawal-preview) |
| Build | [`POST /v1/earn/external-wallet/withdrawal-request-transactions`](/docs/reference/api/earn/create-earn-external-wallet-withdrawal-request-transaction) |
| Submit | [`POST /v1/earn/external-wallet/withdrawal-requests`](/docs/reference/api/earn/create-earn-external-wallet-withdrawal-request), with an `Idempotency-Key` |
Resolve every `blockingIssues` entry in the preview, then build, sign, and
submit as for an instant withdrawal. Save the returned `withdrawalRequestId`.
Keep the signed request and its idempotency key until the request resolves.
The wallet may already have broadcast it, so SDP accepts it even after its
build expires:
| Submit answer | What to do |
| --- | --- |
| `409 CONFLICT` on a late request | SDP released the build's reservation and accepts the request once the exact transaction is finalized. Retry the same bytes and key. For an operator redemption whose cycle already closed, contact support instead. |
| `400 TRANSACTION_FAILED` | The transaction finalized as failed. Build a new request. |
| `503` | SDP could not finish checking the request's history. Retry the same bytes and key, and contact support if it persists. |
### Track a request
Poll [`GET /v1/earn/external-wallet/withdrawal-requests/{withdrawalRequestId}`](/docs/reference/api/earn/get-earn-external-wallet-withdrawal-request)
every minute or two. Status values spell it `cancelled`.
| Status | Meaning | What to show |
| --- | --- | --- |
| `creating` | SDP recorded the signed request; its chain outcome is unresolved. | Pending. |
| `pending` | Waiting for maturity or for the operator. | Pending, with the expected timing. |
| `fulfillable` | A solver may fulfill it now. | Pending. |
| `expiredCancelable` | The solver deadline passed without fulfillment. | Offer cancellation. |
| `cancelling` | A cancellation was submitted; a fulfillment can still race it. | Canceling. |
| `closedOrUnknown` | The request closed, and SDP has not yet proved a payout or a cancellation. | Pending; keep tracking. |
| `fulfilled` | SDP observed the payout. `assetsPaid` holds the amount. | Done. |
| `cancelled` | SDP observed the cancellation. | Canceled; refresh holdings. |
| `failed` | It did not happen: SDP decides that only after repeated checks find no trace of it. `failureReason` says why. | Failed. |
Only `fulfilled`, `cancelled`, and `failed` are final, and only `fulfilled`
pays out. Never credit `quotedAssets`, a request transaction, or a
cancellation as received funds. A solver queue request that no solver takes
stays `fulfillable` until its deadline, then becomes `expiredCancelable`, and
its shares stay escrowed until the customer cancels it.
On startup and after reconnecting, restore every open request:
[`GET /v1/earn/external-wallet/withdrawal-requests?ownerAddress=&settled=false`](/docs/reference/api/earn/list-earn-external-wallet-withdrawal-requests),
reading every page.
**warn**
Sign withdrawal requests and cancellations without broadcasting them, and
submit them through SDP promptly. If a wallet broadcasts one itself and SDP
first sees it after the build expires, SDP may not be able to track it.
### Cancel a request
Offer cancellation for a solver queue request only at `expiredCancelable`,
never from a client clock; building one earlier returns `409 CONFLICT`. An
operator redemption can be canceled while `pending` if its live terms allow
it. Refresh the request first: a fulfillment can race the cancellation.
| Step | Request |
| --- | --- |
| Build | [`POST /v1/earn/external-wallet/withdrawal-request-cancel-transactions`](/docs/reference/api/earn/create-earn-external-wallet-withdrawal-request-cancel-transaction) |
| Submit | [`POST /v1/earn/external-wallet/withdrawal-request-cancellations`](/docs/reference/api/earn/create-earn-external-wallet-withdrawal-request-cancellation) |
Both steps take `earn.manage`, and cancellation stays available after
production access is removed or new deposits are disabled. Use a new
`Idempotency-Key` for the cancellation. A canceled solver request returns
the escrowed shares; a canceled operator redemption leaves the intermediate
token, not the original shares. Refresh holdings before another withdrawal.
---
## Earn: Integrate
### Embedded Yield
Source: https://platform.solana.com/docs/guides/embedded-yield
> Build customer-signed deposits, balances, and withdrawals into your app, from first request to go-live.
> **Earn is under active development.** Features, API endpoints, and docs may change.
Embedded Yield lets your customers earn from their own wallets inside your
app. Your backend calls SDP with your API key, your app asks the customer's
wallet to sign, and SDP verifies, broadcasts, and tracks every movement. SDP
never holds the customer's key.
New to Earn? Run the [Quickstart](/docs/earn/quickstart) first, then come
back to build the full integration.
## How the pieces fit
| Part | Does | Never |
| --- | --- | --- |
| Your backend | Holds the API key. Previews, builds, submits, and reads. Decides which customer may use which wallet. | Sends the API key to a browser or mobile app. |
| Your app | Shows quotes and status. Asks the customer's wallet to sign. | Broadcasts the transaction. |
| The customer's wallet | Signs the exact transaction SDP built. Owns the position and receives payouts. | Shares its private key. |
| SDP | Builds each transaction, verifies the signatures, records the movement, broadcasts it, and tracks it to finality. | Holds the customer's key or pools their funds. |
A deposit then runs: your backend builds, your app collects the signature,
your backend submits, and your app shows the result as your backend tracks
it.
## Authenticate on your backend
Create an API key in each project with the grants your integration uses:
| Grant | Allows |
| --- | --- |
| `earn.read` | Strategies, previews, positions, movements, earnings, and withdrawal requests. |
| `earn.execute` | Builds and submits for deposits, withdrawals, and withdrawal requests. Includes `earn.read`. |
| `earn.manage` | Building and submitting a withdrawal request cancellation. Includes `earn.read`. |
`earn.execute` and `earn.manage` do not include each other. The backend
module reads one `SDP_API_KEY`, so give that key every grant you call through
it. See [Manage API keys](/docs/developing-with-sdp/manage-api-keys).
Withdrawals and cancellations stay available after production access is
removed or new deposits are disabled. A suspended organization still cannot
authenticate, and Earn runs only on deployments whose release channel
includes it.
Your API key can read every customer wallet your project has recorded. Before
any request, check that the signed-in customer owns the `ownerAddress` you
send, for example by verifying a message the wallet signed when the customer
linked it. Custody-wallet scopes on the API key do not apply to customer wallets:
the customer's signature authorizes each transaction.
## Get the backend module
The dashboard generates a TypeScript module for a strategy with every call
this guide uses:
1. In the dashboard, open **Markets** > **Embedded Yield** > **Integrate**.
2. Choose a strategy. Only fundable strategies can be selected.
3. Click **Copy all code**, and save it as a server-only file such as
`earn.ts`. Set `SDP_API_KEY` in your backend's environment.
The module holds the strategy ID and the API URL for the project you
generated it in, so generate it again for production. It needs Node.js 20.18
or later, and `@solana/kit` only for its fee sponsor signer.
| Task | Module function |
| --- | --- |
| List the catalog | `listEarnStrategies` |
| Quote a deposit | `previewEarnDeposit` |
| Build a deposit | `buildEarnDepositTransaction` |
| Collect signatures | `signEarnTransaction`, `createSponsorSigner` |
| Submit a deposit | `submitEarnDeposit` |
| Track a movement | `waitForEarnMovement`, `getEarnMovement` |
| Read balances | `listEarnPositions` (every page), `getEarnEarnings`, `listEarnMovements` (one page per call) |
| Withdraw instantly | `previewEarnWithdrawal`, `buildEarnWithdrawalTransaction`, `submitEarnWithdrawal` |
| Choose a route | `getEarnWithdrawalOptions` |
| Request a withdrawal | `previewEarnQueuedWithdrawal`, `previewEarnParRedemption`, `buildEarnQueuedWithdrawalRequest`, `buildEarnParRedemptionRequest`, `submitEarnWithdrawalRequest` |
| Track requests | `getEarnWithdrawalRequest`, `waitForEarnQueuedWithdrawal`, `listPendingEarnQueuedWithdrawals` |
| Cancel a request | `buildEarnWithdrawalRequestCancellation`, `submitEarnWithdrawalRequestCancellation` |
When SDP refuses a request, a module function throws an `EarnApiError` with
`status`, `code`, `details`, and `retryAfter`. Branch on those, never on the
message.
Prefer raw requests? Every function maps to one endpoint in
[Deposit funds](/docs/earn/deposits) and
[Withdraw funds](/docs/earn/withdrawals).
## Make a deposit
| Step | Module function | Endpoint |
| --- | --- | --- |
| 1. Quote | `previewEarnDeposit` | [`POST /v1/earn/vault-deposit-previews`](/docs/reference/api/earn/create-earn-vault-deposit-preview) |
| 2. Build | `buildEarnDepositTransaction` | [`POST /v1/earn/external-wallet/deposit-transactions`](/docs/reference/api/earn/create-earn-external-wallet-deposit-transaction) |
| 3. Sign | `signEarnTransaction` | In the customer's wallet |
| 4. Submit | `submitEarnDeposit` | [`POST /v1/earn/external-wallet/deposits`](/docs/reference/api/earn/create-earn-external-wallet-deposit) |
| 5. Track | `waitForEarnMovement` | [`GET /v1/earn/external-wallet/movements/{movementId}`](/docs/reference/api/earn/get-earn-external-wallet-movement) |
`buildEarnDepositTransaction` previews the deposit itself when the strategy
needs a floor; call `previewEarnDeposit` too when you want to show your
customer a quote first.
`signEarnTransaction(built, customerSigner, sponsorSigner?)` passes the
base64 transaction to your signer, checks that the signed message is
unchanged, and adds your sponsor's signature when the build named a
`feePayer`. `createSponsorSigner(secretKey, feePayerAddress)` makes that
sponsor signer from your fee payer's 64-byte secret key, the array a Solana
CLI keypair file holds. The Quickstart shows a
[customer signer for a browser wallet](/docs/earn/quickstart#sign-in-your-customers-wallet).
Three rules keep deposits safe:
- **Sign exactly what SDP built.** The wallet must not change or broadcast
the transaction. SDP refuses a changed one with `400`.
- **Save before you submit.** Store the idempotency key with the signed
transaction, then submit. After an uncertain response, retry with both
unchanged; see [Retry safely](/docs/earn/integration-details#retry-safely).
- **Track to the end.** Save `movementId` and track it until `finalized` or
`failed`.
## Show balances
Read positions for withdrawals, earnings for the totals your customer sees,
and movements for their history. [Balances and earnings](/docs/earn/balances)
covers every field. Two rules matter most:
- Show a missing live value as unavailable, never as zero.
- Read every page of a list. A short list hides money.
## Withdraw
1. Read the position's live `withdrawableShares` with `listEarnPositions`,
then its routes with `getEarnWithdrawalOptions`.
2. For an instant withdrawal, preview, build, sign, submit, and track, as for
a deposit.
3. For a solver queue or operator redemption, create a withdrawal request and
track it to `fulfilled`, `cancelled`, or `failed`.
4. On startup, restore every open request with
`listPendingEarnQueuedWithdrawals`.
| Step | Module function | Endpoint |
| --- | --- | --- |
| Choose a route | `getEarnWithdrawalOptions` | [`POST /v1/earn/external-wallet/withdrawal-options`](/docs/reference/api/earn/get-earn-external-wallet-withdrawal-options) |
| Withdraw instantly | `buildEarnWithdrawalTransaction`, `submitEarnWithdrawal` | [`POST /v1/earn/external-wallet/withdrawal-transactions`](/docs/reference/api/earn/create-earn-external-wallet-withdrawal-transaction), [`POST /v1/earn/external-wallet/withdrawals`](/docs/reference/api/earn/create-earn-external-wallet-withdrawal) |
| Preview a request | `previewEarnQueuedWithdrawal` | [`POST /v1/earn/external-wallet/queued-withdrawal-previews`](/docs/reference/api/earn/create-earn-external-wallet-queued-withdrawal-preview) |
| Build a request | `buildEarnQueuedWithdrawalRequest` | [`POST /v1/earn/external-wallet/withdrawal-request-transactions`](/docs/reference/api/earn/create-earn-external-wallet-withdrawal-request-transaction) |
| Submit a request | `submitEarnWithdrawalRequest` | [`POST /v1/earn/external-wallet/withdrawal-requests`](/docs/reference/api/earn/create-earn-external-wallet-withdrawal-request) |
| Track a request | `getEarnWithdrawalRequest` | [`GET /v1/earn/external-wallet/withdrawal-requests/{withdrawalRequestId}`](/docs/reference/api/earn/get-earn-external-wallet-withdrawal-request) |
| Cancel a request | `buildEarnWithdrawalRequestCancellation`, `submitEarnWithdrawalRequestCancellation` | [`POST /v1/earn/external-wallet/withdrawal-request-cancel-transactions`](/docs/reference/api/earn/create-earn-external-wallet-withdrawal-request-cancel-transaction), [`POST /v1/earn/external-wallet/withdrawal-request-cancellations`](/docs/reference/api/earn/create-earn-external-wallet-withdrawal-request-cancellation) |
A withdrawal spends shares, not a dollar amount, and a request transaction
is not a payout. [Withdraw funds](/docs/earn/withdrawals) covers each route.
## Go live
- [ ] Get production access: request mainnet access and switch to your
production project, as steps 1 and 2 of
[Go to mainnet](/docs/guides/go-to-mainnet) show. Embedded Yield needs
no custody provider or wallet funding.
- [ ] Create a production API key with your Earn grants, and regenerate the
backend module in your production project. Strategy IDs differ per
environment.
- [ ] Sign with your customer's wallet on `solana:mainnet`.
- [ ] Support deposit floors. Nearly every production deposit requires
`minSharesOut`. The module sets it from a live preview when the
strategy's `depositSlippage` asked for one at generation, so regenerate
it if a build starts requiring a floor.
- [ ] Persist idempotency keys and signed transactions before you submit.
- [ ] Restore open work after every restart: keep tracking each saved
`movementId` that is not `finalized` or `failed`, and restore open
requests with `listPendingEarnQueuedWithdrawals`.
- [ ] Confirm investor and wallet eligibility for each strategy you offer;
see [Strategies](/docs/earn/strategies#confirm-eligibility).
- [ ] Decide who pays fees, and if you sponsor them, bound and monitor your
fee payer; see [Fees and sponsorship](/docs/earn/integration-details#fees-and-sponsorship).
---
### Integration details
Source: https://platform.solana.com/docs/earn/integration-details
> Slippage floors, fees and sponsorship, swap-funded deposits, retries, errors, and polling for a production Earn integration.
> **Earn is under active development.** Features, API endpoints, and docs may change.
These rules apply to every Earn integration. Read them before you take real
deposits.
## Slippage floors
A floor is the least a transaction may return: `minSharesOut` on a deposit,
`minAmountOut` on a withdrawal. A strategy's `depositSlippage` and
`withdrawalSlippage` say whether it takes one, as
[Read the slippage policy](/docs/earn/strategies#read-the-slippage-policy)
shows. When a policy is not null, preview first and send the floor: the build
refuses the transaction without it.
A null policy means the transaction cannot enforce a floor: a provider order
is priced later, and some sandbox vaults lack the floor instruction. Omit the
floor then, because a build refuses one it cannot enforce. In production,
nearly every deposit takes a floor. A preview quotes the rate; it does not
reserve it.
Compute the floor in integer base units, never with a JavaScript `number`:
```typescript title="floor.ts"
/** Takes `toleranceBps` off a decimal quote, rounding down to the mint's scale. */
export function floorForTolerance(quote: string, decimals: number, toleranceBps: number): string {
if (!Number.isInteger(toleranceBps) || toleranceBps < 1 || toleranceBps > 1_000) {
throw new Error("Tolerance must be 1 to 1,000 basis points");
}
const [whole = "", fraction = ""] = quote.split(".");
const significant = fraction.replace(/0+$/, "");
if (!/^\d+$/.test(whole) || !/^\d*$/.test(fraction) || significant.length > decimals) {
throw new Error("The quote is not a decimal at the mint's scale");
}
const units = BigInt(whole + significant.padEnd(decimals, "0"));
if (units === 0n) throw new Error("The quote is zero");
const floored = (units * BigInt(10_000 - toleranceBps)) / 10_000n || 1n;
const digits = floored.toString().padStart(decimals + 1, "0");
if (decimals === 0) return digits;
const fractionPart = digits.slice(-decimals).replace(/0+$/, "");
return fractionPart ? `${digits.slice(0, -decimals)}.${fractionPart}` : digits.slice(0, -decimals);
}
// floorForTolerance("50", 6, 10) returns "49.95": a 10 bps floor on 50 shares.
```
Use the preview's `shareDecimals` for a deposit floor and `assetDecimals` for
a withdrawal floor. Start from the policy's `defaultToleranceBps`, and let
your customer choose another; this helper accepts 1 to 1,000 basis points.
The [backend module](/docs/guides/embedded-yield#get-the-backend-module)
ships the same function whenever its strategy needs a floor.
## Fees and sponsorship
By default the customer's wallet pays each transaction's network fee and the
rent for any account it creates:
| Transaction | Accounts it can create |
| --- | --- |
| A first deposit into a strategy | The share account, about 0.002 SOL. Some providers also create an account for the wallet, for up to about 0.0035 SOL in all. |
| A withdrawal request | The request's own accounts, about 0.0027 SOL. |
Rent is separate from the deposit and its yield, and it is not part of
`earned`. Where a request's rent goes when it closes depends on the provider's
program; Hastra returns it to the owner. To pay these costs for your
customer, sponsor the transaction:
1. Set `feePayer` to a wallet your backend controls when you build.
2. Have the customer's wallet sign first.
3. Check that the message is unchanged and that its fee payer is your wallet,
then add your signature to the same transaction.
4. Submit the transaction with both signatures.
With raw requests, run step 3 on your backend with your fee payer's key pair:
```typescript title="sponsor.ts"
import {
getAddressFromPublicKey,
getBase64EncodedWireTransaction,
getBase64Encoder,
getCompiledTransactionMessageDecoder,
getTransactionDecoder,
partiallySignTransaction,
} from "@solana/kit";
const decode = (base64: string) =>
getTransactionDecoder().decode(getBase64Encoder().encode(base64));
/** Adds your fee payer's signature to a transaction the customer's wallet signed. */
export async function cosignAsFeePayer(
unsignedBase64: string,
customerSignedBase64: string,
feePayer: CryptoKeyPair,
): Promise {
const unsigned = decode(unsignedBase64);
const signed = decode(customerSignedBase64);
const unchanged =
signed.messageBytes.length === unsigned.messageBytes.length &&
signed.messageBytes.every((byte, index) => byte === unsigned.messageBytes[index]);
if (!unchanged) throw new Error("The wallet changed the transaction; build a new one");
// The fee payer is the message's first account.
const { staticAccounts } = getCompiledTransactionMessageDecoder().decode(signed.messageBytes);
if (staticAccounts[0] !== (await getAddressFromPublicKey(feePayer.publicKey))) {
throw new Error("This build's fee payer is not your wallet");
}
return getBase64EncodedWireTransaction(await partiallySignTransaction([feePayer], signed));
}
```
Pass it the build's `transaction` and the customer's signed transaction, then
submit what it returns. A missing sponsor signature fails the submit with
`400`, and changing the fee payer means a new build and new signatures. The
build's `sponsored` field stays `false`: it reports whether SDP paid, and SDP
never pays for an external wallet. In the
[backend module](/docs/guides/embedded-yield#get-the-backend-module),
`signEarnTransaction` runs steps 2 and 3, and `createSponsorSigner` checks
the fee payer before it signs.
- Sponsor only builds your backend started for an authenticated customer.
- Keep your sponsor's SOL balance small, rate-limit sponsored requests, and
watch the spend.
- Store the sponsor's key in your secret manager.
The catalog's `feeSponsored` describes deposits and withdrawals from a Treasury custody
wallet. It never applies to a customer's wallet.
A full withdrawal from a Kamino strategy leaves the empty share account open.
The customer's wallet can close it to recover the rent; do not promise an
automatic refund.
## Swap-funded deposits
Set `sourceTokenMint` to fund a deposit with a different supported
stablecoin. SDP adds a Jupiter swap to the deposit's deposit token in the
same transaction, so both happen or neither does. Swap routes need mainnet,
so you cannot test them in sandbox.
When the two do not fit in one transaction, the build returns
`requiresSeparateSwap: true` with a `swap` and a `followUp` instead of
`transaction`:
1. Save the swap's signed bytes, its signature, and the follow-up body.
2. Broadcast the swap yourself and confirm it.
3. Build the follow-up deposit with its original floor and fee payer.
SDP does not record the standalone swap as a movement, so your app owns its
recovery. Resolve an unfinished swap before you start another.
## Retry safely
Every Earn submit takes an `Idempotency-Key`. Use a new random UUID for each
new deposit, withdrawal, request, or cancellation, and save it with the
signed transaction before the first send. Never put customer data in an
idempotency key.
| Situation | What SDP does | What you do |
| --- | --- | --- |
| A submit times out or returns `5xx`. | Nothing twice. | Retry with the same idempotency key and body. |
| You retry with the same idempotency key and `transactionId`. | Returns the original movement in its current state, with `replayed: true`, even if the signed bytes differ. | Track it as usual. |
| You reuse an idempotency key for a different build. | `409 CONFLICT`. Nothing runs. | Retry the original build, or use a new idempotency key for a new action. |
| You submit the same build under a new idempotency key. | `409 CONFLICT`. | Find the original action and its idempotency key. |
| The blockhash expired before you submitted. | Records the original signed bytes, which may already have landed. If they never did, the movement ends `failed` with `Transaction blockhash expired before confirmation`. | Submit the original and track it. Do not rebuild until it fails. |
| A late withdrawal request returns `409 CONFLICT`. | Waits to see the exact signed transaction finalized. | Retry the same bytes and key. |
| Recovery finds the transaction finalized as failed. | `400 TRANSACTION_FAILED`. Nothing moved. | Build a new transaction. |
| An unsigned build expired. | Nothing has moved. | Build again before asking for a signature. |
| The preview has blocking issues. | Nothing has moved. | Resolve them and preview again. |
Earn's submits keep their own idempotency rules, which replace the shared
[Idempotency](/docs/developing-with-sdp/idempotency) page's. A submit without
a key returns `400 BAD_REQUEST`, and a misused key returns `409 CONFLICT`,
never `422`. A key stays bound to its movement for good, with no 24-hour
window, so never reuse one for a new action.
Submit withdrawal requests and cancellations through SDP promptly. If a
wallet broadcasts one itself and SDP first sees it after its build expires,
SDP may be unable to track it. Keep unresolved actions visible, and never
assume no funds moved.
## Errors
Every error has the shape `{ "error": { "code", "message", "details" } }`.
The codes Earn integrations meet most:
| Status | Code | Meaning | What to do |
| --- | --- | --- | --- |
| `400` | `BAD_REQUEST` | A field is invalid, the `Idempotency-Key` is missing, or the signed transaction differs from the build. | Read `message` and `details`, fix the request, and build again when the transaction changed. |
| `400` | `TRANSACTION_FAILED` | Recovery found the submitted transaction finalized as failed. | Build a new transaction. |
| `403` | `INSUFFICIENT_PERMISSIONS` | The API key lacks the grant in `details.action`. | Add the grant to the API key. |
| `403` | `FORBIDDEN` | `details.reason` is `provider_not_entitled`: your organization is not activated for the provider. With `provider_not_offered`, SDP takes no new deposits for it. | Ask your SDP contact to activate it. Withdrawals still work either way. |
| `404` | `NOT_FOUND` | No such strategy, position, movement, build, or request in your project. | Check the ID and the project. |
| `409` | `CONFLICT` | An idempotency key or a signed transaction was already used differently, a late withdrawal request awaits proof that it landed, or a request cannot be canceled yet. | See [Retry safely](#retry-safely). Retry a late request with its exact bytes and key; for a cancellation, wait for `expiredCancelable`. |
| `429` | `RATE_LIMITED` | You are over your rate limit. | Wait for `Retry-After`, then retry. |
| `501` | `NOT_IMPLEMENTED` | The provider cannot quote this operation. | Continue without the preview when the floor policy is null. |
| `503` | `PROVIDER_UNAVAILABLE` | SDP could not read the provider's live state or reach a dependency. | Retry with backoff, and show the action as unavailable meanwhile. Retry a submit with the same idempotency key and body. |
See [Errors](/docs/reference/conventions/errors) for the shared error model.
## Poll politely
Track a movement by polling its `GET` route about once a second for the
first minute, then every five seconds. Stop at `finalized` or `failed`, and
treat `429` as a signal to slow down. Withdrawal requests can take hours or
days, so poll them every minute or two, and restore them on startup instead
of holding a loop open. The backend module's wait functions follow this
schedule. See
[Rate limits](/docs/reference/conventions/rate-limits).
---
## Resources
### Self-hosting
Source: https://platform.solana.com/docs/self-hosting
> Run the Solana Developer Platform on your own infrastructure with Docker Compose.
Self-hosting runs the full Solana Developer Platform (API, dashboard, docs, database, and cache) on infrastructure you control, from prebuilt container images. You bring your own authentication (Clerk), Solana RPC, and signing provider.
## What you run
The release's `compose.yml` starts six services:
| Service | Image | Purpose |
|---|---|---|
| `sdp-api` | `sdp-api` | The API, on port `8787` by default. |
| `sdp-web` | `sdp-web` | The dashboard, on port `3000` by default. |
| `sdp-docs` | `sdp-docs` | These docs, on port `3001` by default. |
| `sdp-migrate` | `sdp-api` | Applies pending database migrations, then exits. |
| `postgres` | `postgres:16-alpine` | The bundled database. You can use an external one instead. |
| `redis` | `redis:7-alpine` | The bundled cache. You can use an external one instead. |
## Start here
- [Quickstart](/docs/self-hosting/quickstart): install the stack and bring it up.
- [First devnet deployment](/docs/self-hosting/first-devnet-deployment): sign in to the dashboard, create a local wallet, and call the API with a wallet-scoped key.
- [Configurator](/docs/self-hosting/configurator): generate a complete `.env` in your browser or terminal.
- [Environment reference](/docs/self-hosting/env-reference): what the variables do and which ones are required.
## Operate
- [Upgrade and backup](/docs/self-hosting/upgrade-and-backup): move to a new release and protect your data.
- [Troubleshooting](/docs/self-hosting/troubleshooting): common startup and configuration failures.
- [External co-signers](/docs/self-hosting/external-co-signers): host provider-specific signing services beside the stack.
## Requirements
- A 64-bit Linux or macOS host (x86-64 or Arm64) with Docker Engine and Docker Compose v2.
- Outbound network access to pull images and reach your Solana RPC and Clerk.
- A [Clerk](https://clerk.com) application and a Solana RPC endpoint. The [environment reference](/docs/self-hosting/env-reference) lists the rest.
---
## API Reference
### API Reference
Source: https://platform.solana.com/docs/reference/api
> Choose an API family to explore its endpoints.
- [Health](/docs/reference/api/health.md)
Service health and readiness endpoints.
- [Wallets](/docs/reference/api/wallets.md)
Wallet signing provider configuration and wallet management.
- [Projects](/docs/reference/api/projects.md)
Projects and their provider availability.
- [Payments](/docs/reference/api/payments.md)
Wallet balances, transfers, and payment flows.
- [Counterparties](/docs/reference/api/counterparties.md)
A counterparty is a person or business you pay or get paid by.
- [Earn](/docs/reference/api/earn.md)
Strategy reads, previews, withdrawal-route discovery, unsigned builds, submission, queued actions, and tenant tracking.
[API conventions](/docs/reference/conventions) cover authentication, environments, idempotency, errors, pagination, rate limits, and versioning.
Download Postman collection
Open raw JSON
---
## Health
### Health
Source: https://platform.solana.com/docs/reference/api/health
> Service health and readiness endpoints.
| Method | Endpoint | Summary |
| --- | --- | --- |
| `GET` | `/health` | [Health check](/docs/reference/api/health/health-check.md) |
| `GET` | `/health/ready` | [Readiness check](/docs/reference/api/health/health-readiness.md) |
---
### Health check
Source: https://platform.solana.com/docs/reference/api/health/health-check
> Returns service health and build metadata.
`GET /health`
Public: this route takes no API key.
## Responses
- **`200`** Service is healthy
- **`status`** · `string` · required · always `ok`
Health status.
- **`timestamp`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Health check timestamp.
- **`version`** · `string` · required · example `0.1.0`
Service version.
- **`environment`** · `string` · required · example `production`
Runtime environment.
- **`releaseChannel`** · `string` · required · one of `experimental`, `beta`, `stable` · example `experimental`
Release channel: which modules this deployment runs.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/health'
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/health", {
method: "GET"
});
```
```python tab="Python"
import requests
r = requests.get("https://api.solana.com/health")
```
## Example responses
```json tab="200"
{
"status": "ok",
"timestamp": "2025-01-01T00:00:00.000Z",
"version": "0.1.0",
"environment": "production",
"releaseChannel": "experimental"
}
```
---
### Readiness check
Source: https://platform.solana.com/docs/reference/api/health/health-readiness
> Performs readiness checks for downstream dependencies.
`GET /health/ready`
Public: this route takes no API key.
## Responses
- **`200`** Service is ready
- **`status`** · `string` · required · one of `ready`, `not_ready` · example `ready`
Readiness status.
- **`timestamp`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Readiness check timestamp.
- **`revision`** · `string` · required · example `sdp-prod-api-public-00042-abc`
Cloud Run revision serving the request, or `local` outside Cloud Run.
- **`commit`** · `string` · required · example `0123456789abcdef0123456789abcdef01234567`
Git commit the serving image was built from, or `local` when the image carries none.
- **`releaseChannel`** · `string` · required · one of `experimental`, `beta`, `stable` · example `stable`
Release channel: which modules this deployment runs.
- **`checks`** · `object` · required
Dependency health checks.
- **`database`** · `string` · required · one of `ok`, `error` · example `ok`
Database connectivity status.
- **`redis`** · `string` · required · one of `ok`, `error` · example `ok`
Redis connectivity status.
- **`egress`** · `string` · one of `pending`, `ok`, `unverified` · example `ok`
Outbound internet connectivity of this instance. `pending` (not ready) while a fresh instance waits for working egress, `ok` once an outbound call succeeded, `unverified` when it stopped waiting and serves anyway. Absent where the deployment does not wait for egress.
- **`503`** Service is not ready
Same fields as `200`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/health/ready'
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/health/ready", {
method: "GET"
});
```
```python tab="Python"
import requests
r = requests.get("https://api.solana.com/health/ready")
```
## Example responses
```json tab="200"
{
"status": "ready",
"timestamp": "2025-01-01T00:00:00.000Z",
"revision": "sdp-prod-api-public-00042-abc",
"commit": "0123456789abcdef0123456789abcdef01234567",
"releaseChannel": "stable",
"checks": {
"database": "ok",
"redis": "ok",
"egress": "ok"
}
}
```
```json tab="503"
{
"status": "not_ready",
"timestamp": "2025-01-01T00:00:00.000Z",
"revision": "sdp-prod-api-public-00042-abc",
"commit": "0123456789abcdef0123456789abcdef01234567",
"releaseChannel": "stable",
"checks": {
"database": "ok",
"redis": "error",
"egress": "ok"
}
}
```
---
## Wallets
### Wallets
Source: https://platform.solana.com/docs/reference/api/wallets
> Wallet signing provider configuration and wallet management.
| Method | Endpoint | Summary |
| --- | --- | --- |
| `POST` | `/v1/wallets` | [Create wallet](/docs/reference/api/wallets/create-wallet.md) |
| `DELETE` | `/v1/wallets` | [Delete wallet](/docs/reference/api/wallets/delete-wallet.md) |
| `GET` | `/v1/wallets` | [List wallets](/docs/reference/api/wallets/list-wallets.md) |
| `GET` | `/v1/wallets/configs` | [List wallet signing configs](/docs/reference/api/wallets/list-wallet-configs.md) |
| `GET` | `/v1/wallets/aggregate` | [Aggregate wallet balances](/docs/reference/api/wallets/aggregate-wallet-balances.md) |
| `GET` | `/v1/wallets/public-key` | [Get wallet public key](/docs/reference/api/wallets/get-wallet-public-key.md) |
| `POST` | `/v1/wallets/signer-check` | [Check signer via simulated memo transaction](/docs/reference/api/wallets/check-wallet-signer.md) |
| `GET` | `/v1/wallets/{walletId}` | [Get wallet by ID](/docs/reference/api/wallets/get-wallet-by-id.md) |
| `PATCH` | `/v1/wallets/{walletId}` | [Update wallet](/docs/reference/api/wallets/update-wallet.md) |
---
### Create wallet
Source: https://platform.solana.com/docs/reference/api/wallets/create-wallet
> Provisions a new wallet under exactly one provider account: the project's Managed config for provider, or the Custody Connection named by connectionId.
`POST /v1/wallets`
A body naming both, or neither, is rejected with `400`.
With an `Idempotency-Key`, SDP never records a second wallet for one key: a retry returns the wallet an earlier attempt recorded, even after the 24-hour key window. A provider that accepts a request id also receives one derived from the key.
On a provider that dedupes on the request id, a `5xx` is not stored: a same-key retry runs again and gets back the wallet the first attempt made, even when SDP failed to record it, for as long as the provider keeps the request id (permanently, or for 24 hours; the Idempotency guide lists each provider). On any other provider a failure is stored as a `503`, and a new key is a new provider request. A `503` can mean the provider created the wallet before the error. With a provider that accepts no request id, a failure that left no SDP wallet row makes even the same key a new provider request after 24 hours. Before any retry after an ambiguous failure, whatever the key, check `GET /v1/wallets` and, if the wallet is not there, the provider account.
The provider request id and the recorded-wallet lookup both include the credential. After an API key rotation, once the key's 24-hour record has expired, the same key under the new credential is a new provider request and can create a second wallet: list wallets with `GET /v1/wallets` and check the provider account before retrying a create.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `wallets.manage` 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`
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 wallet request body. Names exactly one owner: provider (Managed config) or connectionId (Custody Connection); a body naming both, or neither, is rejected with 400.
Any of:
- **Create a wallet under the project's Managed config for provider.** · must not include `connectionId`
- **`provider`** · `string` · required · one of `fireblocks`, `privy`, `coinbase_cdp`, `para`, `turnkey`, `dfns`, `ibm_haven`, `anchorage`, `utila` · example `privy`
Provider whose Managed config owns the new wallet. A project holds one active config per provider.
- **`label`** · `string` · max length 100 · example `Mint authority wallet`
Optional label for the new wallet.
- **`purpose`** · `string` · one of `transfer`, `fee_payer` · default `transfer`
What the wallet is for. Defaults to `transfer`, the only purpose a payment, transfer batch or recurring payment can send from.
- **Create a wallet under a Custody Connection.** · must not include `provider`
- **`connectionId`** · `string` · required · min length 1 · example `cconn_123`
Custody Connection (BYOK) that owns the new wallet.
- **`label`** · `string` · max length 100 · example `Mint authority wallet`
Optional label for the new wallet.
- **`purpose`** · `string` · one of `transfer`, `fee_payer` · default `transfer`
What the wallet is for. Defaults to `transfer`, the only purpose a payment, transfer batch or recurring payment can send from.
## Responses
- **`201`** Wallet created
- **`data`** · `object` · required
Created wallet response payload.
- **`wallet`** · `object` · required
Wallet details.
- **`id`** · `string` · required · example `cwlt_example`
Wallet record ID.
- **`custodyConfigId`** · `string` · example `cfg_example`
Owning custody configuration ID.
- **`custodyConnectionId`** · `string` · example `cconn_example`
Owning Custody Connection ID.
- **`provider`** · `string` · example `privy`
Wallet signing provider.
- **`isRuntimeExecutionAllowed`** · `boolean` · required · example `true`
Whether SDP currently permits attempting runtime execution through this wallet's owner. This is request-time admission, not Provider health or a success guarantee.
- **`walletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Provider wallet ID.
- **`publicKey`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Base58-encoded Solana address.
- **`label`** · `string | null` · required · example `Treasury wallet`
Optional wallet label.
- **`purpose`** · `string | null` · required · example `transfer`
Optional wallet purpose.
- **`status`** · `string` · required · one of `active`, `inactive` · example `active`
Wallet status.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`balances`** · `object[]`
Tracked token balances when requested and successfully observed. Omitted when the balance lookup is unavailable; an empty array is a successful zero-token result.
- **`token`** · `string` · required · example `USDC`
Tracked token symbol.
- **`mint`** · `string` · required · min length 32 · max length 44 · example `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`
Tracked token mint address.
- **`amount`** · `string` · required · example `1250000`
Raw token amount as a string.
- **`uiAmount`** · `string` · required · example `1.25`
Human-readable token balance.
- **`decimals`** · `integer` · required · min 0 · example `6`
Token decimals.
- **`usdPrice`** · `number` · example `1`
Resolved USD price per token when available.
- **`usdValue`** · `number` · example `125.5`
Resolved USD value of this balance when pricing is available.
One of:
- **Owned by a Managed config.** · must include `custodyConfigId` · must not include `custodyConnectionId`
- **Owned by a Custody Connection.** · must include `custodyConnectionId` · must not include `custodyConfigId`
- **`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 `wallets.manage` 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. Forbidden: the caller lacks the route's action or the API key is wallet-scoped, or the provider account was refused. A refusal's details.reason names the failed check: custody_mode_not_allowed when provider names a Managed config in a Production project (Production is BYOK only: create the wallet in a Custody Connection with connectionId), custody_provider_not_in_release_channel, or provider_not_entitled.
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`.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST 'https://api.solana.com/v1/wallets' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "privy",
"label": "Treasury wallet",
"purpose": "transfer"
}'
```
```ts tab="TypeScript"
// Reuse this key when you retry this request.
const idempotencyKey = crypto.randomUUID();
const res = await fetch("https://api.solana.com/v1/wallets", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
"provider": "privy",
"label": "Treasury wallet",
"purpose": "transfer"
})
});
```
```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/wallets",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
json={
"provider": "privy",
"label": "Treasury wallet",
"purpose": "transfer"
})
```
## Example responses
```json tab="201"
{
"data": {
"wallet": {
"id": "cwlt_example",
"custodyConfigId": "cfg_example",
"provider": "privy",
"isRuntimeExecutionAllowed": true,
"walletId": "privy_wallet_123",
"publicKey": "So11111111111111111111111111111111111111112",
"label": "Treasury wallet",
"purpose": "transfer",
"status": "active",
"createdAt": "2025-01-01T00:00:00.000Z"
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: wallets.manage",
"details": {
"action": "wallets.manage"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Delete wallet
Source: https://platform.solana.com/docs/reference/api/wallets/delete-wallet
> Deletes the wallet from its exact owning custody target when that Provider supports wallet deletion.
`DELETE /v1/wallets`
Provider, when supplied, is a consistency assertion.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `wallets.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`
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
Delete wallet request body.
- **`provider`** · `string` · one of `fireblocks`, `privy`, `coinbase_cdp`, `para`, `turnkey`, `dfns`, `ibm_haven`, `anchorage`, `utila` · example `privy`
Optional consistency assertion for the Provider of the wallet being deleted.
- **`walletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Provider wallet ID to delete from its exact owning target.
## Responses
- **`200`** Wallet deleted
- **`data`** · `object` · required
Delete wallet response payload.
- **`walletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Wallet ID that was deleted.
- **`deleted`** · `boolean` · required · always `true` · example `true`
Deletion confirmation flag.
- **`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 `wallets.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`.
## Example request
```bash tab="cURL"
IDEMPOTENCY_KEY=$(uuidgen)
curl -X DELETE 'https://api.solana.com/v1/wallets' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "privy",
"walletId": "privy_wallet_123"
}'
```
```ts tab="TypeScript"
// Reuse this key when you retry this request.
const idempotencyKey = crypto.randomUUID();
const res = await fetch("https://api.solana.com/v1/wallets", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
"provider": "privy",
"walletId": "privy_wallet_123"
})
});
```
```python tab="Python"
import os, requests, uuid
# Reuse this key when you retry this request.
idempotency_key = str(uuid.uuid4())
r = requests.delete("https://api.solana.com/v1/wallets",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
json={
"provider": "privy",
"walletId": "privy_wallet_123"
})
```
## Example responses
```json tab="200"
{
"data": {
"walletId": "privy_wallet_123",
"deleted": true
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: wallets.execute",
"details": {
"action": "wallets.execute"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### List wallets
Source: https://platform.solana.com/docs/reference/api/wallets/list-wallets
> Lists active wallets under active Config and Connection owners, oldest first.
`GET /v1/wallets`
Provider narrows the list to one custody provider.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `wallets.read` action.
## Query parameters
- **`provider`** · `string` · one of `fireblocks`, `privy`, `coinbase_cdp`, `para`, `turnkey`, `dfns`, `ibm_haven`, `anchorage`, `utila` · example `privy`
Wallet signing provider.
- **`includeBalances`** · `boolean`
- **`view`** · `string` · always `summary`
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Wallets
- **`data`** · `object` · required
Wallets list response payload.
- **`wallets`** · `object[]` · required
Wallets.
- **`id`** · `string` · required · example `cwlt_example`
Wallet record ID.
- **`custodyConfigId`** · `string` · example `cfg_example`
Owning custody configuration ID.
- **`custodyConnectionId`** · `string` · example `cconn_example`
Owning Custody Connection ID.
- **`provider`** · `string` · example `privy`
Wallet signing provider.
- **`isRuntimeExecutionAllowed`** · `boolean` · required · example `true`
Whether SDP currently permits attempting runtime execution through this wallet's owner. This is request-time admission, not Provider health or a success guarantee.
- **`walletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Provider wallet ID.
- **`publicKey`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Base58-encoded Solana address.
- **`label`** · `string | null` · required · example `Treasury wallet`
Optional wallet label.
- **`purpose`** · `string | null` · required · example `transfer`
Optional wallet purpose.
- **`status`** · `string` · required · one of `active`, `inactive` · example `active`
Wallet status.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`balances`** · `object[]`
Tracked token balances when requested and successfully observed. Omitted when the balance lookup is unavailable; an empty array is a successful zero-token result.
- **`token`** · `string` · required · example `USDC`
Tracked token symbol.
- **`mint`** · `string` · required · min length 32 · max length 44 · example `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`
Tracked token mint address.
- **`amount`** · `string` · required · example `1250000`
Raw token amount as a string.
- **`uiAmount`** · `string` · required · example `1.25`
Human-readable token balance.
- **`decimals`** · `integer` · required · min 0 · example `6`
Token decimals.
- **`usdPrice`** · `number` · example `1`
Resolved USD price per token when available.
- **`usdValue`** · `number` · example `125.5`
Resolved USD value of this balance when pricing is available.
One of:
- **Owned by a Managed config.** · must include `custodyConfigId` · must not include `custodyConnectionId`
- **Owned by a Custody Connection.** · must include `custodyConnectionId` · must not include `custodyConfigId`
- **`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 `wallets.read` 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`.
- **`409`** Conflict
Same fields as `400`.
- **`500`** Internal Server Error
Same fields as `400`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/wallets' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/wallets", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/wallets",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"wallets": [
{
"id": "cwlt_example",
"custodyConfigId": "cfg_example",
"provider": "privy",
"isRuntimeExecutionAllowed": true,
"walletId": "privy_wallet_123",
"publicKey": "So11111111111111111111111111111111111111112",
"label": "Treasury wallet",
"purpose": "transfer",
"status": "active",
"createdAt": "2025-01-01T00:00:00.000Z"
}
]
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: wallets.read",
"details": {
"action": "wallets.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### List wallet signing configs
Source: https://platform.solana.com/docs/reference/api/wallets/list-wallet-configs
> Returns the active Config-owned wallet signing configurations for the requested scope, one per provider.
`GET /v1/wallets/configs`
Requires an [API key](/docs/developing-with-sdp/authentication) with the `wallets.read` action.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Wallet signing configurations
- **`data`** · `object` · required
Wallet signing configurations response payload.
- **`configs`** · `object[]` · required
Active wallet signing configurations for the requested scope.
- **`id`** · `string` · required · example `cfg_example`
Wallet signing config ID.
- **`organizationId`** · `string` · required · example `org_example`
Organization ID that owns this wallet signing config.
- **`projectId`** · `string | null` · required · min length 1 · example `prj_example`
Optional project scope for this config.
- **`provider`** · `string` · required · example `privy`
Wallet signing provider.
- **`status`** · `string` · required · one of `active`, `inactive` · example `active`
Config status.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 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.
- **`401`** Unauthorized
- **`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.
- **`403`** Forbidden. Requires the `wallets.read` 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 `401`.
- **`500`** Internal Server Error
Same fields as `401`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/wallets/configs' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/wallets/configs", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/wallets/configs",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"configs": [
{
"id": "cfg_example",
"organizationId": "org_example",
"projectId": "prj_example",
"provider": "privy",
"status": "active",
"createdAt": "2025-01-01T00:00:00.000Z"
}
]
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: wallets.read",
"details": {
"action": "wallets.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Aggregate wallet balances
Source: https://platform.solana.com/docs/reference/api/wallets/aggregate-wallet-balances
> Aggregates tracked balances for active wallets under the same owner-aware inclusion rules as the wallet list, without exposing one aggregate-level owner or runtime-admission value.
`GET /v1/wallets/aggregate`
Returns 503 if any included wallet balance cannot be read, rather than returning an incomplete total.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `wallets.read` action.
## Query parameters
- **`provider`** · `string` · one of `fireblocks`, `privy`, `coinbase_cdp`, `para`, `turnkey`, `dfns`, `ibm_haven`, `anchorage`, `utila` · example `privy`
Wallet signing provider.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Aggregated wallet balances
- **`data`** · `object` · required
Aggregated wallet balance response payload.
- **`aggregate`** · `object` · required
Aggregated wallet balance summary.
- **`walletCount`** · `integer` · required · min 0 · example `3`
Number of wallets included in the aggregate.
- **`balances`** · `object[]` · required
Aggregated tracked token balances across the included wallets.
- **`token`** · `string` · required · example `USDC`
Tracked token symbol.
- **`mint`** · `string` · required · min length 32 · max length 44 · example `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`
Tracked token mint address.
- **`amount`** · `string` · required · example `1250000`
Raw token amount as a string.
- **`uiAmount`** · `string` · required · example `1.25`
Human-readable token balance.
- **`decimals`** · `integer` · required · min 0 · example `6`
Token decimals.
- **`usdPrice`** · `number` · example `1`
Resolved USD price per token when available.
- **`usdValue`** · `number` · example `125.5`
Resolved USD value of this balance when pricing is available.
- **`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 `wallets.read` 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`.
- **`409`** Conflict
Same fields as `400`.
- **`500`** Internal Server Error
Same fields as `400`.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/wallets/aggregate' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/wallets/aggregate", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/wallets/aggregate",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"aggregate": {
"walletCount": 3,
"balances": [
{
"token": "USDC",
"mint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"amount": "1250000",
"uiAmount": "1.25",
"decimals": 6,
"usdPrice": 1,
"usdValue": 125.5
}
]
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: wallets.read",
"details": {
"action": "wallets.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Get wallet public key
Source: https://platform.solana.com/docs/reference/api/wallets/get-wallet-public-key
> Returns the persisted public key for an exact active walletId. walletId is required (400 without it); an API key with selected wallets gets 404 for a wallet it does not list.
`GET /v1/wallets/public-key`
Resolution is DB-backed only.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `wallets.read` action.
## Query parameters
- **`walletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Provider wallet ID — the `walletId` field returned by GET /v1/wallets.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Wallet public key
- **`publicKey`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Base58-encoded Solana address.
- **`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 `wallets.read` 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`.
- **`500`** Internal Server Error
Same fields as `400`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/wallets/public-key?walletId=privy_wallet_123' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/wallets/public-key?walletId=privy_wallet_123", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/wallets/public-key?walletId=privy_wallet_123",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"publicKey": "So11111111111111111111111111111111111111112"
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: wallets.read",
"details": {
"action": "wallets.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Check signer via simulated memo transaction
Source: https://platform.solana.com/docs/reference/api/wallets/check-wallet-signer
> Signs a server-authored memo message with the named wallet, verifies the signature, and simulates the transaction.
`POST /v1/wallets/signer-check`
Nothing is broadcast and no sponsorship is spent. The wallet is the only readonly signer and the request cannot supply memo text.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `wallets.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`
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
walletId is the Provider wallet ID to check, required. Ambiguous Provider IDs are rejected; the check uses the resolved exact custody wallet record, and an API key may check only a wallet within its wallet scope.
- **`walletId`** · `string` · required · min length 1
## Responses
- **`200`** Signer check verified in simulation
- **`data`** · `object` · required
Signer check response payload.
- **`walletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Resolved signing wallet ID.
- **`walletAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Resolved signer address used for the check message.
- **`feePayer`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Funded fee payer address used in the simulated message; nothing is spent from it.
- **`memo`** · `string` · required · example `SDP signer check 123e4567-e89b-42d3-a456-426614174000`
Server-generated memo text in the simulated check message.
- **`signature`** · `string` · required · example `sig_example`
The custody wallet's Ed25519 signature over the check message, base58. Verified server-side and exercised in RPC simulation only — the check never broadcasts, so this signature does not exist on chain.
- **`simulated`** · `boolean` · required · always `true`
Always true: signer checks run in simulation and cannot spend sponsorship.
- **`checkedAt`** · `string` · required · format `date-time` · example `2026-02-20T00:00:00.000Z`
Timestamp recorded after the simulation succeeded.
- **`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 `wallets.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`.
- **`429`** Too Many Requests
Same fields as `400`.
- **`500`** Internal Server Error
Same fields as `400`.
- **`502`** Bad Gateway
Same fields as `400`.
## Example request
```bash tab="cURL"
IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST 'https://api.solana.com/v1/wallets/signer-check' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"walletId": "privy_wallet_123"
}'
```
```ts tab="TypeScript"
// Reuse this key when you retry this request.
const idempotencyKey = crypto.randomUUID();
const res = await fetch("https://api.solana.com/v1/wallets/signer-check", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
"walletId": "privy_wallet_123"
})
});
```
```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/wallets/signer-check",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
json={
"walletId": "privy_wallet_123"
})
```
## Example responses
```json tab="200"
{
"data": {
"walletId": "privy_wallet_123",
"walletAddress": "So11111111111111111111111111111111111111112",
"feePayer": "So11111111111111111111111111111111111111112",
"memo": "SDP signer check 123e4567-e89b-42d3-a456-426614174000",
"signature": "sig_example",
"simulated": true,
"checkedAt": "2026-02-20T00:00:00.000Z"
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: wallets.execute",
"details": {
"action": "wallets.execute"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Get wallet by ID
Source: https://platform.solana.com/docs/reference/api/wallets/get-wallet-by-id
> Returns active wallet metadata, exact Config or Connection ownership, runtime execution admission, Provider, public key, and by default the current SOL balance.
`GET /v1/wallets/{walletId}`
If the balance read fails, returns 503 instead of a zero balance. Set includeBalance=false for a metadata-only read.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `wallets.read` action.
## Path parameters
- **`walletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Provider wallet ID — the `walletId` field returned by GET /v1/wallets.
## Query parameters
- **`includeBalance`** · `string` · one of `true`, `false` · example `false`
Whether to resolve the current SOL balance. Defaults to true; false returns metadata without balance RPC or pricing calls.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Wallet details
- **`data`** · `object` · required
Wallet details by ID response payload.
- **`wallet`** · `object` · required
Wallet details with provider and optional SOL balance.
- **`id`** · `string` · required · example `cwlt_example`
Wallet record ID.
- **`custodyConfigId`** · `string` · example `cfg_example`
Owning custody configuration ID.
- **`custodyConnectionId`** · `string` · example `cconn_example`
Owning Custody Connection ID.
- **`provider`** · `string` · required · example `privy`
Wallet custody provider.
- **`isRuntimeExecutionAllowed`** · `boolean` · required · example `true`
Whether SDP currently permits attempting runtime execution through this wallet's owner. This is request-time admission, not Provider health or a success guarantee.
- **`walletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Provider wallet ID.
- **`publicKey`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Base58-encoded Solana address.
- **`label`** · `string | null` · required · example `Treasury wallet`
Optional wallet label.
- **`purpose`** · `string | null` · required · example `transfer`
Optional wallet purpose.
- **`status`** · `string` · required · one of `active`, `inactive` · example `active`
Wallet status.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`balance`** · `object`
Current SOL balance for the wallet public key. Omitted when includeBalance=false.
- **`token`** · `string` · required · always `SOL` · example `SOL`
Balance token symbol.
- **`mint`** · `string` · required · example `So11111111111111111111111111111111111111112`
Native SOL mint address.
- **`amount`** · `string` · required · example `123456789`
Raw lamports balance as a string.
- **`uiAmount`** · `string` · required · example `0.123456789`
Human-readable SOL balance.
- **`decimals`** · `number` · required · always `9` · example `9`
Token decimals for SOL.
One of:
- **Owned by a Managed config.** · must include `custodyConfigId` · must not include `custodyConnectionId`
- **Owned by a Custody Connection.** · must include `custodyConnectionId` · must not include `custodyConfigId`
- **`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.
- **`401`** Unauthorized
- **`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.
- **`403`** Forbidden. Requires the `wallets.read` 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 `401`.
- **`404`** Not Found
Same fields as `401`.
- **`409`** Conflict
Same fields as `401`.
- **`500`** Internal Server Error
Same fields as `401`.
- **`503`** Service Unavailable
Same fields as `401`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/wallets/privy_wallet_123' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/wallets/privy_wallet_123", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/wallets/privy_wallet_123",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"wallet": {
"id": "cwlt_example",
"custodyConfigId": "cfg_example",
"provider": "privy",
"isRuntimeExecutionAllowed": true,
"walletId": "privy_wallet_123",
"publicKey": "So11111111111111111111111111111111111111112",
"label": "Treasury wallet",
"purpose": "transfer",
"status": "active",
"createdAt": "2025-01-01T00:00:00.000Z"
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: wallets.read",
"details": {
"action": "wallets.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Update wallet
Source: https://platform.solana.com/docs/reference/api/wallets/update-wallet
> Updates the display label of an active wallet under its exact Config or Connection owner.
`PATCH /v1/wallets/{walletId}`
Requires an [API key](/docs/developing-with-sdp/authentication) with the `wallets.manage` action.
## Path parameters
- **`walletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Provider wallet ID — the `walletId` field returned by GET /v1/wallets.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
- **`Idempotency-Key`** · `string`
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
Update wallet request body.
- **`label`** · `string | null` · max length 100 · example `Treasury signer`
Optional wallet label. Set to null to clear the label.
## Responses
- **`200`** Wallet updated
- **`data`** · `object` · required
Created wallet response payload.
- **`wallet`** · `object` · required
Wallet details.
- **`id`** · `string` · required · example `cwlt_example`
Wallet record ID.
- **`custodyConfigId`** · `string` · example `cfg_example`
Owning custody configuration ID.
- **`custodyConnectionId`** · `string` · example `cconn_example`
Owning Custody Connection ID.
- **`provider`** · `string` · example `privy`
Wallet signing provider.
- **`isRuntimeExecutionAllowed`** · `boolean` · required · example `true`
Whether SDP currently permits attempting runtime execution through this wallet's owner. This is request-time admission, not Provider health or a success guarantee.
- **`walletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Provider wallet ID.
- **`publicKey`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Base58-encoded Solana address.
- **`label`** · `string | null` · required · example `Treasury wallet`
Optional wallet label.
- **`purpose`** · `string | null` · required · example `transfer`
Optional wallet purpose.
- **`status`** · `string` · required · one of `active`, `inactive` · example `active`
Wallet status.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`balances`** · `object[]`
Tracked token balances when requested and successfully observed. Omitted when the balance lookup is unavailable; an empty array is a successful zero-token result.
- **`token`** · `string` · required · example `USDC`
Tracked token symbol.
- **`mint`** · `string` · required · min length 32 · max length 44 · example `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`
Tracked token mint address.
- **`amount`** · `string` · required · example `1250000`
Raw token amount as a string.
- **`uiAmount`** · `string` · required · example `1.25`
Human-readable token balance.
- **`decimals`** · `integer` · required · min 0 · example `6`
Token decimals.
- **`usdPrice`** · `number` · example `1`
Resolved USD price per token when available.
- **`usdValue`** · `number` · example `125.5`
Resolved USD value of this balance when pricing is available.
One of:
- **Owned by a Managed config.** · must include `custodyConfigId` · must not include `custodyConnectionId`
- **Owned by a Custody Connection.** · must include `custodyConnectionId` · must not include `custodyConfigId`
- **`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 `wallets.manage` 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`.
## Example request
```bash tab="cURL"
IDEMPOTENCY_KEY=$(uuidgen)
curl -X PATCH 'https://api.solana.com/v1/wallets/privy_wallet_123' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "Treasury signer"
}'
```
```ts tab="TypeScript"
// Reuse this key when you retry this request.
const idempotencyKey = crypto.randomUUID();
const res = await fetch("https://api.solana.com/v1/wallets/privy_wallet_123", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
"label": "Treasury signer"
})
});
```
```python tab="Python"
import os, requests, uuid
# Reuse this key when you retry this request.
idempotency_key = str(uuid.uuid4())
r = requests.patch("https://api.solana.com/v1/wallets/privy_wallet_123",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
json={
"label": "Treasury signer"
})
```
## Example responses
```json tab="200"
{
"data": {
"wallet": {
"id": "cwlt_example",
"custodyConfigId": "cfg_example",
"provider": "privy",
"isRuntimeExecutionAllowed": true,
"walletId": "privy_wallet_123",
"publicKey": "So11111111111111111111111111111111111111112",
"label": "Treasury wallet",
"purpose": "transfer",
"status": "active",
"createdAt": "2025-01-01T00:00:00.000Z"
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: wallets.manage",
"details": {
"action": "wallets.manage"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
## Projects
### Projects
Source: https://platform.solana.com/docs/reference/api/projects
> Projects and their provider availability. Project members and keys are managed in the dashboard.
| Method | Endpoint | Summary |
| --- | --- | --- |
| `GET` | `/v1/projects` | [List projects](/docs/reference/api/projects/list-projects.md) |
| `GET` | `/v1/projects/{projectId}` | [Get project](/docs/reference/api/projects/get-project.md) |
| `GET` | `/v1/projects/{projectId}/provider-availability` | [Get project provider availability](/docs/reference/api/projects/get-project-provider-availability.md) |
| `GET` | `/v1/integrations` | [List project integrations](/docs/reference/api/projects/list-project-integrations.md) |
---
### List projects
Source: https://platform.solana.com/docs/reference/api/projects/list-projects
> Lists projects in the organization.
`GET /v1/projects`
Requires an [API key](/docs/developing-with-sdp/authentication) with the `projects.read` action.
## Query parameters
- **`includeArchived`** · `boolean` · example `false`
Include archived resources in results.
## Responses
- **`200`** Projects list
- **`data`** · `object` · required
List of projects.
- **`projects`** · `object[]` · required
Projects.
- **`id`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`name`** · `string` · required · example `Payments`
Project name.
- **`slug`** · `string` · required · example `payments`
URL-friendly slug.
- **`description`** · `string | null` · required · example `Payments workflow`
Optional project description.
- **`environment`** · `string` · required · one of `sandbox`, `beta`, `production` · example `sandbox`
Project environment.
- **`settings`** · `object | null` · required
Project settings, or null when none have been set.
- **`webhookUrl`** · `string` · format `uri` · example `https://example.com/webhook`
Webhook URL for event notifications.
- **`metadata`** · `object`
Arbitrary metadata key/value pairs.
- **`*`** · `string`
- **`status`** · `string` · required · one of `active`, `archived` · example `active`
Project status.
- **`createdBy`** · `string` · required · min length 1 · example `usr_example`
Identifier of the creator.
- **`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.
- **`401`** Unauthorized
- **`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.
- **`403`** Forbidden. Requires the `projects.read` 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 `401`.
- **`500`** Internal Server Error
Same fields as `401`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/projects' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/projects", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/projects",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"projects": [
{
"id": "prj_example",
"organizationId": "org_example",
"name": "Payments",
"slug": "payments",
"description": "Payments workflow",
"environment": "sandbox",
"settings": {
"webhookUrl": "https://example.com/webhook",
"metadata": {
"region": "us"
}
},
"status": "active",
"createdBy": "usr_example",
"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: projects.read",
"details": {
"action": "projects.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Get project
Source: https://platform.solana.com/docs/reference/api/projects/get-project
> Gets project details.
`GET /v1/projects/{projectId}`
Requires an [API key](/docs/developing-with-sdp/authentication) with the `projects.read` action.
## Path parameters
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
## Responses
- **`200`** Project
- **`data`** · `object` · required
Project response payload.
- **`project`** · `object` · required
Project details.
- **`id`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`name`** · `string` · required · example `Payments`
Project name.
- **`slug`** · `string` · required · example `payments`
URL-friendly slug.
- **`description`** · `string | null` · required · example `Payments workflow`
Optional project description.
- **`environment`** · `string` · required · one of `sandbox`, `beta`, `production` · example `sandbox`
Project environment.
- **`settings`** · `object | null` · required
Project settings, or null when none have been set.
- **`webhookUrl`** · `string` · format `uri` · example `https://example.com/webhook`
Webhook URL for event notifications.
- **`metadata`** · `object`
Arbitrary metadata key/value pairs.
- **`*`** · `string`
- **`status`** · `string` · required · one of `active`, `archived` · example `active`
Project status.
- **`createdBy`** · `string` · required · min length 1 · example `usr_example`
Identifier of the creator.
- **`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.
- **`401`** Unauthorized
- **`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.
- **`403`** Forbidden. Requires the `projects.read` 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 `401`.
- **`404`** Not Found
Same fields as `401`.
- **`500`** Internal Server Error
Same fields as `401`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/projects/prj_example' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/projects/prj_example", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/projects/prj_example",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"project": {
"id": "prj_example",
"organizationId": "org_example",
"name": "Payments",
"slug": "payments",
"description": "Payments workflow",
"environment": "sandbox",
"settings": {
"webhookUrl": "https://example.com/webhook",
"metadata": {
"region": "us"
}
},
"status": "active",
"createdBy": "usr_example",
"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: projects.read",
"details": {
"action": "projects.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Get project provider availability
Source: https://platform.solana.com/docs/reference/api/projects/get-project-provider-availability
> Lists every provider this deployment's release channel publishes, with whether this project can use it, counting the deployment's provider credentials for the project's environment.
`GET /v1/projects/{projectId}/provider-availability`
The same rule gates every provider entry point, so a provider reported unavailable is refused there with 403, or 503 when the deployment does not hold its credentials. A project outside the caller's organization, or another project for a project-bound API key, returns 404.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `projects.read` action.
## Path parameters
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
## Responses
- **`200`** Project provider availability
- **`data`** · `object` · required
Every provider this deployment's release channel publishes, with whether this project can use it; a provider the channel leaves out is omitted, matching this document. Custody entries list the custody modes the project can set the provider up in (empty when none) in `modes`, and each refused mode with the first failed check in `unavailableModes`; every other entry carries `available`, plus `reason` naming the first failed check when unavailable. A provider is available only when the deployment's release channel includes it, SDP currently offers it, the organization is entitled to it, and the deployment holds its credentials for the project's environment; the release channel offers the same providers to Sandbox and Production projects. Custody `modes` include `managed` only in a Sandbox project whose deployment holds the provider's credentials; `byok` runs on the organization's own credentials and is the only mode a Production project can use.
- **`projectId`** · `string` · required
- **`environment`** · `string` · required · one of `sandbox`, `production`
- **`providers`** · `object[]` · required
One of:
- **family: "custody"**
- **`family`** · `string` · required · always `custody`
- **`provider`** · `string` · required
- **`modes`** · `string[]` · required · each one of `managed`, `byok`
- **`unavailableModes`** · `object[]` · required
- **`mode`** · `string` · required · one of `managed`, `byok`
- **`reason`** · `string` · required · one of `custody_provider_not_in_release_channel`, `custody_mode_not_allowed`, `provider_not_entitled`, `provider_not_in_release_channel`, `provider_not_offered`, `provider_not_configured`
- **family: "earn"**
One of:
- **available: true**
- **`family`** · `string` · required · always `earn`
- **`provider`** · `string` · required · one of `veda`, `upshift`, `perena`, `kamino`, `jupiter_lend`, `ondo`, `hastra`, `wisdomtree`, `plume`
- **`available`** · `boolean` · required · always `true`
- **available: false**
- **`family`** · `string` · required · always `earn`
- **`provider`** · `string` · required · one of `veda`, `upshift`, `perena`, `kamino`, `jupiter_lend`, `ondo`, `hastra`, `wisdomtree`, `plume`
- **`available`** · `boolean` · required · always `false`
- **`reason`** · `string` · required · one of `custody_provider_not_in_release_channel`, `custody_mode_not_allowed`, `provider_not_entitled`, `provider_not_in_release_channel`, `provider_not_offered`, `provider_not_configured`
- **`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.
- **`401`** Unauthorized
- **`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.
- **`403`** Forbidden. Requires the `projects.read` 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 `401`.
- **`404`** Not Found
Same fields as `401`.
- **`500`** Internal Server Error
Same fields as `401`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/projects/prj_example/provider-availability' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/projects/prj_example/provider-availability", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/projects/prj_example/provider-availability",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"projectId": "prj_example",
"environment": "production",
"providers": [
{
"family": "custody",
"provider": "privy",
"modes": [
"byok"
],
"unavailableModes": [
{
"mode": "managed",
"reason": "custody_mode_not_allowed"
}
]
},
{
"family": "custody",
"provider": "fireblocks",
"modes": [],
"unavailableModes": [
{
"mode": "managed",
"reason": "custody_mode_not_allowed"
},
{
"mode": "byok",
"reason": "custody_provider_not_in_release_channel"
}
]
},
{
"family": "earn",
"provider": "kamino",
"available": false,
"reason": "provider_not_entitled"
}
]
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: projects.read",
"details": {
"action": "projects.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### List project integrations
Source: https://platform.solana.com/docs/reference/api/projects/list-project-integrations
> Lists the project's integrations and how each reaches the project.
`GET /v1/integrations`
A provider is listed only when the project can use it, decided by the same rule that admits its use, so an integration listed here is never refused for availability at its entry point. The project comes from the `x-project-id` header, or from the API key's own project.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `projects.read` action.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Project integrations
- **`data`** · `object` · required
The project's integrations, grouped by family in a fixed order. A family this deployment does not run, or with no provider the project can use, is omitted. Each integration's `state` is `not_configured` (the project can set it up, in the listed `modes`), `provided` (SDP supplies it and the project sets nothing up), or `connected` (the project's own setup is active; `mode` says whether it runs on the deployment's provider account, `managed`, or the project's own, `byok`).
- **`projectId`** · `string` · required
- **`environment`** · `string` · required · one of `sandbox`, `production`
- **`families`** · `object[]` · required
One of:
- **family: "custody"**
- **`family`** · `string` · required · always `custody`
- **`integrations`** · `object[]` · required · min items 1
- **`provider`** · `string` · required
- **`category`** · `string` · required · one of `server`, `institutional`
- **`state`** · `object` · required
One of:
- **status: "not_configured"**
- **`status`** · `string` · required · always `not_configured`
- **`modes`** · `string[]` · required · min items 1 · each one of `managed`, `byok`
- **status: "provided"**
- **`status`** · `string` · required · always `provided`
- **`modes`** · `string[]` · required · each one of `managed`, `byok`
- **status: "connected"**
- **`status`** · `string` · required · always `connected`
- **`mode`** · `string` · required · one of `managed`, `byok`
- **family: "rpc"**
- **`family`** · `string` · required · always `rpc`
- **`integrations`** · `object[]` · required · min items 1
- **`provider`** · `string` · required · one of `alchemy`, `helius`, `nodit`, `quicknode`, `triton`, `validationcloud`
- **`state`** · `object` · required
One of:
- **status: "not_configured"**
- **`status`** · `string` · required · always `not_configured`
- **`modes`** · `string[]` · required · min items 1 · each one of `managed`, `byok`
- **status: "provided"**
- **`status`** · `string` · required · always `provided`
- **`modes`** · `string[]` · required · each one of `managed`, `byok`
- **status: "connected"**
- **`status`** · `string` · required · always `connected`
- **`mode`** · `string` · required · one of `managed`, `byok`
- **`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 `projects.read` 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`.
- **`500`** Internal Server Error
Same fields as `400`.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/integrations' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/integrations", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/integrations",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"projectId": "prj_sandbox01",
"environment": "sandbox",
"families": [
{
"family": "custody",
"integrations": [
{
"provider": "privy",
"category": "server",
"state": {
"status": "connected",
"mode": "byok"
}
},
{
"provider": "fireblocks",
"category": "institutional",
"state": {
"status": "not_configured",
"modes": [
"managed",
"byok"
]
}
}
]
}
]
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: projects.read",
"details": {
"action": "projects.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
## Payments
### Payments
Source: https://platform.solana.com/docs/reference/api/payments
> Wallet balances, transfers, and payment flows.
## Balances and transactions
| Method | Endpoint | Summary |
| --- | --- | --- |
| `GET` | `/v1/payments/wallets/{walletId}/balances` | [Get wallet balances](/docs/reference/api/payments/get-payment-wallet-balances.md) |
| `GET` | `/v1/transactions` | [List transactions](/docs/reference/api/payments/list-unified-transactions.md) |
## Transfers
| Method | Endpoint | Summary |
| --- | --- | --- |
| `POST` | `/v1/payments/transfers` | [Execute transfer (custody)](/docs/reference/api/payments/create-payment-transfer.md) |
| `GET` | `/v1/payments/transfers` | [List transfers](/docs/reference/api/payments/list-payment-transfers.md) |
| `GET` | `/v1/payments/transfers/{transferId}` | [Get transfer](/docs/reference/api/payments/get-payment-transfer.md) |
## Transfer batches
| Method | Endpoint | Summary |
| --- | --- | --- |
| `POST` | `/v1/payments/transfer-batches/estimate` | [Estimate transfer batch](/docs/reference/api/payments/estimate-payment-transfer-batch.md) |
| `POST` | `/v1/payments/transfer-batches` | [Create transfer batch](/docs/reference/api/payments/create-payment-transfer-batch.md) |
| `GET` | `/v1/payments/transfer-batches` | [List transfer batches](/docs/reference/api/payments/list-payment-transfer-batches.md) |
| `POST` | `/v1/payments/transfer-batches/{batchId}/retry` | [Retry a transfer batch's failed recipients](/docs/reference/api/payments/retry-payment-transfer-batch.md) |
| `GET` | `/v1/payments/transfer-batches/{batchId}` | [Get transfer batch](/docs/reference/api/payments/get-payment-transfer-batch.md) |
## Recurring payments
| Method | Endpoint | Summary |
| --- | --- | --- |
| `POST` | `/v1/payments/recurring-payments` | [Create recurring payment](/docs/reference/api/payments/create-payment-recurring-payment.md) |
| `GET` | `/v1/payments/recurring-payments` | [List recurring payments](/docs/reference/api/payments/list-payment-recurring-payments.md) |
| `PATCH` | `/v1/payments/recurring-payments/{id}` | [Update recurring payment](/docs/reference/api/payments/update-payment-recurring-payment.md) |
| `GET` | `/v1/payments/recurring-payments/{id}` | [Get recurring payment](/docs/reference/api/payments/get-payment-recurring-payment.md) |
| `POST` | `/v1/payments/recurring-payments/{id}/activate` | [Activate recurring payment](/docs/reference/api/payments/activate-payment-recurring-payment.md) |
| `POST` | `/v1/payments/recurring-payments/{id}/cancel` | [Cancel recurring payment](/docs/reference/api/payments/cancel-payment-recurring-payment.md) |
| `POST` | `/v1/payments/recurring-payments/{id}/collect` | [Collect recurring payment](/docs/reference/api/payments/collect-payment-recurring-payment.md) |
| `POST` | `/v1/payments/recurring-payments/{id}/resume` | [Resume recurring payment](/docs/reference/api/payments/resume-payment-recurring-payment.md) |
| `GET` | `/v1/payments/recurring-payments/{id}/collection-attempts` | [List recurring payment collection attempts](/docs/reference/api/payments/list-payment-recurring-payment-collection-attempts.md) |
## Payment requests
| Method | Endpoint | Summary |
| --- | --- | --- |
| `POST` | `/v1/payments/requests` | [Create payment request](/docs/reference/api/payments/create-payment-request.md) |
| `GET` | `/v1/payments/requests` | [List payment requests](/docs/reference/api/payments/list-payment-requests.md) |
| `GET` | `/v1/payments/requests/{paymentRequestId}` | [Get payment request](/docs/reference/api/payments/get-payment-request.md) |
| `GET` | `/v1/payments/requests/{paymentRequestId}/link` | [Get payment request link](/docs/reference/api/payments/get-payment-request-link.md) |
| `POST` | `/v1/payments/requests/{paymentRequestId}/cancel` | [Cancel payment request](/docs/reference/api/payments/cancel-payment-request.md) |
## Subscription plans
| Method | Endpoint | Summary |
| --- | --- | --- |
| `POST` | `/v1/payments/subscription-plans` | [Create subscription plan](/docs/reference/api/payments/create-payment-subscription-plan.md) |
| `GET` | `/v1/payments/subscription-plans` | [List subscription plans](/docs/reference/api/payments/list-payment-subscription-plans.md) |
| `POST` | `/v1/payments/subscription-plans/{planId}/prepare-create` | [Prepare subscription plan creation](/docs/reference/api/payments/prepare-payment-subscription-plan-create.md) |
| `GET` | `/v1/payments/subscription-plans/{planId}` | [Get subscription plan](/docs/reference/api/payments/get-payment-subscription-plan.md) |
| `PATCH` | `/v1/payments/subscription-plans/{planId}` | [Update subscription plan](/docs/reference/api/payments/update-payment-subscription-plan.md) |
## Subscriptions
| Method | Endpoint | Summary |
| --- | --- | --- |
| `POST` | `/v1/payments/subscriptions` | [Create subscription](/docs/reference/api/payments/create-payment-subscription.md) |
| `GET` | `/v1/payments/subscriptions` | [List subscriptions](/docs/reference/api/payments/list-payment-subscriptions.md) |
| `POST` | `/v1/payments/subscriptions/{subscriptionId}/prepare-authorization` | [Prepare subscription authorization](/docs/reference/api/payments/prepare-payment-subscription-authorization.md) |
| `POST` | `/v1/payments/subscriptions/{subscriptionId}/prepare-cancel` | [Prepare subscription cancellation](/docs/reference/api/payments/prepare-payment-subscription-cancel.md) |
| `POST` | `/v1/payments/subscriptions/{subscriptionId}/prepare-resume` | [Prepare subscription resume](/docs/reference/api/payments/prepare-payment-subscription-resume.md) |
| `POST` | `/v1/payments/subscriptions/{subscriptionId}/prepare-collection` | [Prepare subscription collection](/docs/reference/api/payments/prepare-payment-subscription-collection.md) |
| `GET` | `/v1/payments/subscriptions/{subscriptionId}` | [Get subscription](/docs/reference/api/payments/get-payment-subscription.md) |
| `GET` | `/v1/payments/subscriptions/{subscriptionId}/collection-attempts` | [List subscription collection attempts](/docs/reference/api/payments/list-payment-subscription-collection-attempts.md) |
---
## Payments: Balances and transactions
### Get wallet balances
Source: https://platform.solana.com/docs/reference/api/payments/get-payment-wallet-balances
> Retrieves balances for a custody wallet.
`GET /v1/payments/wallets/{walletId}/balances`
If any SOL or SPL balance read fails, returns 503 instead of partial or zero balances. Optional minimumSlot requires confirmed account state at or after that Solana slot; the response acknowledges the bound in data.balanceReadContext.minimumSlot. Wallet lifecycle and provisioning are managed through /v1/wallets.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `wallets.read` action.
## Path parameters
- **`walletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Provider wallet ID — the `walletId` field returned by GET /v1/wallets. The wallet record `id` and the public key are not accepted.
## Query parameters
- **`minimumSlot`** · `string` · pattern `^(0|[1-9]\d*)$`
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Wallet balances
- **`data`** · `object` · required
Wallet balances response payload.
- **`walletBalances`** · `object` · required
Wallet balances details.
- **`walletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Provider wallet ID — the `walletId` field returned by GET /v1/wallets.
- **`address`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Wallet address.
- **`balances`** · `object[]` · required
Token balances.
- **`token`** · `string` · required · example `USDC`
Token symbol or mint address.
- **`mint`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Token mint address.
- **`amount`** · `string` · required · example `100000000`
Raw amount in smallest units.
- **`uiAmount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`decimals`** · `integer` · required · example `6`
Token decimals.
- **`usdPrice`** · `number` · example `1`
Resolved USD price per token when available.
- **`usdValue`** · `number` · example `100`
Resolved USD value of this balance when pricing is available.
- **`confidential`** · `boolean` · example `false`
Confidential balance flag (when applicable).
- **`balanceReadContext`** · `object`
- **`minimumSlot`** · `integer` · required · min 0 · max 9007199254740991
- **`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 `wallets.read` 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`.
- **`500`** Internal Server Error
Same fields as `400`.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/payments/wallets/privy_wallet_123/balances' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/wallets/privy_wallet_123/balances", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/payments/wallets/privy_wallet_123/balances",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"walletBalances": {
"walletId": "privy_wallet_123",
"address": "So11111111111111111111111111111111111111112",
"balances": [
{
"token": "USDC",
"mint": "So11111111111111111111111111111111111111112",
"amount": "100000000",
"uiAmount": "100.00",
"decimals": 6,
"usdPrice": 1,
"usdValue": 100,
"confidential": false
}
]
},
"balanceReadContext": {
"minimumSlot": 1
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: wallets.read",
"details": {
"action": "wallets.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### List transactions
Source: https://platform.solana.com/docs/reference/api/payments/list-unified-transactions
> Lists the project's transactions across SDP modules with stable cursor pagination.
`GET /v1/transactions`
Requires `transactions.read`. Every row also needs the `read` action of the module it comes from (which its `manage` or `execute` includes): a Payments row needs `transfers.read` for a transfer or deposit, `transfer_batches.read` for a batch item, `recurring.read` for a recurring collection, `payment_requests.read` for a payment request's fulfilment; every other module's rows need that module's `read`. Rows the caller cannot read are left out. Requesting a module, or a Payments `kind`, without its action returns `403` `INSUFFICIENT_PERMISSIONS` naming it.
`search` matches transaction id, module id, or signature as a prefix, not a substring. The endpoint is metered (60/min per credential, 240/min per organization); exceeding the quota returns `429`, and `503` indicates the quota backend was unavailable.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `transactions.read` action.
## Query parameters
- **`module`** · `string` · one of `payments`, `earn`
- **`kind`** · `string`
- **`status`** · `string` · one of `pending`, `succeeded`, `failed`, `canceled`
- **`custodyWalletId`** · `string` · max length 128
- **`counterpartyId`** · `string` · max length 128
- **`token`** · `string` · max length 128
- **`search`** · `string` · min length 3 · max length 200
Prefix match against transaction id, module id, or signature — not a substring search.
- **`createdAtFrom`** · `string` · format `date-time`
- **`createdAtTo`** · `string` · format `date-time`
- **`cursor`** · `string`
- **`limit`** · `integer` · min 1 · max 100 · default `50`
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Unified transactions
- **`data`** · `object` · required
- **`transactions`** · `object[]` · required
One of:
- **module: "payments"**
- **`id`** · `string` · required
- **`moduleId`** · `string` · required
- **`status`** · `string` · required · one of `pending`, `succeeded`, `failed`, `canceled`
- **`organizationId`** · `string` · required
- **`projectId`** · `string | null` · required
- **`custodyWalletId`** · `string | null` · required
- **`custodyWalletLabel`** · `string | null` · required
- **`token`** · `string | null` · required
- **`amount`** · `string | null` · required
- **`counterpartyId`** · `string | null` · required
- **`signature`** · `string | null` · required
- **`createdAt`** · `string` · required
- **`module`** · `string` · required · always `payments`
- **`kind`** · `string` · required
- **`moduleStatus`** · `string` · required · one of `pending`, `processing`, `confirmed`, `finalized`, `failed`, `awaiting_payment`, `settling`, `completed`, `canceled`, `expired`
- **module: "earn"**
- **`id`** · `string` · required
- **`moduleId`** · `string` · required
- **`status`** · `string` · required · one of `pending`, `succeeded`, `failed`, `canceled`
- **`organizationId`** · `string` · required
- **`projectId`** · `string | null` · required
- **`custodyWalletId`** · `string | null` · required
- **`custodyWalletLabel`** · `string | null` · required
- **`token`** · `string | null` · required
- **`amount`** · `string | null` · required
- **`counterpartyId`** · `string | null` · required
- **`signature`** · `string | null` · required
- **`createdAt`** · `string` · required
- **`module`** · `string` · required · always `earn`
- **`kind`** · `string` · required · one of `deposit`, `withdraw`
- **`moduleStatus`** · `string` · required · one of `requested`, `processing`, `pending_approval`, `completed`, `partially_completed`, `failed`, `cancelled`, `submitted`, `confirmed`, `finalized`
- **`nextCursor`** · `string | null` · required
- **`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 `transactions.read` 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`.
- **`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"
curl 'https://api.solana.com/v1/transactions' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/transactions", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/transactions",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"transactions": [
{
"id": "_example",
"moduleId": "module_example",
"status": "pending",
"organizationId": "organization_example",
"projectId": "project_example",
"custodyWalletId": "custodywallet_example",
"custodyWalletLabel": "example",
"token": "example",
"amount": "example",
"counterpartyId": "counterparty_example",
"signature": "example",
"createdAt": "example",
"module": "payments",
"kind": "example",
"moduleStatus": "pending"
}
],
"nextCursor": "example"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: transactions.read",
"details": {
"action": "transactions.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
## Payments: Transfers
### Execute transfer (custody)
Source: https://platform.solana.com/docs/reference/api/payments/create-payment-transfer
> 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).
---
### List transfers
Source: https://platform.solana.com/docs/reference/api/payments/list-payment-transfers
> Lists persisted payment transfers for the authenticated scope.
`GET /v1/payments/transfers`
Set custodyWalletId to select one exact SDP wallet; observed address history is opt-in with includeObserved=true.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `transfers.read` action.
## Query parameters
- **`custodyWalletId`** · `string` · min length 1 · example `cwlt_example`
Filter persisted transfers by exact SDP Wallet ID.
- **`search`** · `string` · max length 200 · example `xfr_example`
Search transfer ID, signature, source or destination address, memo, counterparty ID, or counterparty display name. Use at least 3 non-whitespace characters; a blank value is treated as no search filter.
- **`token`** · `string` · example `USDC`
Filter by token symbol or mint.
- **`direction`** · `string` · one of `inbound`, `outbound` · example `outbound`
Filter by transfer direction.
- **`status`** · `string` · example `confirmed,finalized`
Filter by transfer status. Accepts a comma-separated list of statuses.
- **`category`** · `string` · always `wallet`
Filter by transfer category.
- **`type`** · `string` · example `transfer,transfer_batch`
Filter by one or more comma-separated transfer types.
- **`counterpartyId`** · `string` · min length 1 · example `cpty_example`
Filter transfers tied to a specific counterparty.
- **`from`** · `string` · format `date-time` · example `2025-01-01T00:00:00.000Z`
Filter from timestamp.
- **`to`** · `string` · format `date-time` · example `2025-01-02T00:00:00.000Z`
Filter to timestamp.
- **`includeObserved`** · `string` · one of `true`, `false` · default `false` · example `false`
When custodyWalletId is supplied, also include RPC-observed activity for that exact wallet row's address. Observed rows have custodyWalletId null.
- **`sortBy`** · `string` · one of `createdAt`, `updatedAt`, `amount`, `status` · default `createdAt` · example `createdAt`
Field used to sort the transfer list.
- **`sortDirection`** · `string` · one of `asc`, `desc` · default `desc` · example `desc`
Sort direction.
- **`page`** · `integer` · greater than 0 · default `1` · example `1`
Page number (1-based).
- **`pageSize`** · `integer` · min 1 · max 100 · default `20` · example `20`
Items per page.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Transfer list
- **`data`** · `object[]` · required
- **`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
Pagination metadata.
- **`total`** · `integer` · required · min 0
Total items.
- **`page`** · `integer` · required · greater than 0
Current page number.
- **`pageSize`** · `integer` · required · greater than 0
Items per page.
- **`hasMore`** · `boolean` · required
Whether more pages exist.
- **`requestId`** · `string` · required · min length 1 · example `req_example`
Request identifier for tracing.
- **`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.read` 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`.
- **`500`** Internal Server Error
Same fields as `400`.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/payments/transfers' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/transfers", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/payments/transfers",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": [
{
"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": {
"total": 1,
"page": 1,
"pageSize": 1,
"hasMore": false,
"requestId": "req_example"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: transfers.read",
"details": {
"action": "transfers.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Get transfer
Source: https://platform.solana.com/docs/reference/api/payments/get-payment-transfer
> Retrieves details for a specific transfer.
`GET /v1/payments/transfers/{transferId}`
Requires an [API key](/docs/developing-with-sdp/authentication) with the `transfers.read` action.
## Path parameters
- **`transferId`** · `string` · required · min length 1 · example `xfr_example`
Transfer identifier (SDP record ID, not the on-chain signature).
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Transfer details
- **`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.
- **`401`** Unauthorized
- **`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.
- **`403`** Forbidden. Requires the `transfers.read` 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 `401`.
- **`404`** Not Found
Same fields as `401`.
- **`500`** Internal Server Error
Same fields as `401`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/payments/transfers/xfr_example' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/transfers/xfr_example", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/payments/transfers/xfr_example",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## 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.read",
"details": {
"action": "transfers.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
## Payments: Transfer batches
### Estimate transfer batch
Source: https://platform.solana.com/docs/reference/api/payments/estimate-payment-transfer-batch
> Validates an exact-wallet transfer batch request and estimates transaction chunking and fees.
`POST /v1/payments/transfer-batches/estimate`
`warnings` lists what would make the batch fail: a native SOL amount below the rent-exempt minimum sent to an address with no account, or a source wallet left below the minimum; create refuses those with 409 `AMOUNT_BELOW_RENT_MINIMUM`. Returns 503 `ESTIMATE_NOT_AVAILABLE` when Solana RPC cannot price a chunk's fee; the estimate stores nothing, so retry it.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `transfer_batches.read` action.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Request body
`application/json` · required
Estimate transaction chunking and fees for a transfer batch.
- **`projectId`** · `string` · min length 1 · example `prj_example`
Project identifier for the transfer batch context.
- **`sourceCustodyWalletId`** · `string` · required · min length 1 · example `cwlt_example`
Exact SDP Wallet ID (`id` from `GET /v1/wallets`).
- **`token`** · `string` · required · example `SOL`
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.
- **`recipients`** · `object[]` · required · min items 1 · max items 500
Counterparty-account recipients to pay in this batch.
- **`externalId`** · `string` · min length 1 · max length 256
- **`counterpartyId`** · `string` · min length 1
- **`counterpartyAccountId`** · `string` · required · min length 1
- **`amount`** · `string` · required
- **`externalId`** · `string` · min length 1 · max length 256 · example `payroll_2026_06_30`
Optional: an estimate moves nothing, so it names no payout run. Pass the run's `externalId` to estimate the batch you will create with it.
## Responses
- **`200`** Transfer batch estimate
- **`data`** · `object` · required
Transfer batch estimate response payload.
- **`estimate`** · `object` · required
Transfer batch estimate.
- **`recipientCount`** · `integer` · required
Requested recipient count.
- **`transactionCount`** · `integer` · required
Estimated number of Solana transaction chunks.
- **`estimatedFees`** · `object` · required
Estimated fee components.
- **`networkFeeLamports`** · `string` · required · example `5000`
Estimated base network fee in lamports.
- **`priorityFeeLamports`** · `string` · required · example `0`
Estimated priority fee in lamports.
- **`tokenAccountRentLamports`** · `string` · required · example `0`
Estimated rent needed for newly-created recipient token accounts.
- **`sponsored`** · `boolean` · required · example `true`
Whether SDP sponsors transaction fees for this batch.
- **`warnings`** · `object[]` · required
What would make the batch fail. Empty when the batch can be sent as estimated.
One of:
- **code: "AMOUNT_BELOW_RENT_MINIMUM"**
- **`code`** · `string` · required · always `AMOUNT_BELOW_RENT_MINIMUM`
A native SOL recipient's address holds no account and its amount is below the rent-exempt minimum.
- **`recipientIndex`** · `integer` · required
Zero-based index of the recipient in the request.
- **`minimumAmount`** · `string` · required
The smallest SOL amount the address can receive while it has no account.
- **`message`** · `string` · required
Human-readable warning.
- **code: "SOURCE_BELOW_RENT_MINIMUM"**
- **`code`** · `string` · required · always `SOURCE_BELOW_RENT_MINIMUM`
The source wallet would be left holding more than nothing but less than the rent-exempt minimum, at the end or after some order of the batch's concurrent transactions.
- **`sourceRemainder`** · `string` · required
The SOL the source wallet would be left with.
- **`minimumAmount`** · `string` · required
The rent-exempt minimum for a SOL wallet.
- **`message`** · `string` · required
Human-readable warning.
- **`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 `transfer_batches.read` 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`.
- **`500`** Internal Server Error
Same fields as `400`.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
curl -X POST 'https://api.solana.com/v1/payments/transfer-batches/estimate' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"projectId": "prj_example",
"sourceCustodyWalletId": "cwlt_example",
"token": "SOL",
"recipients": [
{
"externalId": "payroll_row_001",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"amount": "25.00"
}
],
"externalId": "payroll_2026_06_30"
}'
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/transfer-batches/estimate", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"projectId": "prj_example",
"sourceCustodyWalletId": "cwlt_example",
"token": "SOL",
"recipients": [
{
"externalId": "payroll_row_001",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"amount": "25.00"
}
],
"externalId": "payroll_2026_06_30"
})
});
```
```python tab="Python"
import os, requests
r = requests.post("https://api.solana.com/v1/payments/transfer-batches/estimate",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"},
json={
"projectId": "prj_example",
"sourceCustodyWalletId": "cwlt_example",
"token": "SOL",
"recipients": [
{
"externalId": "payroll_row_001",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"amount": "25.00"
}
],
"externalId": "payroll_2026_06_30"
})
```
## Example responses
```json tab="200"
{
"data": {
"estimate": {
"recipientCount": 1,
"transactionCount": 1,
"estimatedFees": {
"networkFeeLamports": "5000",
"priorityFeeLamports": "0",
"tokenAccountRentLamports": "0",
"sponsored": true
},
"warnings": [
{
"code": "AMOUNT_BELOW_RENT_MINIMUM",
"recipientIndex": 1,
"minimumAmount": "example",
"message": "example"
}
]
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: transfer_batches.read",
"details": {
"action": "transfer_batches.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Create transfer batch
Source: https://platform.solana.com/docs/reference/api/payments/create-payment-transfer-batch
> Executes an outbound transfer batch from one exact SDP Wallet ID, chunks recipients into Solana transactions, and returns the batch, recipient, and transfer records.
`POST /v1/payments/transfer-batches`
Requires an Idempotency-Key: a retry with the same key and request returns the original batch without another on-chain submission. Recipient order does not change the request. A native SOL batch is refused with 409 `AMOUNT_BELOW_RENT_MINIMUM`, before anything is written or sent, when a recipient whose address has no account gets less than the rent-exempt minimum (`details.recipientIndexes`) or the source wallet would be left holding more than nothing but less than the minimum (`details.sourceRemainder`). A token batch is refused with 409 `SPONSORED_RENT_OVER_CAP` when one recipient's new token account costs more rent than the sponsor funds in a transaction (`details.recipientIndex`). A recipient's counterparty account archived while the batch is being prepared returns 404, and one whose address changed returns 409 `TRANSFER_BATCH_DESTINATION_CHANGED` (`details.counterpartyAccountIds`); either way nothing is written or sent, so the same request can be sent again. A chunk the source wallet cannot fund fails its recipients and transfer with the reason in `error` (the wallet does not have enough funds); the batch still returns 200. An `externalId` already used by a batch on the same project and source wallet never pays again: the same request from the same credential returns that batch, and any other returns `409 TRANSFER_BATCH_RUN_EXISTS` with its `batchId` in `error.details`. To finish a batch after a partial failure, call `POST /v1/payments/transfer-batches/{batchId}/retry`: it sends only the batch's `failed` recipients. Never resend the whole batch under a new Idempotency-Key, which pays the recipients that succeeded again.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `transfer_batches.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 a custody-executed outbound transfer batch.
- **`projectId`** · `string` · min length 1 · example `prj_example`
Project identifier for the transfer batch context.
- **`sourceCustodyWalletId`** · `string` · required · min length 1 · example `cwlt_example`
Exact SDP Wallet ID (`id` from `GET /v1/wallets`).
- **`token`** · `string` · required · example `SOL`
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.
- **`recipients`** · `object[]` · required · min items 1 · max items 500
Counterparty-account recipients to pay in this batch.
- **`externalId`** · `string` · min length 1 · max length 256
- **`counterpartyId`** · `string` · min length 1
- **`counterpartyAccountId`** · `string` · required · min length 1
- **`amount`** · `string` · required
- **`externalId`** · `string` · required · min length 1 · max length 256 · example `payroll_2026_06_30`
Your ID for this payout run, unique per project and source wallet. Derive it from your own data so a restarted worker computes the same value (for example `payroll-2026-05`); never generate it at random, which gives no protection. Sending the same request again with this `externalId`, from the same credential, under any Idempotency-Key returns the existing batch; a different request with it returns `409 TRANSFER_BATCH_RUN_EXISTS` with the existing batch's `batchId` in `error.details`. Neither pays anyone again. New recipients go in a new run with a new `externalId`. Required: it is what stops a resend under a new Idempotency-Key from paying the same recipients twice.
## Responses
- **`200`** Transfer batch created
- **`data`** · `object` · required
Transfer batch response payload.
- **`batch`** · `object` · required
Transfer batch details.
- **`id`** · `string` · required · example `xbatch_example`
Transfer batch identifier.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier for the transfer batch.
- **`externalId`** · `string | null` · required · example `payroll_2026_06_30`
Caller-provided batch correlation ID.
- **`sourceCustodyWalletId`** · `string | null` · required · example `cwlt_example`
Exact source SDP Wallet ID, or null for unresolved legacy batches.
- **`sourceProviderWalletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Source Provider wallet ID retained as evidence.
- **`sourceAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Source custody wallet address.
- **`token`** · `string` · required · example `SOL`
Token symbol or mint address.
- **`status`** · `string` · required · one of `pending`, `processing`, `confirmed`, `failed`, `partially_failed`, `archived` · example `processing`
Transfer batch status.
- **`totalAmount`** · `string | null` · required · example `1000.00`
Total requested batch amount in UI units, when computed.
- **`recipientCount`** · `integer` · required · example `20`
Number of recipient rows in the batch.
- **`transactionCount`** · `integer` · required · example `1`
Number of Solana transaction chunks for the batch.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`recipients`** · `object[]`
Recipient rows for the batch, included on detail responses.
- **`id`** · `string` · required
Transfer batch recipient identifier.
- **`batchId`** · `string` · required
Parent transfer batch identifier.
- **`transferId`** · `string | null` · required · min length 1 · example `xfr_example`
Payment transfer chunk that settled this recipient, once assigned.
- **`externalId`** · `string | null` · required · example `payroll_row_001`
Caller-provided recipient correlation ID.
- **`counterpartyId`** · `string` · required · example `cpty_example`
Counterparty identifier for the recipient.
- **`counterpartyAccountId`** · `string` · required · example `cpacct_example`
Counterparty account identifier for the recipient.
- **`destination`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Resolved Solana destination address.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`status`** · `string` · required · one of `pending`, `processing`, `confirmed`, `failed`, `archived` · example `pending`
Transfer batch recipient status.
- **`error`** · `string | null` · required
Recipient-level failure reason, when available. A recipient the source wallet could not fund reads "The wallet used for this payment does not have enough funds. Add funds and try again."
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`transfers`** · `object[]`
Payment transfer chunk records associated with this batch.
- **`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 `transfer_batches.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/transfer-batches' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"projectId": "prj_example",
"sourceCustodyWalletId": "cwlt_example",
"token": "SOL",
"recipients": [
{
"externalId": "payroll_row_001",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"amount": "25.00"
}
],
"externalId": "payroll_2026_06_30"
}'
```
```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/transfer-batches", {
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",
"token": "SOL",
"recipients": [
{
"externalId": "payroll_row_001",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"amount": "25.00"
}
],
"externalId": "payroll_2026_06_30"
})
});
```
```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/transfer-batches",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
json={
"projectId": "prj_example",
"sourceCustodyWalletId": "cwlt_example",
"token": "SOL",
"recipients": [
{
"externalId": "payroll_row_001",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"amount": "25.00"
}
],
"externalId": "payroll_2026_06_30"
})
```
## Example responses
```json tab="200"
{
"data": {
"batch": {
"id": "xbatch_example",
"organizationId": "org_example",
"projectId": "prj_example",
"externalId": "payroll_2026_06_30",
"sourceCustodyWalletId": "cwlt_example",
"sourceProviderWalletId": "privy_wallet_123",
"sourceAddress": "So11111111111111111111111111111111111111112",
"token": "SOL",
"status": "processing",
"totalAmount": "1000.00",
"recipientCount": 20,
"transactionCount": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"recipients": [
{
"id": "_example",
"batchId": "batch_example",
"transferId": "xfr_example",
"externalId": "payroll_row_001",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"destination": "So11111111111111111111111111111111111111112",
"amount": "100.00",
"status": "pending",
"error": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
],
"transfers": [
{
"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: transfer_batches.execute",
"details": {
"action": "transfer_batches.execute"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### List transfer batches
Source: https://platform.solana.com/docs/reference/api/payments/list-payment-transfer-batches
> Lists transfer batches for the authenticated organization or project scope.
`GET /v1/payments/transfer-batches`
Requires an [API key](/docs/developing-with-sdp/authentication) with the `transfer_batches.read` action.
## Query parameters
- **`sourceCustodyWalletId`** · `string` · min length 1 · example `cwlt_example`
Filter by exact source SDP Wallet ID.
- **`token`** · `string` · example `SOL`
Filter by token symbol or mint.
- **`status`** · `string` · one of `pending`, `processing`, `confirmed`, `failed`, `partially_failed`, `archived` · example `processing`
Filter by transfer batch status.
- **`externalId`** · `string` · min length 1 · max length 256 · example `payroll_2026_06_30`
Filter by caller-provided batch correlation ID.
- **`page`** · `integer` · greater than 0 · default `1` · example `1`
Page number (1-based).
- **`pageSize`** · `integer` · min 1 · max 100 · default `20` · example `20`
Number of batches per page.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Transfer batch list
- **`data`** · `object[]` · required
- **`id`** · `string` · required · example `xbatch_example`
Transfer batch identifier.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier for the transfer batch.
- **`externalId`** · `string | null` · required · example `payroll_2026_06_30`
Caller-provided batch correlation ID.
- **`sourceCustodyWalletId`** · `string | null` · required · example `cwlt_example`
Exact source SDP Wallet ID, or null for unresolved legacy batches.
- **`sourceProviderWalletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Source Provider wallet ID retained as evidence.
- **`sourceAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Source custody wallet address.
- **`token`** · `string` · required · example `SOL`
Token symbol or mint address.
- **`status`** · `string` · required · one of `pending`, `processing`, `confirmed`, `failed`, `partially_failed`, `archived` · example `processing`
Transfer batch status.
- **`totalAmount`** · `string | null` · required · example `1000.00`
Total requested batch amount in UI units, when computed.
- **`recipientCount`** · `integer` · required · example `20`
Number of recipient rows in the batch.
- **`transactionCount`** · `integer` · required · example `1`
Number of Solana transaction chunks for the batch.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`meta`** · `object` · required
Pagination metadata.
- **`total`** · `integer` · required · min 0
Total items.
- **`page`** · `integer` · required · greater than 0
Current page number.
- **`pageSize`** · `integer` · required · greater than 0
Items per page.
- **`hasMore`** · `boolean` · required
Whether more pages exist.
- **`requestId`** · `string` · required · min length 1 · example `req_example`
Request identifier for tracing.
- **`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 `transfer_batches.read` 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`.
- **`500`** Internal Server Error
Same fields as `400`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/payments/transfer-batches' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/transfer-batches", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/payments/transfer-batches",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": [
{
"id": "xbatch_example",
"organizationId": "org_example",
"projectId": "prj_example",
"externalId": "payroll_2026_06_30",
"sourceCustodyWalletId": "cwlt_example",
"sourceProviderWalletId": "privy_wallet_123",
"sourceAddress": "So11111111111111111111111111111111111111112",
"token": "SOL",
"status": "processing",
"totalAmount": "1000.00",
"recipientCount": 20,
"transactionCount": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
],
"meta": {
"total": 1,
"page": 1,
"pageSize": 1,
"hasMore": false,
"requestId": "req_example"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: transfer_batches.read",
"details": {
"action": "transfer_batches.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Retry a transfer batch's failed recipients
Source: https://platform.solana.com/docs/reference/api/payments/retry-payment-transfer-batch
> Sends the batch's failed recipients again, from the same source wallet, and no other recipient.
`POST /v1/payments/transfer-batches/{batchId}/retry`
A failed recipient was not paid: its transaction never reached the network or the network rejected it. Recipients still `pending` or `processing` may land and are never sent again. Each resent recipient keeps its row and links to its new chunk transfer.
Takes no body and requires an `Idempotency-Key`. A retry with the same key sends nothing again: while its response is stored, it returns that saved response, and after a server error or once the response has expired, the batch as it stands. Read the batch's current state with `GET /v1/payments/transfer-batches/{batchId}`, and use a new key for each retry you mean to send.
Any credential that may send from the batch's wallet can retry it. Each recipient is paid the address the batch recorded for it. Only recipients that are still `failed` are ever sent, so a retry under a new key never pays anyone twice.
Returns `409`, sending nothing: `TRANSFER_BATCH_NOTHING_TO_RETRY` when the batch has no failed recipients (while it is `processing`, its unsent recipients become `failed` only once its request can no longer send them), `TRANSFER_BATCH_RETRY_CLAIMED` when a concurrent retry claimed them first, and `TRANSFER_BATCH_DESTINATION_CHANGED` (`details.recipientIds`) when a failed recipient's counterparty account has changed its address since (send that recipient in a new batch). A native SOL retry is refused with `409` `AMOUNT_BELOW_RENT_MINIMUM`, as create is, when a failed recipient would still be paid less than the rent-exempt minimum at an address with no account; its recipients stay `failed`.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `transfer_batches.execute` action.
## Path parameters
- **`batchId`** · `string` · required · min length 1 · example `xbatch_example`
Transfer batch identifier.
## 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).
## Responses
- **`200`** Failed recipients resent
- **`data`** · `object` · required
Transfer batch response payload.
- **`batch`** · `object` · required
Transfer batch details.
- **`id`** · `string` · required · example `xbatch_example`
Transfer batch identifier.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier for the transfer batch.
- **`externalId`** · `string | null` · required · example `payroll_2026_06_30`
Caller-provided batch correlation ID.
- **`sourceCustodyWalletId`** · `string | null` · required · example `cwlt_example`
Exact source SDP Wallet ID, or null for unresolved legacy batches.
- **`sourceProviderWalletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Source Provider wallet ID retained as evidence.
- **`sourceAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Source custody wallet address.
- **`token`** · `string` · required · example `SOL`
Token symbol or mint address.
- **`status`** · `string` · required · one of `pending`, `processing`, `confirmed`, `failed`, `partially_failed`, `archived` · example `processing`
Transfer batch status.
- **`totalAmount`** · `string | null` · required · example `1000.00`
Total requested batch amount in UI units, when computed.
- **`recipientCount`** · `integer` · required · example `20`
Number of recipient rows in the batch.
- **`transactionCount`** · `integer` · required · example `1`
Number of Solana transaction chunks for the batch.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`recipients`** · `object[]`
Recipient rows for the batch, included on detail responses.
- **`id`** · `string` · required
Transfer batch recipient identifier.
- **`batchId`** · `string` · required
Parent transfer batch identifier.
- **`transferId`** · `string | null` · required · min length 1 · example `xfr_example`
Payment transfer chunk that settled this recipient, once assigned.
- **`externalId`** · `string | null` · required · example `payroll_row_001`
Caller-provided recipient correlation ID.
- **`counterpartyId`** · `string` · required · example `cpty_example`
Counterparty identifier for the recipient.
- **`counterpartyAccountId`** · `string` · required · example `cpacct_example`
Counterparty account identifier for the recipient.
- **`destination`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Resolved Solana destination address.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`status`** · `string` · required · one of `pending`, `processing`, `confirmed`, `failed`, `archived` · example `pending`
Transfer batch recipient status.
- **`error`** · `string | null` · required
Recipient-level failure reason, when available. A recipient the source wallet could not fund reads "The wallet used for this payment does not have enough funds. Add funds and try again."
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`transfers`** · `object[]`
Payment transfer chunk records associated with this batch.
- **`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 `transfer_batches.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/transfer-batches/xbatch_example/retry' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY"
```
```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/transfer-batches/xbatch_example/retry", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey
}
});
```
```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/transfer-batches/xbatch_example/retry",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key})
```
## Example responses
```json tab="200"
{
"data": {
"batch": {
"id": "xbatch_example",
"organizationId": "org_example",
"projectId": "prj_example",
"externalId": "payroll_2026_06_30",
"sourceCustodyWalletId": "cwlt_example",
"sourceProviderWalletId": "privy_wallet_123",
"sourceAddress": "So11111111111111111111111111111111111111112",
"token": "SOL",
"status": "processing",
"totalAmount": "1000.00",
"recipientCount": 20,
"transactionCount": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"recipients": [
{
"id": "_example",
"batchId": "batch_example",
"transferId": "xfr_example",
"externalId": "payroll_row_001",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"destination": "So11111111111111111111111111111111111111112",
"amount": "100.00",
"status": "pending",
"error": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
],
"transfers": [
{
"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: transfer_batches.execute",
"details": {
"action": "transfer_batches.execute"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Get transfer batch
Source: https://platform.solana.com/docs/reference/api/payments/get-payment-transfer-batch
> Retrieves a transfer batch with recipient rows and chunk transfer summaries.
`GET /v1/payments/transfer-batches/{batchId}`
Requires an [API key](/docs/developing-with-sdp/authentication) with the `transfer_batches.read` action.
## Path parameters
- **`batchId`** · `string` · required · min length 1 · example `xbatch_example`
Transfer batch identifier.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Transfer batch details
- **`data`** · `object` · required
Transfer batch response payload.
- **`batch`** · `object` · required
Transfer batch details.
- **`id`** · `string` · required · example `xbatch_example`
Transfer batch identifier.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier for the transfer batch.
- **`externalId`** · `string | null` · required · example `payroll_2026_06_30`
Caller-provided batch correlation ID.
- **`sourceCustodyWalletId`** · `string | null` · required · example `cwlt_example`
Exact source SDP Wallet ID, or null for unresolved legacy batches.
- **`sourceProviderWalletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Source Provider wallet ID retained as evidence.
- **`sourceAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Source custody wallet address.
- **`token`** · `string` · required · example `SOL`
Token symbol or mint address.
- **`status`** · `string` · required · one of `pending`, `processing`, `confirmed`, `failed`, `partially_failed`, `archived` · example `processing`
Transfer batch status.
- **`totalAmount`** · `string | null` · required · example `1000.00`
Total requested batch amount in UI units, when computed.
- **`recipientCount`** · `integer` · required · example `20`
Number of recipient rows in the batch.
- **`transactionCount`** · `integer` · required · example `1`
Number of Solana transaction chunks for the batch.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`recipients`** · `object[]`
Recipient rows for the batch, included on detail responses.
- **`id`** · `string` · required
Transfer batch recipient identifier.
- **`batchId`** · `string` · required
Parent transfer batch identifier.
- **`transferId`** · `string | null` · required · min length 1 · example `xfr_example`
Payment transfer chunk that settled this recipient, once assigned.
- **`externalId`** · `string | null` · required · example `payroll_row_001`
Caller-provided recipient correlation ID.
- **`counterpartyId`** · `string` · required · example `cpty_example`
Counterparty identifier for the recipient.
- **`counterpartyAccountId`** · `string` · required · example `cpacct_example`
Counterparty account identifier for the recipient.
- **`destination`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Resolved Solana destination address.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`status`** · `string` · required · one of `pending`, `processing`, `confirmed`, `failed`, `archived` · example `pending`
Transfer batch recipient status.
- **`error`** · `string | null` · required
Recipient-level failure reason, when available. A recipient the source wallet could not fund reads "The wallet used for this payment does not have enough funds. Add funds and try again."
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`transfers`** · `object[]`
Payment transfer chunk records associated with this batch.
- **`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.
- **`401`** Unauthorized
- **`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.
- **`403`** Forbidden. Requires the `transfer_batches.read` 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 `401`.
- **`404`** Not Found
Same fields as `401`.
- **`500`** Internal Server Error
Same fields as `401`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/payments/transfer-batches/xbatch_example' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/transfer-batches/xbatch_example", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/payments/transfer-batches/xbatch_example",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"batch": {
"id": "xbatch_example",
"organizationId": "org_example",
"projectId": "prj_example",
"externalId": "payroll_2026_06_30",
"sourceCustodyWalletId": "cwlt_example",
"sourceProviderWalletId": "privy_wallet_123",
"sourceAddress": "So11111111111111111111111111111111111111112",
"token": "SOL",
"status": "processing",
"totalAmount": "1000.00",
"recipientCount": 20,
"transactionCount": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"recipients": [
{
"id": "_example",
"batchId": "batch_example",
"transferId": "xfr_example",
"externalId": "payroll_row_001",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"destination": "So11111111111111111111111111111111111111112",
"amount": "100.00",
"status": "pending",
"error": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
],
"transfers": [
{
"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: transfer_batches.read",
"details": {
"action": "transfer_batches.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
## Payments: Recurring payments
### Create recurring payment
Source: https://platform.solana.com/docs/reference/api/payments/create-payment-recurring-payment
> Creates an SDP-custody outbound recurring payment intent from a custody wallet to a counterparty crypto-wallet account.
`POST /v1/payments/recurring-payments`
This stores backend state only; activation and collection are added by follow-up endpoints.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.manage` 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
Creates an SDP-custody outbound recurring payment intent. Activation and collection are added by follow-up endpoints.
- **`sourceCustodyWalletId`** · `string` · required · min length 1 · example `cwlt_example`
Exact SDP Wallet ID (`id` from `GET /v1/wallets`) that will fund the recurring payment. Provider `walletId`, wallet addresses, and the legacy `sourceWalletId` selector are not accepted.
- **`counterpartyId`** · `string` · required · min length 1 · example `cpty_example`
Counterparty receiving the recurring payment.
- **`counterpartyAccountId`** · `string` · required · min length 1 · example `cpacct_example`
Counterparty crypto_wallet account. It must contain Solana wallet details.
- **`token`** · `string` · required · example `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`
Token mint address or well-known symbol. Must be a USD stablecoin (e.g. USDC) or a token issued in this project; native SOL is not supported.
- **`amount`** · `string` · required · example `25.00`
Recurring payment amount in UI units.
- **`periodHours`** · `integer` · required · greater than 0 · max 8760 · example `720`
Billing period length in hours.
- **`firstCollectionAt`** · `string` · format `date-time` · example `2099-01-01T00:00:00.000Z`
Optional first collection timestamp. Defaults to activation time when omitted.
- **`metadataUri`** · `string` · format `uri` · max length 128 · example `https://example.com/subscriptions/monthly-usdc.json`
Optional plan metadata URI.
## Responses
- **`201`** Recurring payment created
- **`data`** · `object` · required
Recurring payment response payload.
- **`recurringPayment`** · `object` · required
SDP-custody outbound recurring payment record.
- **`id`** · `string` · required · example `prp_example`
SDP recurring payment ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`sourceCustodyWalletId`** · `string | null` · required · example `cwlt_example`
Exact source SDP Wallet ID, or null for unresolved legacy records.
- **`sourceProviderWalletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Source Provider wallet ID retained as evidence.
- **`sourceAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Source wallet address.
- **`counterpartyId`** · `string` · required · example `cpty_example`
Counterparty ID.
- **`counterpartyAccountId`** · `string` · required · example `cpacct_example`
Counterparty account ID.
- **`destinationAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Counterparty wallet owner address.
- **`destinationTokenAccount`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Derived counterparty token account used for collection.
- **`token`** · `string` · required
SPL token mint address.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`periodHours`** · `integer` · required · greater than 0 · example `720`
- **`firstCollectionAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Requested first collection timestamp.
- **`nextCollectionDueAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Next due collection timestamp.
- **`planId`** · `string | null` · required
Linked SDP subscription plan ID.
- **`subscriptionId`** · `string | null` · required
Linked SDP subscription ID.
- **`planPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain plan PDA.
- **`planCreatedAt`** · `string | null` · required
On-chain plan createdAt value as an unsigned integer string.
- **`planCreationSignature`** · `string | null` · required
Solana signature for plan creation.
- **`subscriptionPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription PDA.
- **`subscriptionAuthorityAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription authority address.
- **`authorizationSignature`** · `string | null` · required
Solana signature for subscription authorization.
- **`status`** · `string` · required · one of `pending_activation`, `activating`, `active`, `updating`, `canceling`, `resuming`, `paused`, `canceled`, `expired` · example `active`
Recurring payment status.
- **`pausedReason`** · `string | null` · required · one of `custody_policy_refused`, `failed_attempts`, `destination_unavailable` · example `null`
Why SDP paused the recurring payment, set exactly while `status` is `paused`: `custody_policy_refused` when the custody provider's policy refused to sign a collection, `failed_attempts` when one due period failed 5 times, or `destination_unavailable` when its counterparty or account is archived or unavailable in the project. Resume requires an active recipient and clears the reason.
- **`metadataUri`** · `string | null` · required
Optional plan metadata URI.
- **`createdBy`** · `string | null` · required
Creator user ID or API key ID.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`tokenApproval`** · `object`
Present when the recurring payment is canceled, on the cancel response and on get: what became of the token approval it relied on.
- **`status`** · `string` · required · one of `revoked`, `kept`, `revoking` · example `kept`
`revoked` once the approval has been revoked since the cancel. `kept` while other live recurring payments on the same wallet and token, in this or another project, still use it; `sharedWith` lists the ones in this project. It is revoked once the last of them ends. `revoking` when nothing else uses it but the revoke has not confirmed yet, for example while a collection that was already processing settles; the background sweep finishes it.
- **`delegate`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
The approval's delegate: the Subscriptions program's authority for this wallet and token.
- **`sharedWith`** · `object[]` · required
The other live recurring payments in this project on the same wallet and token that keep the approval. Empty unless `status` is `kept`, and empty then when only recurring payments in other projects keep it.
- **`id`** · `string` · required · example `prp_example`
SDP recurring payment ID.
- **`status`** · `string` · required · one of `pending_activation`, `activating`, `active`, `updating`, `canceling`, `resuming`, `paused`, `canceled`, `expired` · example `active`
Recurring payment status.
- **`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 `recurring.manage` 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`.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST 'https://api.solana.com/v1/payments/recurring-payments' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"sourceCustodyWalletId": "cwlt_example",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "25.00",
"periodHours": 720,
"firstCollectionAt": "2099-01-01T00:00:00.000Z",
"metadataUri": "https://example.com/subscriptions/monthly-usdc.json"
}'
```
```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/recurring-payments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
"sourceCustodyWalletId": "cwlt_example",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "25.00",
"periodHours": 720,
"firstCollectionAt": "2099-01-01T00:00:00.000Z",
"metadataUri": "https://example.com/subscriptions/monthly-usdc.json"
})
});
```
```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/recurring-payments",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
json={
"sourceCustodyWalletId": "cwlt_example",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "25.00",
"periodHours": 720,
"firstCollectionAt": "2099-01-01T00:00:00.000Z",
"metadataUri": "https://example.com/subscriptions/monthly-usdc.json"
})
```
## Example responses
```json tab="201"
{
"data": {
"recurringPayment": {
"id": "prp_example",
"organizationId": "org_example",
"projectId": "prj_example",
"sourceCustodyWalletId": "cwlt_example",
"sourceProviderWalletId": "privy_wallet_123",
"sourceAddress": "So11111111111111111111111111111111111111112",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"destinationAddress": "So11111111111111111111111111111111111111112",
"destinationTokenAccount": "So11111111111111111111111111111111111111112",
"token": "example",
"amount": "100.00",
"periodHours": 720,
"firstCollectionAt": "2025-01-01T00:00:00.000Z",
"nextCollectionDueAt": "2025-01-01T00:00:00.000Z",
"planId": "plan_example",
"subscriptionId": "subscription_example",
"planPda": "So11111111111111111111111111111111111111112",
"planCreatedAt": "example",
"planCreationSignature": "example",
"subscriptionPda": "So11111111111111111111111111111111111111112",
"subscriptionAuthorityAddress": "So11111111111111111111111111111111111111112",
"authorizationSignature": "example",
"status": "active",
"pausedReason": null,
"metadataUri": "example",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"tokenApproval": {
"status": "kept",
"delegate": "So11111111111111111111111111111111111111112",
"sharedWith": [
{
"id": "prp_example",
"status": "active"
}
]
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.manage",
"details": {
"action": "recurring.manage"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### List recurring payments
Source: https://platform.solana.com/docs/reference/api/payments/list-payment-recurring-payments
> Lists SDP-custody outbound recurring payments for the authenticated organization or project scope.
`GET /v1/payments/recurring-payments`
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.read` action.
## Query parameters
- **`counterpartyId`** · `string` · min length 1 · example `cpty_example`
Filter recurring payments by counterparty.
- **`status`** · `string` · one of `pending_activation`, `activating`, `active`, `updating`, `canceling`, `resuming`, `paused`, `canceled`, `expired` · example `active`
Recurring payment status.
- **`page`** · `integer` · greater than 0 · default `1`
- **`pageSize`** · `integer` · min 1 · max 100 · default `20`
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Recurring payment list
- **`data`** · `object` · required
Recurring payment list response payload.
- **`recurringPayments`** · `object[]` · required
- **`id`** · `string` · required · example `prp_example`
SDP recurring payment ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`sourceCustodyWalletId`** · `string | null` · required · example `cwlt_example`
Exact source SDP Wallet ID, or null for unresolved legacy records.
- **`sourceProviderWalletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Source Provider wallet ID retained as evidence.
- **`sourceAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Source wallet address.
- **`counterpartyId`** · `string` · required · example `cpty_example`
Counterparty ID.
- **`counterpartyAccountId`** · `string` · required · example `cpacct_example`
Counterparty account ID.
- **`destinationAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Counterparty wallet owner address.
- **`destinationTokenAccount`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Derived counterparty token account used for collection.
- **`token`** · `string` · required
SPL token mint address.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`periodHours`** · `integer` · required · greater than 0 · example `720`
- **`firstCollectionAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Requested first collection timestamp.
- **`nextCollectionDueAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Next due collection timestamp.
- **`planId`** · `string | null` · required
Linked SDP subscription plan ID.
- **`subscriptionId`** · `string | null` · required
Linked SDP subscription ID.
- **`planPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain plan PDA.
- **`planCreatedAt`** · `string | null` · required
On-chain plan createdAt value as an unsigned integer string.
- **`planCreationSignature`** · `string | null` · required
Solana signature for plan creation.
- **`subscriptionPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription PDA.
- **`subscriptionAuthorityAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription authority address.
- **`authorizationSignature`** · `string | null` · required
Solana signature for subscription authorization.
- **`status`** · `string` · required · one of `pending_activation`, `activating`, `active`, `updating`, `canceling`, `resuming`, `paused`, `canceled`, `expired` · example `active`
Recurring payment status.
- **`pausedReason`** · `string | null` · required · one of `custody_policy_refused`, `failed_attempts`, `destination_unavailable` · example `null`
Why SDP paused the recurring payment, set exactly while `status` is `paused`: `custody_policy_refused` when the custody provider's policy refused to sign a collection, `failed_attempts` when one due period failed 5 times, or `destination_unavailable` when its counterparty or account is archived or unavailable in the project. Resume requires an active recipient and clears the reason.
- **`metadataUri`** · `string | null` · required
Optional plan metadata URI.
- **`createdBy`** · `string | null` · required
Creator user ID or API key ID.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`total`** · `integer` · required
- **`page`** · `integer` · required
- **`pageSize`** · `integer` · required
- **`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 `recurring.read` 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`.
- **`500`** Internal Server Error
Same fields as `400`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/payments/recurring-payments' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/recurring-payments", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/payments/recurring-payments",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"recurringPayments": [
{
"id": "prp_example",
"organizationId": "org_example",
"projectId": "prj_example",
"sourceCustodyWalletId": "cwlt_example",
"sourceProviderWalletId": "privy_wallet_123",
"sourceAddress": "So11111111111111111111111111111111111111112",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"destinationAddress": "So11111111111111111111111111111111111111112",
"destinationTokenAccount": "So11111111111111111111111111111111111111112",
"token": "example",
"amount": "100.00",
"periodHours": 720,
"firstCollectionAt": "2025-01-01T00:00:00.000Z",
"nextCollectionDueAt": "2025-01-01T00:00:00.000Z",
"planId": "plan_example",
"subscriptionId": "subscription_example",
"planPda": "So11111111111111111111111111111111111111112",
"planCreatedAt": "example",
"planCreationSignature": "example",
"subscriptionPda": "So11111111111111111111111111111111111111112",
"subscriptionAuthorityAddress": "So11111111111111111111111111111111111111112",
"authorizationSignature": "example",
"status": "active",
"pausedReason": null,
"metadataUri": "example",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
],
"total": 1,
"page": 1,
"pageSize": 1
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.read",
"details": {
"action": "recurring.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Update recurring payment
Source: https://platform.solana.com/docs/reference/api/payments/update-payment-recurring-payment
> Updates an SDP-custody recurring payment.
`PATCH /v1/payments/recurring-payments/{id}`
Pending records are updated directly. Active metadata and due-date edits are applied in place, while active term, source, destination, or token edits create a replacement Solana subscription, authorize it, cancel the old subscription, and then swap the recurring payment to the replacement records.
Send `expectedUpdatedAt` from the version you reviewed to make the edit apply to that version only: when the recurring payment has changed since, the edit returns `409` `RECURRING_PAYMENT_CHANGED` and changes nothing.
Requires an `Idempotency-Key`, because an edit can sign and broadcast a replacement subscription. A same-key retry of a completed edit replays its answer. A retry of an interrupted edit gets `409` `IDEMPOTENCY_KEY_IN_FLIGHT` with `Retry-After` until the attempt is stale, then resumes from the last step SDP recorded. If a step was sent but not yet recorded when the edit was interrupted, which is rare, the retry can fail or send that step again, so read the recurring payment with `GET` before editing it again.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.execute` action.
## Path parameters
- **`id`** · `string` · required · min length 1 · example `prp_example`
SDP recurring payment record ID.
## 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
Updates a recurring payment. Pending payments are edited directly. Active metadata or due-date updates are applied in place, while active term/source/destination/token updates create and authorize a replacement subscription before canceling the old one.
- **`expectedUpdatedAt`** · `string` · min length 1 · max length 64
Optional `updatedAt` of the version you reviewed, from the last read of this recurring payment. When the recurring payment has changed since, the request returns `409` `RECURRING_PAYMENT_CHANGED` with the current `updatedAt` in `error.details` and changes nothing: read it again and review the current terms.
- **`sourceCustodyWalletId`** · `string` · min length 1 · example `cwlt_example`
Optional exact replacement SDP Wallet ID (`id` from `GET /v1/wallets`). Provider `walletId`, wallet addresses, and the legacy `sourceWalletId` selector are not accepted. Active replacements require write access to both the old and new source wallets.
- **`counterpartyId`** · `string` · min length 1 · example `cpty_example`
Optional replacement counterparty. When provided, counterpartyAccountId is also required.
- **`counterpartyAccountId`** · `string` · min length 1 · example `cpacct_example`
Optional replacement counterparty account. Without counterpartyId, this changes the account for the current counterparty.
- **`token`** · `string` · example `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`
Optional replacement token. Must be a USD stablecoin (e.g. USDC) or a token issued in this project; native SOL is not supported.
- **`amount`** · `string` · example `25.00`
Optional replacement amount in UI units.
- **`periodHours`** · `integer` · greater than 0 · max 8760 · example `720`
Optional replacement billing period length in hours. Active term changes create a replacement on-chain subscription.
- **`firstCollectionAt`** · `string | null` · format `date-time` · example `2099-01-01T00:00:00.000Z`
Pending-only first collection timestamp. Use null to clear the pending first collection override.
- **`nextCollectionDueAt`** · `string | null` · format `date-time` · example `2099-02-01T00:00:00.000Z`
Active-only next due timestamp. Use null to reset to the earliest eligible collection time.
- **`metadataUri`** · `string | null` · format `uri` · max length 128 · example `https://example.com/subscriptions/monthly-usdc.json`
Optional plan metadata URI. Use null to clear it. Active metadata-only edits update the existing on-chain plan in place.
## Responses
- **`200`** Recurring payment updated
- **`data`** · `object` · required
Recurring payment response payload.
- **`recurringPayment`** · `object` · required
SDP-custody outbound recurring payment record.
- **`id`** · `string` · required · example `prp_example`
SDP recurring payment ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`sourceCustodyWalletId`** · `string | null` · required · example `cwlt_example`
Exact source SDP Wallet ID, or null for unresolved legacy records.
- **`sourceProviderWalletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Source Provider wallet ID retained as evidence.
- **`sourceAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Source wallet address.
- **`counterpartyId`** · `string` · required · example `cpty_example`
Counterparty ID.
- **`counterpartyAccountId`** · `string` · required · example `cpacct_example`
Counterparty account ID.
- **`destinationAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Counterparty wallet owner address.
- **`destinationTokenAccount`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Derived counterparty token account used for collection.
- **`token`** · `string` · required
SPL token mint address.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`periodHours`** · `integer` · required · greater than 0 · example `720`
- **`firstCollectionAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Requested first collection timestamp.
- **`nextCollectionDueAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Next due collection timestamp.
- **`planId`** · `string | null` · required
Linked SDP subscription plan ID.
- **`subscriptionId`** · `string | null` · required
Linked SDP subscription ID.
- **`planPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain plan PDA.
- **`planCreatedAt`** · `string | null` · required
On-chain plan createdAt value as an unsigned integer string.
- **`planCreationSignature`** · `string | null` · required
Solana signature for plan creation.
- **`subscriptionPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription PDA.
- **`subscriptionAuthorityAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription authority address.
- **`authorizationSignature`** · `string | null` · required
Solana signature for subscription authorization.
- **`status`** · `string` · required · one of `pending_activation`, `activating`, `active`, `updating`, `canceling`, `resuming`, `paused`, `canceled`, `expired` · example `active`
Recurring payment status.
- **`pausedReason`** · `string | null` · required · one of `custody_policy_refused`, `failed_attempts`, `destination_unavailable` · example `null`
Why SDP paused the recurring payment, set exactly while `status` is `paused`: `custody_policy_refused` when the custody provider's policy refused to sign a collection, `failed_attempts` when one due period failed 5 times, or `destination_unavailable` when its counterparty or account is archived or unavailable in the project. Resume requires an active recipient and clears the reason.
- **`metadataUri`** · `string | null` · required
Optional plan metadata URI.
- **`createdBy`** · `string | null` · required
Creator user ID or API key ID.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`tokenApproval`** · `object`
Present when the recurring payment is canceled, on the cancel response and on get: what became of the token approval it relied on.
- **`status`** · `string` · required · one of `revoked`, `kept`, `revoking` · example `kept`
`revoked` once the approval has been revoked since the cancel. `kept` while other live recurring payments on the same wallet and token, in this or another project, still use it; `sharedWith` lists the ones in this project. It is revoked once the last of them ends. `revoking` when nothing else uses it but the revoke has not confirmed yet, for example while a collection that was already processing settles; the background sweep finishes it.
- **`delegate`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
The approval's delegate: the Subscriptions program's authority for this wallet and token.
- **`sharedWith`** · `object[]` · required
The other live recurring payments in this project on the same wallet and token that keep the approval. Empty unless `status` is `kept`, and empty then when only recurring payments in other projects keep it.
- **`id`** · `string` · required · example `prp_example`
SDP recurring payment ID.
- **`status`** · `string` · required · one of `pending_activation`, `activating`, `active`, `updating`, `canceling`, `resuming`, `paused`, `canceled`, `expired` · example `active`
Recurring payment status.
- **`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 `recurring.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 PATCH 'https://api.solana.com/v1/payments/recurring-payments/prp_example' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"sourceCustodyWalletId": "cwlt_example",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "25.00",
"periodHours": 720,
"firstCollectionAt": "2099-01-01T00:00:00.000Z",
"nextCollectionDueAt": "2099-02-01T00:00:00.000Z",
"metadataUri": "https://example.com/subscriptions/monthly-usdc.json"
}'
```
```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/recurring-payments/prp_example", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
"sourceCustodyWalletId": "cwlt_example",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "25.00",
"periodHours": 720,
"firstCollectionAt": "2099-01-01T00:00:00.000Z",
"nextCollectionDueAt": "2099-02-01T00:00:00.000Z",
"metadataUri": "https://example.com/subscriptions/monthly-usdc.json"
})
});
```
```python tab="Python"
import os, requests, uuid
# Reuse this key when you retry this request.
idempotency_key = str(uuid.uuid4())
r = requests.patch("https://api.solana.com/v1/payments/recurring-payments/prp_example",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
json={
"sourceCustodyWalletId": "cwlt_example",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "25.00",
"periodHours": 720,
"firstCollectionAt": "2099-01-01T00:00:00.000Z",
"nextCollectionDueAt": "2099-02-01T00:00:00.000Z",
"metadataUri": "https://example.com/subscriptions/monthly-usdc.json"
})
```
## Example responses
```json tab="200"
{
"data": {
"recurringPayment": {
"id": "prp_example",
"organizationId": "org_example",
"projectId": "prj_example",
"sourceCustodyWalletId": "cwlt_example",
"sourceProviderWalletId": "privy_wallet_123",
"sourceAddress": "So11111111111111111111111111111111111111112",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"destinationAddress": "So11111111111111111111111111111111111111112",
"destinationTokenAccount": "So11111111111111111111111111111111111111112",
"token": "example",
"amount": "100.00",
"periodHours": 720,
"firstCollectionAt": "2025-01-01T00:00:00.000Z",
"nextCollectionDueAt": "2025-01-01T00:00:00.000Z",
"planId": "plan_example",
"subscriptionId": "subscription_example",
"planPda": "So11111111111111111111111111111111111111112",
"planCreatedAt": "example",
"planCreationSignature": "example",
"subscriptionPda": "So11111111111111111111111111111111111111112",
"subscriptionAuthorityAddress": "So11111111111111111111111111111111111111112",
"authorizationSignature": "example",
"status": "active",
"pausedReason": null,
"metadataUri": "example",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"tokenApproval": {
"status": "kept",
"delegate": "So11111111111111111111111111111111111111112",
"sharedWith": [
{
"id": "prp_example",
"status": "active"
}
]
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.execute",
"details": {
"action": "recurring.execute"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Get recurring payment
Source: https://platform.solana.com/docs/reference/api/payments/get-payment-recurring-payment
> Retrieves an SDP-custody outbound recurring payment record.
`GET /v1/payments/recurring-payments/{id}`
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.read` action.
## Path parameters
- **`id`** · `string` · required · min length 1 · example `prp_example`
SDP recurring payment record ID.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Recurring payment
- **`data`** · `object` · required
Recurring payment response payload.
- **`recurringPayment`** · `object` · required
SDP-custody outbound recurring payment record.
- **`id`** · `string` · required · example `prp_example`
SDP recurring payment ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`sourceCustodyWalletId`** · `string | null` · required · example `cwlt_example`
Exact source SDP Wallet ID, or null for unresolved legacy records.
- **`sourceProviderWalletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Source Provider wallet ID retained as evidence.
- **`sourceAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Source wallet address.
- **`counterpartyId`** · `string` · required · example `cpty_example`
Counterparty ID.
- **`counterpartyAccountId`** · `string` · required · example `cpacct_example`
Counterparty account ID.
- **`destinationAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Counterparty wallet owner address.
- **`destinationTokenAccount`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Derived counterparty token account used for collection.
- **`token`** · `string` · required
SPL token mint address.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`periodHours`** · `integer` · required · greater than 0 · example `720`
- **`firstCollectionAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Requested first collection timestamp.
- **`nextCollectionDueAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Next due collection timestamp.
- **`planId`** · `string | null` · required
Linked SDP subscription plan ID.
- **`subscriptionId`** · `string | null` · required
Linked SDP subscription ID.
- **`planPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain plan PDA.
- **`planCreatedAt`** · `string | null` · required
On-chain plan createdAt value as an unsigned integer string.
- **`planCreationSignature`** · `string | null` · required
Solana signature for plan creation.
- **`subscriptionPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription PDA.
- **`subscriptionAuthorityAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription authority address.
- **`authorizationSignature`** · `string | null` · required
Solana signature for subscription authorization.
- **`status`** · `string` · required · one of `pending_activation`, `activating`, `active`, `updating`, `canceling`, `resuming`, `paused`, `canceled`, `expired` · example `active`
Recurring payment status.
- **`pausedReason`** · `string | null` · required · one of `custody_policy_refused`, `failed_attempts`, `destination_unavailable` · example `null`
Why SDP paused the recurring payment, set exactly while `status` is `paused`: `custody_policy_refused` when the custody provider's policy refused to sign a collection, `failed_attempts` when one due period failed 5 times, or `destination_unavailable` when its counterparty or account is archived or unavailable in the project. Resume requires an active recipient and clears the reason.
- **`metadataUri`** · `string | null` · required
Optional plan metadata URI.
- **`createdBy`** · `string | null` · required
Creator user ID or API key ID.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`tokenApproval`** · `object`
Present when the recurring payment is canceled, on the cancel response and on get: what became of the token approval it relied on.
- **`status`** · `string` · required · one of `revoked`, `kept`, `revoking` · example `kept`
`revoked` once the approval has been revoked since the cancel. `kept` while other live recurring payments on the same wallet and token, in this or another project, still use it; `sharedWith` lists the ones in this project. It is revoked once the last of them ends. `revoking` when nothing else uses it but the revoke has not confirmed yet, for example while a collection that was already processing settles; the background sweep finishes it.
- **`delegate`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
The approval's delegate: the Subscriptions program's authority for this wallet and token.
- **`sharedWith`** · `object[]` · required
The other live recurring payments in this project on the same wallet and token that keep the approval. Empty unless `status` is `kept`, and empty then when only recurring payments in other projects keep it.
- **`id`** · `string` · required · example `prp_example`
SDP recurring payment ID.
- **`status`** · `string` · required · one of `pending_activation`, `activating`, `active`, `updating`, `canceling`, `resuming`, `paused`, `canceled`, `expired` · example `active`
Recurring payment status.
- **`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.
- **`401`** Unauthorized
- **`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.
- **`403`** Forbidden. Requires the `recurring.read` 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 `401`.
- **`404`** Not Found
Same fields as `401`.
- **`500`** Internal Server Error
Same fields as `401`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/payments/recurring-payments/prp_example' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/recurring-payments/prp_example", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/payments/recurring-payments/prp_example",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"recurringPayment": {
"id": "prp_example",
"organizationId": "org_example",
"projectId": "prj_example",
"sourceCustodyWalletId": "cwlt_example",
"sourceProviderWalletId": "privy_wallet_123",
"sourceAddress": "So11111111111111111111111111111111111111112",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"destinationAddress": "So11111111111111111111111111111111111111112",
"destinationTokenAccount": "So11111111111111111111111111111111111111112",
"token": "example",
"amount": "100.00",
"periodHours": 720,
"firstCollectionAt": "2025-01-01T00:00:00.000Z",
"nextCollectionDueAt": "2025-01-01T00:00:00.000Z",
"planId": "plan_example",
"subscriptionId": "subscription_example",
"planPda": "So11111111111111111111111111111111111111112",
"planCreatedAt": "example",
"planCreationSignature": "example",
"subscriptionPda": "So11111111111111111111111111111111111111112",
"subscriptionAuthorityAddress": "So11111111111111111111111111111111111111112",
"authorizationSignature": "example",
"status": "active",
"pausedReason": null,
"metadataUri": "example",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"tokenApproval": {
"status": "kept",
"delegate": "So11111111111111111111111111111111111111112",
"sharedWith": [
{
"id": "prp_example",
"status": "active"
}
]
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.read",
"details": {
"action": "recurring.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Activate recurring payment
Source: https://platform.solana.com/docs/reference/api/payments/activate-payment-recurring-payment
> Activates a pending SDP-custody recurring payment by creating the Solana subscriptions plan, authorizing the subscription, and storing the resulting on-chain identifiers and signatures.
`POST /v1/payments/recurring-payments/{id}/activate`
A fresh activation signs the terms it read: when an edit lands while it runs, or `expectedUpdatedAt` names an earlier version, it returns `409` `RECURRING_PAYMENT_CHANGED` before claiming the schedule or signing.
Both the counterparty and its account must be active before a fresh activation; an unavailable recipient returns `404` before claiming the schedule or signing. Recovery can confirm saved transactions, but cannot sign a new step for an unavailable recipient.
The source wallet gives the Subscriptions program's authority for that token an approval of 0, and each collection approves exactly its own amount in the same transaction as the pull, so the token program's `Approve` instruction appears in activation and collection transactions. If your custody provider's wallet policy allowlists programs or instructions, allow `Approve` to that authority, or the provider refuses the collection and SDP pauses the recurring payment with `pausedReason` `custody_policy_refused`.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.execute` action.
## Path parameters
- **`id`** · `string` · required · min length 1 · example `prp_example`
SDP recurring payment record ID.
## 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`
Optional precondition for activating a recurring payment.
- **`expectedUpdatedAt`** · `string` · min length 1 · max length 64
Optional `updatedAt` of the version you reviewed, from the last read of this recurring payment. When the recurring payment has changed since, the request returns `409` `RECURRING_PAYMENT_CHANGED` with the current `updatedAt` in `error.details` and changes nothing: read it again and review the current terms.
## Responses
- **`200`** Recurring payment activated
- **`data`** · `object` · required
Recurring payment response payload.
- **`recurringPayment`** · `object` · required
SDP-custody outbound recurring payment record.
- **`id`** · `string` · required · example `prp_example`
SDP recurring payment ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`sourceCustodyWalletId`** · `string | null` · required · example `cwlt_example`
Exact source SDP Wallet ID, or null for unresolved legacy records.
- **`sourceProviderWalletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Source Provider wallet ID retained as evidence.
- **`sourceAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Source wallet address.
- **`counterpartyId`** · `string` · required · example `cpty_example`
Counterparty ID.
- **`counterpartyAccountId`** · `string` · required · example `cpacct_example`
Counterparty account ID.
- **`destinationAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Counterparty wallet owner address.
- **`destinationTokenAccount`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Derived counterparty token account used for collection.
- **`token`** · `string` · required
SPL token mint address.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`periodHours`** · `integer` · required · greater than 0 · example `720`
- **`firstCollectionAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Requested first collection timestamp.
- **`nextCollectionDueAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Next due collection timestamp.
- **`planId`** · `string | null` · required
Linked SDP subscription plan ID.
- **`subscriptionId`** · `string | null` · required
Linked SDP subscription ID.
- **`planPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain plan PDA.
- **`planCreatedAt`** · `string | null` · required
On-chain plan createdAt value as an unsigned integer string.
- **`planCreationSignature`** · `string | null` · required
Solana signature for plan creation.
- **`subscriptionPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription PDA.
- **`subscriptionAuthorityAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription authority address.
- **`authorizationSignature`** · `string | null` · required
Solana signature for subscription authorization.
- **`status`** · `string` · required · one of `pending_activation`, `activating`, `active`, `updating`, `canceling`, `resuming`, `paused`, `canceled`, `expired` · example `active`
Recurring payment status.
- **`pausedReason`** · `string | null` · required · one of `custody_policy_refused`, `failed_attempts`, `destination_unavailable` · example `null`
Why SDP paused the recurring payment, set exactly while `status` is `paused`: `custody_policy_refused` when the custody provider's policy refused to sign a collection, `failed_attempts` when one due period failed 5 times, or `destination_unavailable` when its counterparty or account is archived or unavailable in the project. Resume requires an active recipient and clears the reason.
- **`metadataUri`** · `string | null` · required
Optional plan metadata URI.
- **`createdBy`** · `string | null` · required
Creator user ID or API key ID.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`tokenApproval`** · `object`
Present when the recurring payment is canceled, on the cancel response and on get: what became of the token approval it relied on.
- **`status`** · `string` · required · one of `revoked`, `kept`, `revoking` · example `kept`
`revoked` once the approval has been revoked since the cancel. `kept` while other live recurring payments on the same wallet and token, in this or another project, still use it; `sharedWith` lists the ones in this project. It is revoked once the last of them ends. `revoking` when nothing else uses it but the revoke has not confirmed yet, for example while a collection that was already processing settles; the background sweep finishes it.
- **`delegate`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
The approval's delegate: the Subscriptions program's authority for this wallet and token.
- **`sharedWith`** · `object[]` · required
The other live recurring payments in this project on the same wallet and token that keep the approval. Empty unless `status` is `kept`, and empty then when only recurring payments in other projects keep it.
- **`id`** · `string` · required · example `prp_example`
SDP recurring payment ID.
- **`status`** · `string` · required · one of `pending_activation`, `activating`, `active`, `updating`, `canceling`, `resuming`, `paused`, `canceled`, `expired` · example `active`
Recurring payment status.
- **`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 `recurring.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/recurring-payments/prp_example/activate' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{}'
```
```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/recurring-payments/prp_example/activate", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json"
},
body: JSON.stringify({})
});
```
```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/recurring-payments/prp_example/activate",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
json={})
```
## Example responses
```json tab="200"
{
"data": {
"recurringPayment": {
"id": "prp_example",
"organizationId": "org_example",
"projectId": "prj_example",
"sourceCustodyWalletId": "cwlt_example",
"sourceProviderWalletId": "privy_wallet_123",
"sourceAddress": "So11111111111111111111111111111111111111112",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"destinationAddress": "So11111111111111111111111111111111111111112",
"destinationTokenAccount": "So11111111111111111111111111111111111111112",
"token": "example",
"amount": "100.00",
"periodHours": 720,
"firstCollectionAt": "2025-01-01T00:00:00.000Z",
"nextCollectionDueAt": "2025-01-01T00:00:00.000Z",
"planId": "plan_example",
"subscriptionId": "subscription_example",
"planPda": "So11111111111111111111111111111111111111112",
"planCreatedAt": "example",
"planCreationSignature": "example",
"subscriptionPda": "So11111111111111111111111111111111111111112",
"subscriptionAuthorityAddress": "So11111111111111111111111111111111111111112",
"authorizationSignature": "example",
"status": "active",
"pausedReason": null,
"metadataUri": "example",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"tokenApproval": {
"status": "kept",
"delegate": "So11111111111111111111111111111111111111112",
"sharedWith": [
{
"id": "prp_example",
"status": "active"
}
]
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.execute",
"details": {
"action": "recurring.execute"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Cancel recurring payment
Source: https://platform.solana.com/docs/reference/api/payments/cancel-payment-recurring-payment
> Permanently stops an active or paused SDP-custody recurring payment by submitting the Solana subscriptions immediate cancellation, which ends its on-chain subscription at once rather than at the end of the billing period.
`POST /v1/payments/recurring-payments/{id}/cancel`
A canceled recurring payment never collects or resumes again.
Every recurring payment from one wallet in one token relies on one token approval. When no other live recurring payment uses it, it is revoked right after the cancel; otherwise it stays for the schedules that still use it. The response's `tokenApproval` says which: `revoked`, `kept` with the recurring payments still using it in `sharedWith`, or `revoking` while a collection that was already processing settles, after which the background sweep revokes it. `GET` returns the same `tokenApproval` for a canceled recurring payment.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.manage` action.
## Path parameters
- **`id`** · `string` · required · min length 1 · example `prp_example`
SDP recurring payment record ID.
## 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).
## Responses
- **`200`** Recurring payment canceled
- **`data`** · `object` · required
Recurring payment response payload.
- **`recurringPayment`** · `object` · required
SDP-custody outbound recurring payment record.
- **`id`** · `string` · required · example `prp_example`
SDP recurring payment ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`sourceCustodyWalletId`** · `string | null` · required · example `cwlt_example`
Exact source SDP Wallet ID, or null for unresolved legacy records.
- **`sourceProviderWalletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Source Provider wallet ID retained as evidence.
- **`sourceAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Source wallet address.
- **`counterpartyId`** · `string` · required · example `cpty_example`
Counterparty ID.
- **`counterpartyAccountId`** · `string` · required · example `cpacct_example`
Counterparty account ID.
- **`destinationAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Counterparty wallet owner address.
- **`destinationTokenAccount`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Derived counterparty token account used for collection.
- **`token`** · `string` · required
SPL token mint address.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`periodHours`** · `integer` · required · greater than 0 · example `720`
- **`firstCollectionAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Requested first collection timestamp.
- **`nextCollectionDueAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Next due collection timestamp.
- **`planId`** · `string | null` · required
Linked SDP subscription plan ID.
- **`subscriptionId`** · `string | null` · required
Linked SDP subscription ID.
- **`planPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain plan PDA.
- **`planCreatedAt`** · `string | null` · required
On-chain plan createdAt value as an unsigned integer string.
- **`planCreationSignature`** · `string | null` · required
Solana signature for plan creation.
- **`subscriptionPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription PDA.
- **`subscriptionAuthorityAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription authority address.
- **`authorizationSignature`** · `string | null` · required
Solana signature for subscription authorization.
- **`status`** · `string` · required · one of `pending_activation`, `activating`, `active`, `updating`, `canceling`, `resuming`, `paused`, `canceled`, `expired` · example `active`
Recurring payment status.
- **`pausedReason`** · `string | null` · required · one of `custody_policy_refused`, `failed_attempts`, `destination_unavailable` · example `null`
Why SDP paused the recurring payment, set exactly while `status` is `paused`: `custody_policy_refused` when the custody provider's policy refused to sign a collection, `failed_attempts` when one due period failed 5 times, or `destination_unavailable` when its counterparty or account is archived or unavailable in the project. Resume requires an active recipient and clears the reason.
- **`metadataUri`** · `string | null` · required
Optional plan metadata URI.
- **`createdBy`** · `string | null` · required
Creator user ID or API key ID.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`tokenApproval`** · `object`
Present when the recurring payment is canceled, on the cancel response and on get: what became of the token approval it relied on.
- **`status`** · `string` · required · one of `revoked`, `kept`, `revoking` · example `kept`
`revoked` once the approval has been revoked since the cancel. `kept` while other live recurring payments on the same wallet and token, in this or another project, still use it; `sharedWith` lists the ones in this project. It is revoked once the last of them ends. `revoking` when nothing else uses it but the revoke has not confirmed yet, for example while a collection that was already processing settles; the background sweep finishes it.
- **`delegate`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
The approval's delegate: the Subscriptions program's authority for this wallet and token.
- **`sharedWith`** · `object[]` · required
The other live recurring payments in this project on the same wallet and token that keep the approval. Empty unless `status` is `kept`, and empty then when only recurring payments in other projects keep it.
- **`id`** · `string` · required · example `prp_example`
SDP recurring payment ID.
- **`status`** · `string` · required · one of `pending_activation`, `activating`, `active`, `updating`, `canceling`, `resuming`, `paused`, `canceled`, `expired` · example `active`
Recurring payment status.
- **`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 `recurring.manage` 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/recurring-payments/prp_example/cancel' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY"
```
```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/recurring-payments/prp_example/cancel", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey
}
});
```
```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/recurring-payments/prp_example/cancel",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key})
```
## Example responses
```json tab="200"
{
"data": {
"recurringPayment": {
"id": "prp_example",
"organizationId": "org_example",
"projectId": "prj_example",
"sourceCustodyWalletId": "cwlt_example",
"sourceProviderWalletId": "privy_wallet_123",
"sourceAddress": "So11111111111111111111111111111111111111112",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"destinationAddress": "So11111111111111111111111111111111111111112",
"destinationTokenAccount": "So11111111111111111111111111111111111111112",
"token": "example",
"amount": "100.00",
"periodHours": 720,
"firstCollectionAt": "2025-01-01T00:00:00.000Z",
"nextCollectionDueAt": "2025-01-01T00:00:00.000Z",
"planId": "plan_example",
"subscriptionId": "subscription_example",
"planPda": "So11111111111111111111111111111111111111112",
"planCreatedAt": "example",
"planCreationSignature": "example",
"subscriptionPda": "So11111111111111111111111111111111111111112",
"subscriptionAuthorityAddress": "So11111111111111111111111111111111111111112",
"authorizationSignature": "example",
"status": "active",
"pausedReason": null,
"metadataUri": "example",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"tokenApproval": {
"status": "kept",
"delegate": "So11111111111111111111111111111111111111112",
"sharedWith": [
{
"id": "prp_example",
"status": "active"
}
]
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.manage",
"details": {
"action": "recurring.manage"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Collect recurring payment
Source: https://platform.solana.com/docs/reference/api/payments/collect-payment-recurring-payment
> Manually starts a due active SDP-custody recurring payment collection, creating a linked payment transfer and collection attempt.
`POST /v1/payments/recurring-payments/{id}/collect`
When admitting a new collection, SDP requires an active counterparty and account in the schedule's project. Archival that commits before admission prevents a new collection from signing. If either recipient record is archived or unavailable, the schedule pauses with `pausedReason` `destination_unavailable` and the request returns `409` without signing. That `409`, and every later collect or resume while the recipient stays unavailable, tells the caller to cancel the schedule and create a new one. A collection admitted earlier may still finish or recover.
If submission cannot be confirmed immediately, a `200` response returns the same transfer as `processing` with its known signature; reconciliation settles it, and the next due time advances only after exact on-chain confirmation.
When the source wallet cannot cover the amount, the request returns `400` `TRANSACTION_FAILED`, and the failed collection attempt and its transfer record the same reason in `error`, so the collection history shows why.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.execute` action.
## Path parameters
- **`id`** · `string` · required · min length 1 · example `prp_example`
SDP recurring payment record ID.
## 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).
## Responses
- **`200`** Recurring payment collection result
- **`data`** · `object` · required
Recurring payment collection response payload.
- **`recurringPayment`** · `object` · required
SDP-custody outbound recurring payment record.
- **`id`** · `string` · required · example `prp_example`
SDP recurring payment ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`sourceCustodyWalletId`** · `string | null` · required · example `cwlt_example`
Exact source SDP Wallet ID, or null for unresolved legacy records.
- **`sourceProviderWalletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Source Provider wallet ID retained as evidence.
- **`sourceAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Source wallet address.
- **`counterpartyId`** · `string` · required · example `cpty_example`
Counterparty ID.
- **`counterpartyAccountId`** · `string` · required · example `cpacct_example`
Counterparty account ID.
- **`destinationAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Counterparty wallet owner address.
- **`destinationTokenAccount`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Derived counterparty token account used for collection.
- **`token`** · `string` · required
SPL token mint address.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`periodHours`** · `integer` · required · greater than 0 · example `720`
- **`firstCollectionAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Requested first collection timestamp.
- **`nextCollectionDueAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Next due collection timestamp.
- **`planId`** · `string | null` · required
Linked SDP subscription plan ID.
- **`subscriptionId`** · `string | null` · required
Linked SDP subscription ID.
- **`planPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain plan PDA.
- **`planCreatedAt`** · `string | null` · required
On-chain plan createdAt value as an unsigned integer string.
- **`planCreationSignature`** · `string | null` · required
Solana signature for plan creation.
- **`subscriptionPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription PDA.
- **`subscriptionAuthorityAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription authority address.
- **`authorizationSignature`** · `string | null` · required
Solana signature for subscription authorization.
- **`status`** · `string` · required · one of `pending_activation`, `activating`, `active`, `updating`, `canceling`, `resuming`, `paused`, `canceled`, `expired` · example `active`
Recurring payment status.
- **`pausedReason`** · `string | null` · required · one of `custody_policy_refused`, `failed_attempts`, `destination_unavailable` · example `null`
Why SDP paused the recurring payment, set exactly while `status` is `paused`: `custody_policy_refused` when the custody provider's policy refused to sign a collection, `failed_attempts` when one due period failed 5 times, or `destination_unavailable` when its counterparty or account is archived or unavailable in the project. Resume requires an active recipient and clears the reason.
- **`metadataUri`** · `string | null` · required
Optional plan metadata URI.
- **`createdBy`** · `string | null` · required
Creator user ID or API key ID.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`collectionAttempt`** · `object` · required
Recurring payment collection attempt record.
- **`id`** · `string` · required · example `psca_example`
Collection attempt ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`subscriptionId`** · `string` · required
Subscription ID.
- **`transferId`** · `string | null` · required · min length 1 · example `xfr_example`
Payment transfer record ID when linked.
- **`token`** · `string` · required
Token mint or normalized token identifier.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`dueAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`attemptedAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`status`** · `string` · required · one of `pending`, `processing`, `confirmed`, `failed`, `skipped` · example `pending`
Collection attempt status.
- **`signature`** · `string | null` · required
Solana transaction signature.
- **`error`** · `string | null` · required · example `The wallet used for this payment does not have enough funds. Add funds and try again.`
Why the collection failed, if it did. A source wallet that cannot cover the amount records the same text the collect request returned with `400 TRANSACTION_FAILED`.
- **`metadata`** · `object` · required
Provider/job metadata for the attempt.
One of:
- **source: "manual"**
- **`recurringPaymentId`** · `string` · required
- **`initiatedByKeyId`** · `string | null` · required
- **`source`** · `string` · required · always `manual`
- **source: "automated"**
- **`recurringPaymentId`** · `string` · required
- **`initiatedByKeyId`** · `string | null` · required
- **`source`** · `string` · required · always `automated`
- **source: "retry"**
- **`recurringPaymentId`** · `string` · required
- **`initiatedByKeyId`** · `string | null` · required
- **`source`** · `string` · required · always `retry`
- **`initialSource`** · `string` · required · one of `manual`, `automated`
- **`transferId`** · `string | null` · required
- **`error`** · `string` · required
- **`retryAfterAt`** · `string` · required
- **source: "linked_transfer"**
- **`recurringPaymentId`** · `string` · required
- **`initiatedByKeyId`** · `string | null` · required
- **`source`** · `string` · required · always `linked_transfer`
- **`initialSource`** · `string` · required · one of `manual`, `automated`
- **`transferId`** · `string` · required
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`transfer`** · `object` · required
Transfer transaction record.
- **`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 `recurring.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/recurring-payments/prp_example/collect' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY"
```
```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/recurring-payments/prp_example/collect", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey
}
});
```
```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/recurring-payments/prp_example/collect",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key})
```
## Example responses
```json tab="200"
{
"data": {
"recurringPayment": {
"id": "prp_example",
"organizationId": "org_example",
"projectId": "prj_example",
"sourceCustodyWalletId": "cwlt_example",
"sourceProviderWalletId": "privy_wallet_123",
"sourceAddress": "So11111111111111111111111111111111111111112",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"destinationAddress": "So11111111111111111111111111111111111111112",
"destinationTokenAccount": "So11111111111111111111111111111111111111112",
"token": "example",
"amount": "100.00",
"periodHours": 720,
"firstCollectionAt": "2025-01-01T00:00:00.000Z",
"nextCollectionDueAt": "2025-01-01T00:00:00.000Z",
"planId": "plan_example",
"subscriptionId": "subscription_example",
"planPda": "So11111111111111111111111111111111111111112",
"planCreatedAt": "example",
"planCreationSignature": "example",
"subscriptionPda": "So11111111111111111111111111111111111111112",
"subscriptionAuthorityAddress": "So11111111111111111111111111111111111111112",
"authorizationSignature": "example",
"status": "active",
"pausedReason": null,
"metadataUri": "example",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"collectionAttempt": {
"id": "psca_example",
"organizationId": "org_example",
"projectId": "prj_example",
"subscriptionId": "subscription_example",
"transferId": "xfr_example",
"token": "example",
"amount": "100.00",
"dueAt": "2025-01-01T00:00:00.000Z",
"attemptedAt": "2025-01-01T00:00:00.000Z",
"status": "pending",
"signature": "example",
"error": "The wallet used for this payment does not have enough funds. Add funds and try again.",
"metadata": {
"recurringPaymentId": "recurringpayment_example",
"initiatedByKeyId": "initiatedbykey_example",
"source": "manual"
},
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"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: recurring.execute",
"details": {
"action": "recurring.execute"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Resume recurring payment
Source: https://platform.solana.com/docs/reference/api/payments/resume-payment-recurring-payment
> Resumes a paused SDP-custody recurring payment.
`POST /v1/payments/recurring-payments/{id}/resume`
SDP pauses a recurring payment when a due period fails 5 times, at once when the custody provider's policy refuses to sign a collection, or before a new collection when its counterparty or account is archived or unavailable.
Resume requires both recipient records to be active; while either is archived or unavailable it returns 409, telling the caller to cancel the schedule and create a new one. Its on-chain subscription is still in place, so nothing is signed: the recurring payment returns to active, the failure count starts again, and a due period that has already ended moves to the next one. A canceled recurring payment cannot be resumed.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.execute` action.
## Path parameters
- **`id`** · `string` · required · min length 1 · example `prp_example`
SDP recurring payment record ID.
## 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).
## Responses
- **`200`** Recurring payment resumed
- **`data`** · `object` · required
Recurring payment response payload.
- **`recurringPayment`** · `object` · required
SDP-custody outbound recurring payment record.
- **`id`** · `string` · required · example `prp_example`
SDP recurring payment ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`sourceCustodyWalletId`** · `string | null` · required · example `cwlt_example`
Exact source SDP Wallet ID, or null for unresolved legacy records.
- **`sourceProviderWalletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Source Provider wallet ID retained as evidence.
- **`sourceAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Source wallet address.
- **`counterpartyId`** · `string` · required · example `cpty_example`
Counterparty ID.
- **`counterpartyAccountId`** · `string` · required · example `cpacct_example`
Counterparty account ID.
- **`destinationAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Counterparty wallet owner address.
- **`destinationTokenAccount`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Derived counterparty token account used for collection.
- **`token`** · `string` · required
SPL token mint address.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`periodHours`** · `integer` · required · greater than 0 · example `720`
- **`firstCollectionAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Requested first collection timestamp.
- **`nextCollectionDueAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Next due collection timestamp.
- **`planId`** · `string | null` · required
Linked SDP subscription plan ID.
- **`subscriptionId`** · `string | null` · required
Linked SDP subscription ID.
- **`planPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain plan PDA.
- **`planCreatedAt`** · `string | null` · required
On-chain plan createdAt value as an unsigned integer string.
- **`planCreationSignature`** · `string | null` · required
Solana signature for plan creation.
- **`subscriptionPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription PDA.
- **`subscriptionAuthorityAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription authority address.
- **`authorizationSignature`** · `string | null` · required
Solana signature for subscription authorization.
- **`status`** · `string` · required · one of `pending_activation`, `activating`, `active`, `updating`, `canceling`, `resuming`, `paused`, `canceled`, `expired` · example `active`
Recurring payment status.
- **`pausedReason`** · `string | null` · required · one of `custody_policy_refused`, `failed_attempts`, `destination_unavailable` · example `null`
Why SDP paused the recurring payment, set exactly while `status` is `paused`: `custody_policy_refused` when the custody provider's policy refused to sign a collection, `failed_attempts` when one due period failed 5 times, or `destination_unavailable` when its counterparty or account is archived or unavailable in the project. Resume requires an active recipient and clears the reason.
- **`metadataUri`** · `string | null` · required
Optional plan metadata URI.
- **`createdBy`** · `string | null` · required
Creator user ID or API key ID.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`tokenApproval`** · `object`
Present when the recurring payment is canceled, on the cancel response and on get: what became of the token approval it relied on.
- **`status`** · `string` · required · one of `revoked`, `kept`, `revoking` · example `kept`
`revoked` once the approval has been revoked since the cancel. `kept` while other live recurring payments on the same wallet and token, in this or another project, still use it; `sharedWith` lists the ones in this project. It is revoked once the last of them ends. `revoking` when nothing else uses it but the revoke has not confirmed yet, for example while a collection that was already processing settles; the background sweep finishes it.
- **`delegate`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
The approval's delegate: the Subscriptions program's authority for this wallet and token.
- **`sharedWith`** · `object[]` · required
The other live recurring payments in this project on the same wallet and token that keep the approval. Empty unless `status` is `kept`, and empty then when only recurring payments in other projects keep it.
- **`id`** · `string` · required · example `prp_example`
SDP recurring payment ID.
- **`status`** · `string` · required · one of `pending_activation`, `activating`, `active`, `updating`, `canceling`, `resuming`, `paused`, `canceled`, `expired` · example `active`
Recurring payment status.
- **`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 `recurring.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/recurring-payments/prp_example/resume' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY"
```
```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/recurring-payments/prp_example/resume", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey
}
});
```
```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/recurring-payments/prp_example/resume",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key})
```
## Example responses
```json tab="200"
{
"data": {
"recurringPayment": {
"id": "prp_example",
"organizationId": "org_example",
"projectId": "prj_example",
"sourceCustodyWalletId": "cwlt_example",
"sourceProviderWalletId": "privy_wallet_123",
"sourceAddress": "So11111111111111111111111111111111111111112",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"destinationAddress": "So11111111111111111111111111111111111111112",
"destinationTokenAccount": "So11111111111111111111111111111111111111112",
"token": "example",
"amount": "100.00",
"periodHours": 720,
"firstCollectionAt": "2025-01-01T00:00:00.000Z",
"nextCollectionDueAt": "2025-01-01T00:00:00.000Z",
"planId": "plan_example",
"subscriptionId": "subscription_example",
"planPda": "So11111111111111111111111111111111111111112",
"planCreatedAt": "example",
"planCreationSignature": "example",
"subscriptionPda": "So11111111111111111111111111111111111111112",
"subscriptionAuthorityAddress": "So11111111111111111111111111111111111111112",
"authorizationSignature": "example",
"status": "active",
"pausedReason": null,
"metadataUri": "example",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"tokenApproval": {
"status": "kept",
"delegate": "So11111111111111111111111111111111111111112",
"sharedWith": [
{
"id": "prp_example",
"status": "active"
}
]
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.execute",
"details": {
"action": "recurring.execute"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### List recurring payment collection attempts
Source: https://platform.solana.com/docs/reference/api/payments/list-payment-recurring-payment-collection-attempts
> Lists every collection attempt a recurring payment has made over its whole history, newest first: its current subscription and every subscription an edit replaced.
`GET /v1/payments/recurring-payments/{id}/collection-attempts`
A recurring payment that has not been activated has none.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.read` action.
## Path parameters
- **`id`** · `string` · required · min length 1 · example `prp_example`
SDP recurring payment record ID.
## Query parameters
- **`status`** · `string` · one of `pending`, `processing`, `confirmed`, `failed`, `skipped` · example `pending`
Collection attempt status.
- **`page`** · `integer` · greater than 0 · default `1`
- **`pageSize`** · `integer` · min 1 · max 100 · default `20`
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Collection attempt list
- **`data`** · `object` · required
Collection attempt list response payload.
- **`collectionAttempts`** · `object[]` · required
- **`id`** · `string` · required · example `psca_example`
Collection attempt ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`subscriptionId`** · `string` · required
Subscription ID.
- **`transferId`** · `string | null` · required · min length 1 · example `xfr_example`
Payment transfer record ID when linked.
- **`token`** · `string` · required
Token mint or normalized token identifier.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`dueAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`attemptedAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`status`** · `string` · required · one of `pending`, `processing`, `confirmed`, `failed`, `skipped` · example `pending`
Collection attempt status.
- **`signature`** · `string | null` · required
Solana transaction signature.
- **`error`** · `string | null` · required · example `The wallet used for this payment does not have enough funds. Add funds and try again.`
Why the collection failed, if it did. A source wallet that cannot cover the amount records the same text the collect request returned with `400 TRANSACTION_FAILED`.
- **`metadata`** · `object` · required
Provider/job metadata for the attempt.
One of:
- **source: "manual"**
- **`recurringPaymentId`** · `string` · required
- **`initiatedByKeyId`** · `string | null` · required
- **`source`** · `string` · required · always `manual`
- **source: "automated"**
- **`recurringPaymentId`** · `string` · required
- **`initiatedByKeyId`** · `string | null` · required
- **`source`** · `string` · required · always `automated`
- **source: "retry"**
- **`recurringPaymentId`** · `string` · required
- **`initiatedByKeyId`** · `string | null` · required
- **`source`** · `string` · required · always `retry`
- **`initialSource`** · `string` · required · one of `manual`, `automated`
- **`transferId`** · `string | null` · required
- **`error`** · `string` · required
- **`retryAfterAt`** · `string` · required
- **source: "linked_transfer"**
- **`recurringPaymentId`** · `string` · required
- **`initiatedByKeyId`** · `string | null` · required
- **`source`** · `string` · required · always `linked_transfer`
- **`initialSource`** · `string` · required · one of `manual`, `automated`
- **`transferId`** · `string` · required
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`total`** · `integer` · required
- **`page`** · `integer` · required
- **`pageSize`** · `integer` · required
- **`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 `recurring.read` 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`.
- **`500`** Internal Server Error
Same fields as `400`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/payments/recurring-payments/prp_example/collection-attempts' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/recurring-payments/prp_example/collection-attempts", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/payments/recurring-payments/prp_example/collection-attempts",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"collectionAttempts": [
{
"id": "psca_example",
"organizationId": "org_example",
"projectId": "prj_example",
"subscriptionId": "subscription_example",
"transferId": "xfr_example",
"token": "example",
"amount": "100.00",
"dueAt": "2025-01-01T00:00:00.000Z",
"attemptedAt": "2025-01-01T00:00:00.000Z",
"status": "pending",
"signature": "example",
"error": "The wallet used for this payment does not have enough funds. Add funds and try again.",
"metadata": {
"recurringPaymentId": "recurringpayment_example",
"initiatedByKeyId": "initiatedbykey_example",
"source": "manual"
},
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
],
"total": 1,
"page": 1,
"pageSize": 1
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.read",
"details": {
"action": "recurring.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
## Payments: Payment requests
### Create payment request
Source: https://platform.solana.com/docs/reference/api/payments/create-payment-request
> Creates a payment request: an amount of one token for a payer to send to a custody wallet in the caller's project, through the request's hosted payment link.
`POST /v1/payments/requests`
Requires `payment_requests.manage`. `walletId` is the provider wallet ID (`walletId` from `GET /v1/wallets`), not the SDP Wallet ID transfers take; an unknown wallet, or one outside the project, returns 404. `amount` is a decimal string with at most the mint's decimals, so `0.05` and `0.050` are the same request; more decimals return 400. A native SOL request is refused with 409 `AMOUNT_BELOW_RENT_MINIMUM` when the wallet has no account and the amount is below the rent-exempt minimum. The response is the only one besides `GET /v1/payments/requests/{paymentRequestId}/link` that carries `publicToken`. Accepts an Idempotency-Key: a retry with the same key and request returns the original payment request instead of creating a second one.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `payment_requests.manage` 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`
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 payment request payload.
- **`walletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Receiving custody wallet in the caller's project. Provider wallet ID: the `walletId` field returned by GET /v1/wallets. The wallet record `id` and the public key are not accepted. An SDP Wallet ID (`cwlt_...`) returns 404.
- **`token`** · `string` · required · example `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`
Mint address of the token to request. Symbols are not accepted; for native SOL, pass `So11111111111111111111111111111111111111112`.
- **`amount`** · `string` · required · example `10.00`
Amount to request in UI units, as a decimal string greater than zero with no more decimal places than the mint has.
- **`expiresAt`** · `string | null` · format `date-time` · default `null` · example `2026-12-01T00:00:00.000Z`
When the request stops accepting payment, as an ISO 8601 timestamp. Null or omitted: it never expires.
## Responses
- **`201`** Payment request created
- **`data`** · `object` · required
A new payment request, with its link token.
- **`id`** · `string` · required · example `preq_example`
SDP payment request ID.
- **`organizationId`** · `string` · required
Organization ID.
- **`projectId`** · `string | null` · required
Project ID.
- **`walletId`** · `string` · required
Receiving custody wallet's provider ID.
- **`destinationAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Address the payment is sent to.
- **`token`** · `string` · required
Token mint address.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`reference`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Solana Pay reference account that identifies the payment on chain.
- **`status`** · `string` · required · one of `awaiting_payment`, `paid`, `canceled`, `expired` · example `canceled`
Payment request status. `awaiting_payment` is open; `paid` was settled by an on-chain payment; `canceled` and `expired` closed without one.
- **`expiresAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Expiry, if any.
- **`fulfilledByTransferId`** · `string | null` · required
The inbound transfer that paid the request.
- **`paidLate`** · `boolean` · required
Whether the payment landed after the request expired or was canceled. A payer who signed before then can still pay.
- **`lastCheckedAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
When SDP last read the chain for the payment.
- **`canceledByApiKeyId`** · `string | null` · required
The API key that canceled the request; null when a member did, or it is not canceled.
- **`canceledByUserId`** · `string | null` · required
User ID of the member who canceled the request; null when an API key did, or it is not canceled.
- **`canceledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
When it was canceled.
- **`lifecycle`** · `object[]` · required
Every status the request entered, oldest first.
- **`status`** · `string` · required · one of `awaiting_payment`, `paid`, `canceled`, `expired` · example `canceled`
Payment request status. `awaiting_payment` is open; `paid` was settled by an on-chain payment; `canceled` and `expired` closed without one.
- **`at`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`createdBy`** · `string | null` · required
User ID of the member, or of the API key's creator, who created it.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`publicToken`** · `string` · required · example `example-public-token`
Token behind the request's hosted payment link, `/pay/{publicToken}`. Anyone holding it can build the request's fee-sponsored payment transaction, so share it only with the payer. Reads never return it; get it again with `GET /v1/payments/requests/{paymentRequestId}/link`.
- **`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 `payment_requests.manage` 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`.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST 'https://api.solana.com/v1/payments/requests' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"walletId": "privy_wallet_123",
"token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "10.00",
"expiresAt": null
}'
```
```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/requests", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
"walletId": "privy_wallet_123",
"token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "10.00",
"expiresAt": null
})
});
```
```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/requests",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
json={
"walletId": "privy_wallet_123",
"token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "10.00",
"expiresAt": None
})
```
## Example responses
```json tab="201"
{
"data": {
"id": "preq_example",
"organizationId": "organization_example",
"projectId": "project_example",
"walletId": "wallet_example",
"destinationAddress": "So11111111111111111111111111111111111111112",
"token": "example",
"amount": "100.00",
"reference": "So11111111111111111111111111111111111111112",
"status": "canceled",
"expiresAt": "2025-01-01T00:00:00.000Z",
"fulfilledByTransferId": "fulfilledbytransfer_example",
"paidLate": false,
"lastCheckedAt": "2025-01-01T00:00:00.000Z",
"canceledByApiKeyId": "canceledbyapikey_example",
"canceledByUserId": "canceledbyuser_example",
"canceledAt": "2025-01-01T00:00:00.000Z",
"lifecycle": [
{
"status": "canceled",
"at": "2025-01-01T00:00:00.000Z"
}
],
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z",
"publicToken": "example-public-token"
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: payment_requests.manage",
"details": {
"action": "payment_requests.manage"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### List payment requests
Source: https://platform.solana.com/docs/reference/api/payments/list-payment-requests
> Lists the caller's project's payment requests, newest first.
`GET /v1/payments/requests`
Requires `payment_requests.read`. An open request past its `expiresAt` reads as `expired`. Records never carry `publicToken`.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `payment_requests.read` action.
## Query parameters
- **`page`** · `integer` · greater than 0 · default `1` · example `1`
Page number, starting at 1.
- **`pageSize`** · `integer` · min 1 · max 100 · default `20` · example `20`
Results per page, 1 to 100.
- **`status`** · `string` · one of `awaiting_payment`, `paid`, `canceled`, `expired` · example `canceled`
Payment request status. `awaiting_payment` is open; `paid` was settled by an on-chain payment; `canceled` and `expired` closed without one.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Payment request list
- **`data`** · `object` · required
Payment request list response payload.
- **`paymentRequests`** · `object[]` · required
- **`id`** · `string` · required · example `preq_example`
SDP payment request ID.
- **`organizationId`** · `string` · required
Organization ID.
- **`projectId`** · `string | null` · required
Project ID.
- **`walletId`** · `string` · required
Receiving custody wallet's provider ID.
- **`destinationAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Address the payment is sent to.
- **`token`** · `string` · required
Token mint address.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`reference`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Solana Pay reference account that identifies the payment on chain.
- **`status`** · `string` · required · one of `awaiting_payment`, `paid`, `canceled`, `expired` · example `canceled`
Payment request status. `awaiting_payment` is open; `paid` was settled by an on-chain payment; `canceled` and `expired` closed without one.
- **`expiresAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Expiry, if any.
- **`fulfilledByTransferId`** · `string | null` · required
The inbound transfer that paid the request.
- **`paidLate`** · `boolean` · required
Whether the payment landed after the request expired or was canceled. A payer who signed before then can still pay.
- **`lastCheckedAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
When SDP last read the chain for the payment.
- **`canceledByApiKeyId`** · `string | null` · required
The API key that canceled the request; null when a member did, or it is not canceled.
- **`canceledByUserId`** · `string | null` · required
User ID of the member who canceled the request; null when an API key did, or it is not canceled.
- **`canceledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
When it was canceled.
- **`lifecycle`** · `object[]` · required
Every status the request entered, oldest first.
- **`status`** · `string` · required · one of `awaiting_payment`, `paid`, `canceled`, `expired` · example `canceled`
Payment request status. `awaiting_payment` is open; `paid` was settled by an on-chain payment; `canceled` and `expired` closed without one.
- **`at`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`createdBy`** · `string | null` · required
User ID of the member, or of the API key's creator, who created it.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`total`** · `integer` · required
- **`page`** · `integer` · required
- **`pageSize`** · `integer` · required
- **`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 `payment_requests.read` 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`.
- **`500`** Internal Server Error
Same fields as `400`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/payments/requests' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/requests", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/payments/requests",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"paymentRequests": [
{
"id": "preq_example",
"organizationId": "organization_example",
"projectId": "project_example",
"walletId": "wallet_example",
"destinationAddress": "So11111111111111111111111111111111111111112",
"token": "example",
"amount": "100.00",
"reference": "So11111111111111111111111111111111111111112",
"status": "canceled",
"expiresAt": "2025-01-01T00:00:00.000Z",
"fulfilledByTransferId": "fulfilledbytransfer_example",
"paidLate": false,
"lastCheckedAt": "2025-01-01T00:00:00.000Z",
"canceledByApiKeyId": "canceledbyapikey_example",
"canceledByUserId": "canceledbyuser_example",
"canceledAt": "2025-01-01T00:00:00.000Z",
"lifecycle": [
{
"status": "canceled",
"at": "2025-01-01T00:00:00.000Z"
}
],
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
],
"total": 1,
"page": 1,
"pageSize": 1
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: payment_requests.read",
"details": {
"action": "payment_requests.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Get payment request
Source: https://platform.solana.com/docs/reference/api/payments/get-payment-request
> Retrieves one payment request in the caller's project; another project's request returns 404.
`GET /v1/payments/requests/{paymentRequestId}`
Requires `payment_requests.read`. An open request past its `expiresAt` reads as `expired`. The record never carries `publicToken`; use `GET /v1/payments/requests/{paymentRequestId}/link`.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `payment_requests.read` action.
## Path parameters
- **`paymentRequestId`** · `string` · required · min length 1 · example `preq_example`
SDP payment request ID.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Payment request
- **`data`** · `object` · required
A payment request. Reads never carry its link token.
- **`id`** · `string` · required · example `preq_example`
SDP payment request ID.
- **`organizationId`** · `string` · required
Organization ID.
- **`projectId`** · `string | null` · required
Project ID.
- **`walletId`** · `string` · required
Receiving custody wallet's provider ID.
- **`destinationAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Address the payment is sent to.
- **`token`** · `string` · required
Token mint address.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`reference`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Solana Pay reference account that identifies the payment on chain.
- **`status`** · `string` · required · one of `awaiting_payment`, `paid`, `canceled`, `expired` · example `canceled`
Payment request status. `awaiting_payment` is open; `paid` was settled by an on-chain payment; `canceled` and `expired` closed without one.
- **`expiresAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Expiry, if any.
- **`fulfilledByTransferId`** · `string | null` · required
The inbound transfer that paid the request.
- **`paidLate`** · `boolean` · required
Whether the payment landed after the request expired or was canceled. A payer who signed before then can still pay.
- **`lastCheckedAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
When SDP last read the chain for the payment.
- **`canceledByApiKeyId`** · `string | null` · required
The API key that canceled the request; null when a member did, or it is not canceled.
- **`canceledByUserId`** · `string | null` · required
User ID of the member who canceled the request; null when an API key did, or it is not canceled.
- **`canceledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
When it was canceled.
- **`lifecycle`** · `object[]` · required
Every status the request entered, oldest first.
- **`status`** · `string` · required · one of `awaiting_payment`, `paid`, `canceled`, `expired` · example `canceled`
Payment request status. `awaiting_payment` is open; `paid` was settled by an on-chain payment; `canceled` and `expired` closed without one.
- **`at`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`createdBy`** · `string | null` · required
User ID of the member, or of the API key's creator, who created it.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 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 `payment_requests.read` 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`.
- **`500`** Internal Server Error
Same fields as `400`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/payments/requests/preq_example' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/requests/preq_example", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/payments/requests/preq_example",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"id": "preq_example",
"organizationId": "organization_example",
"projectId": "project_example",
"walletId": "wallet_example",
"destinationAddress": "So11111111111111111111111111111111111111112",
"token": "example",
"amount": "100.00",
"reference": "So11111111111111111111111111111111111111112",
"status": "canceled",
"expiresAt": "2025-01-01T00:00:00.000Z",
"fulfilledByTransferId": "fulfilledbytransfer_example",
"paidLate": false,
"lastCheckedAt": "2025-01-01T00:00:00.000Z",
"canceledByApiKeyId": "canceledbyapikey_example",
"canceledByUserId": "canceledbyuser_example",
"canceledAt": "2025-01-01T00:00:00.000Z",
"lifecycle": [
{
"status": "canceled",
"at": "2025-01-01T00:00:00.000Z"
}
],
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: payment_requests.read",
"details": {
"action": "payment_requests.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Get payment request link
Source: https://platform.solana.com/docs/reference/api/payments/get-payment-request-link
> Returns the token behind a payment request's hosted payment link, /pay/{publicToken}.
`GET /v1/payments/requests/{paymentRequestId}/link`
Anyone holding it can build the request's fee-sponsored payment transaction, so this route requires `payment_requests.manage`, not `payment_requests.read`. Another project's request returns 404. The token is returned whatever the request's status.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `payment_requests.manage` action.
## Path parameters
- **`paymentRequestId`** · `string` · required · min length 1 · example `preq_example`
SDP payment request ID.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Payment request link token
- **`data`** · `object` · required
A payment request's link token.
- **`id`** · `string` · required · example `preq_example`
SDP payment request ID.
- **`publicToken`** · `string` · required · example `example-public-token`
Token behind the request's hosted payment link, `/pay/{publicToken}`. Anyone holding it can build the request's fee-sponsored payment transaction, so share it only with the payer. Reads never return it; get it again with `GET /v1/payments/requests/{paymentRequestId}/link`.
- **`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 `payment_requests.manage` 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`.
- **`500`** Internal Server Error
Same fields as `400`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/payments/requests/preq_example/link' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/requests/preq_example/link", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/payments/requests/preq_example/link",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"id": "preq_example",
"publicToken": "example-public-token"
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: payment_requests.manage",
"details": {
"action": "payment_requests.manage"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Cancel payment request
Source: https://platform.solana.com/docs/reference/api/payments/cancel-payment-request
> Cancels an open payment request, so its payment link stops building and sponsoring transactions.
`POST /v1/payments/requests/{paymentRequestId}/cancel`
Requires `payment_requests.manage`. Canceling a request that is already canceled returns it unchanged. A paid or expired request returns 409, and so does one whose fee-sponsored transaction can still land: retry once that transaction's blockhash expires. A payer who signed before the cancel can still land the payment; SDP records it as paid, with `paidLate` set when it landed after the cancel.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `payment_requests.manage` action.
## Path parameters
- **`paymentRequestId`** · `string` · required · min length 1 · example `preq_example`
SDP payment request ID.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
- **`Idempotency-Key`** · `string`
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).
## Responses
- **`200`** Payment request canceled
- **`data`** · `object` · required
A payment request. Reads never carry its link token.
- **`id`** · `string` · required · example `preq_example`
SDP payment request ID.
- **`organizationId`** · `string` · required
Organization ID.
- **`projectId`** · `string | null` · required
Project ID.
- **`walletId`** · `string` · required
Receiving custody wallet's provider ID.
- **`destinationAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Address the payment is sent to.
- **`token`** · `string` · required
Token mint address.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`reference`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Solana Pay reference account that identifies the payment on chain.
- **`status`** · `string` · required · one of `awaiting_payment`, `paid`, `canceled`, `expired` · example `canceled`
Payment request status. `awaiting_payment` is open; `paid` was settled by an on-chain payment; `canceled` and `expired` closed without one.
- **`expiresAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
Expiry, if any.
- **`fulfilledByTransferId`** · `string | null` · required
The inbound transfer that paid the request.
- **`paidLate`** · `boolean` · required
Whether the payment landed after the request expired or was canceled. A payer who signed before then can still pay.
- **`lastCheckedAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
When SDP last read the chain for the payment.
- **`canceledByApiKeyId`** · `string | null` · required
The API key that canceled the request; null when a member did, or it is not canceled.
- **`canceledByUserId`** · `string | null` · required
User ID of the member who canceled the request; null when an API key did, or it is not canceled.
- **`canceledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
When it was canceled.
- **`lifecycle`** · `object[]` · required
Every status the request entered, oldest first.
- **`status`** · `string` · required · one of `awaiting_payment`, `paid`, `canceled`, `expired` · example `canceled`
Payment request status. `awaiting_payment` is open; `paid` was settled by an on-chain payment; `canceled` and `expired` closed without one.
- **`at`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`createdBy`** · `string | null` · required
User ID of the member, or of the API key's creator, who created it.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 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 `payment_requests.manage` 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/requests/preq_example/cancel' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY"
```
```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/requests/preq_example/cancel", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey
}
});
```
```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/requests/preq_example/cancel",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key})
```
## Example responses
```json tab="200"
{
"data": {
"id": "preq_example",
"organizationId": "organization_example",
"projectId": "project_example",
"walletId": "wallet_example",
"destinationAddress": "So11111111111111111111111111111111111111112",
"token": "example",
"amount": "100.00",
"reference": "So11111111111111111111111111111111111111112",
"status": "canceled",
"expiresAt": "2025-01-01T00:00:00.000Z",
"fulfilledByTransferId": "fulfilledbytransfer_example",
"paidLate": false,
"lastCheckedAt": "2025-01-01T00:00:00.000Z",
"canceledByApiKeyId": "canceledbyapikey_example",
"canceledByUserId": "canceledbyuser_example",
"canceledAt": "2025-01-01T00:00:00.000Z",
"lifecycle": [
{
"status": "canceled",
"at": "2025-01-01T00:00:00.000Z"
}
],
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: payment_requests.manage",
"details": {
"action": "payment_requests.manage"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
## Payments: Subscription plans
### Create subscription plan
Source: https://platform.solana.com/docs/reference/api/payments/create-payment-subscription-plan
> Creates a recurring-payment subscription plan record.
`POST /v1/payments/subscription-plans`
This stores SDP backend state and Solana subscriptions program identifiers; it does not by itself create the on-chain plan.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.manage` 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`
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
Creates an SDP subscription plan record. Prepare the on-chain Solana subscriptions program transaction separately with the plan prepare endpoint.
- **`ownerWalletId`** · `string` · required · min length 1 · example `wal_merchant`
Custody wallet that owns the Solana subscription plan.
- **`token`** · `string` · required · example `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`
Token mint address. Native SOL is not expected for program-backed subscriptions.
- **`amount`** · `string` · required · example `25.00`
Recurring charge amount in UI units.
- **`periodHours`** · `integer` · required · greater than 0 · max 8760 · example `720`
Billing period length in hours.
- **`programPlanId`** · `string` · pattern `^\d+$` · example `17014118346046923173`
Unsigned 64-bit decimal program plan identifier used in the Solana subscriptions PDA seed. Defaults to a generated nonzero value.
- **`planPda`** · `string` · example `7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU`
On-chain plan PDA once created.
- **`destinationAddress`** · `string` · example `7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU`
Optional destination owner address allowed by the on-chain plan.
- **`pullerWalletId`** · `string` · min length 1 · example `wal_collector`
Optional SDP custody wallet allowed to pull subscription payments.
- **`metadataUri`** · `string` · format `uri` · max length 128 · example `https://example.com/subscriptions/monthly-usdc.json`
Optional plan metadata URI.
- **`status`** · `string` · one of `draft`, `active`, `archived` · example `active`
Subscription plan status.
## Responses
- **`201`** Subscription plan created
- **`data`** · `object` · required
Subscription plan response payload.
- **`subscriptionPlan`** · `object` · required
Recurring payment subscription plan record.
- **`id`** · `string` · required · example `psp_example`
SDP subscription plan ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`ownerWalletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Provider wallet ID — the `walletId` field returned by GET /v1/wallets.
- **`ownerAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Base58-encoded Solana address.
- **`token`** · `string` · required
Token mint or normalized token identifier.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`periodHours`** · `integer` · required · greater than 0 · example `720`
- **`programPlanId`** · `string` · required · example `17014118346046923173`
Unsigned 64-bit decimal program plan identifier used in PDA derivation.
- **`planPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain plan PDA.
- **`destinationAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Allowed destination owner address.
- **`pullerWalletId`** · `string | null` · required · min length 1 · example `privy_wallet_123`
Collector wallet ID.
- **`pullerAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Collector wallet address.
- **`metadataUri`** · `string | null` · required
Optional plan metadata URI.
- **`status`** · `string` · required · one of `draft`, `active`, `archived` · example `active`
Subscription plan status.
- **`createdBy`** · `string | null` · required
Creator user ID or API key ID.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 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 `recurring.manage` 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`.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST 'https://api.solana.com/v1/payments/subscription-plans' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"ownerWalletId": "wal_merchant",
"token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "25.00",
"periodHours": 720,
"programPlanId": "17014118346046923173",
"planPda": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"destinationAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"pullerWalletId": "wal_collector",
"metadataUri": "https://example.com/subscriptions/monthly-usdc.json",
"status": "active"
}'
```
```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/subscription-plans", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
"ownerWalletId": "wal_merchant",
"token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "25.00",
"periodHours": 720,
"programPlanId": "17014118346046923173",
"planPda": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"destinationAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"pullerWalletId": "wal_collector",
"metadataUri": "https://example.com/subscriptions/monthly-usdc.json",
"status": "active"
})
});
```
```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/subscription-plans",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
json={
"ownerWalletId": "wal_merchant",
"token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "25.00",
"periodHours": 720,
"programPlanId": "17014118346046923173",
"planPda": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"destinationAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"pullerWalletId": "wal_collector",
"metadataUri": "https://example.com/subscriptions/monthly-usdc.json",
"status": "active"
})
```
## Example responses
```json tab="201"
{
"data": {
"subscriptionPlan": {
"id": "psp_example",
"organizationId": "org_example",
"projectId": "prj_example",
"ownerWalletId": "privy_wallet_123",
"ownerAddress": "So11111111111111111111111111111111111111112",
"token": "example",
"amount": "100.00",
"periodHours": 720,
"programPlanId": "17014118346046923173",
"planPda": "So11111111111111111111111111111111111111112",
"destinationAddress": "So11111111111111111111111111111111111111112",
"pullerWalletId": "privy_wallet_123",
"pullerAddress": "So11111111111111111111111111111111111111112",
"metadataUri": "example",
"status": "active",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.manage",
"details": {
"action": "recurring.manage"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### List subscription plans
Source: https://platform.solana.com/docs/reference/api/payments/list-payment-subscription-plans
> Lists recurring-payment subscription plans.
`GET /v1/payments/subscription-plans`
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.read` action.
## Query parameters
- **`status`** · `string` · one of `draft`, `active`, `archived` · example `active`
Subscription plan status.
- **`page`** · `integer` · greater than 0 · default `1`
- **`pageSize`** · `integer` · min 1 · max 100 · default `20`
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Subscription plan list
- **`data`** · `object` · required
Subscription plan list response payload.
- **`subscriptionPlans`** · `object[]` · required
- **`id`** · `string` · required · example `psp_example`
SDP subscription plan ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`ownerWalletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Provider wallet ID — the `walletId` field returned by GET /v1/wallets.
- **`ownerAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Base58-encoded Solana address.
- **`token`** · `string` · required
Token mint or normalized token identifier.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`periodHours`** · `integer` · required · greater than 0 · example `720`
- **`programPlanId`** · `string` · required · example `17014118346046923173`
Unsigned 64-bit decimal program plan identifier used in PDA derivation.
- **`planPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain plan PDA.
- **`destinationAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Allowed destination owner address.
- **`pullerWalletId`** · `string | null` · required · min length 1 · example `privy_wallet_123`
Collector wallet ID.
- **`pullerAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Collector wallet address.
- **`metadataUri`** · `string | null` · required
Optional plan metadata URI.
- **`status`** · `string` · required · one of `draft`, `active`, `archived` · example `active`
Subscription plan status.
- **`createdBy`** · `string | null` · required
Creator user ID or API key ID.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`total`** · `integer` · required
- **`page`** · `integer` · required
- **`pageSize`** · `integer` · required
- **`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 `recurring.read` 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`.
- **`500`** Internal Server Error
Same fields as `400`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/payments/subscription-plans' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/subscription-plans", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/payments/subscription-plans",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"subscriptionPlans": [
{
"id": "psp_example",
"organizationId": "org_example",
"projectId": "prj_example",
"ownerWalletId": "privy_wallet_123",
"ownerAddress": "So11111111111111111111111111111111111111112",
"token": "example",
"amount": "100.00",
"periodHours": 720,
"programPlanId": "17014118346046923173",
"planPda": "So11111111111111111111111111111111111111112",
"destinationAddress": "So11111111111111111111111111111111111111112",
"pullerWalletId": "privy_wallet_123",
"pullerAddress": "So11111111111111111111111111111111111111112",
"metadataUri": "example",
"status": "active",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
],
"total": 1,
"page": 1,
"pageSize": 1
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.read",
"details": {
"action": "recurring.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Prepare subscription plan creation
Source: https://platform.solana.com/docs/reference/api/payments/prepare-payment-subscription-plan-create
> Prepares an unsigned Solana subscriptions program create-plan transaction from an SDP subscription plan.
`POST /v1/payments/subscription-plans/{planId}/prepare-create`
This derives and stores the plan PDA but does not submit the transaction.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.manage` action.
## Path parameters
- **`planId`** · `string` · required · min length 1 · example `psp_example`
SDP subscription plan record ID.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Request body
`application/json`
Optional overrides used when preparing the Solana subscriptions create-plan transaction.
- **`destinations`** · `string[]` · max items 4
Allowed destination addresses for the on-chain plan. Defaults to the stored plan destination.
- **`pullers`** · `string[]` · max items 4
Allowed puller addresses for the on-chain plan. Defaults to the stored puller or owner address.
- **`endTs`** · `string` · pattern `^\d+$` · example `1770000000`
Optional unsigned 64-bit Unix timestamp when the on-chain plan ends.
- **`metadataUri`** · `string` · format `uri` · max length 128 · example `https://example.com/subscriptions/monthly-usdc.json`
Optional metadata URI to embed in the on-chain plan.
## Responses
- **`200`** Subscription plan creation prepared
- **`data`** · `object` · required
Prepared Solana subscriptions create-plan response payload.
- **`subscriptionPlan`** · `object` · required
Recurring payment subscription plan record.
- **`id`** · `string` · required · example `psp_example`
SDP subscription plan ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`ownerWalletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Provider wallet ID — the `walletId` field returned by GET /v1/wallets.
- **`ownerAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Base58-encoded Solana address.
- **`token`** · `string` · required
Token mint or normalized token identifier.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`periodHours`** · `integer` · required · greater than 0 · example `720`
- **`programPlanId`** · `string` · required · example `17014118346046923173`
Unsigned 64-bit decimal program plan identifier used in PDA derivation.
- **`planPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain plan PDA.
- **`destinationAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Allowed destination owner address.
- **`pullerWalletId`** · `string | null` · required · min length 1 · example `privy_wallet_123`
Collector wallet ID.
- **`pullerAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Collector wallet address.
- **`metadataUri`** · `string | null` · required
Optional plan metadata URI.
- **`status`** · `string` · required · one of `draft`, `active`, `archived` · example `active`
Subscription plan status.
- **`createdBy`** · `string | null` · required
Creator user ID or API key ID.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`planPda`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Derived Solana subscriptions plan PDA.
- **`preparedTransaction`** · `object` · required
Unsigned Solana subscriptions transaction payload for client-side signing.
- **`serialized`** · `string` · required · min length 1 · example `AQID`
Base64-encoded unsigned transaction.
- **`blockhash`** · `string` · required · example `blockhash_example`
Blockhash for the transaction.
- **`lastValidBlockHeight`** · `string` · required · example `123456`
Last valid block height.
- **`requiredSigners`** · `string[]` · required · each min length 32 · each max length 44 · each example `So11111111111111111111111111111111111111112`
Addresses that must sign the prepared transaction. This includes the sponsored fee payer when the transaction uses one.
- **`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 `recurring.manage` 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`.
- **`500`** Internal Server Error
Same fields as `400`.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
curl -X POST 'https://api.solana.com/v1/payments/subscription-plans/psp_example/prepare-create' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destinations": [
"example"
],
"pullers": [
"example"
],
"endTs": "1770000000",
"metadataUri": "https://example.com/subscriptions/monthly-usdc.json"
}'
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/subscription-plans/psp_example/prepare-create", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"destinations": [
"example"
],
"pullers": [
"example"
],
"endTs": "1770000000",
"metadataUri": "https://example.com/subscriptions/monthly-usdc.json"
})
});
```
```python tab="Python"
import os, requests
r = requests.post("https://api.solana.com/v1/payments/subscription-plans/psp_example/prepare-create",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"},
json={
"destinations": [
"example"
],
"pullers": [
"example"
],
"endTs": "1770000000",
"metadataUri": "https://example.com/subscriptions/monthly-usdc.json"
})
```
## Example responses
```json tab="200"
{
"data": {
"subscriptionPlan": {
"id": "psp_example",
"organizationId": "org_example",
"projectId": "prj_example",
"ownerWalletId": "privy_wallet_123",
"ownerAddress": "So11111111111111111111111111111111111111112",
"token": "example",
"amount": "100.00",
"periodHours": 720,
"programPlanId": "17014118346046923173",
"planPda": "So11111111111111111111111111111111111111112",
"destinationAddress": "So11111111111111111111111111111111111111112",
"pullerWalletId": "privy_wallet_123",
"pullerAddress": "So11111111111111111111111111111111111111112",
"metadataUri": "example",
"status": "active",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"planPda": "So11111111111111111111111111111111111111112",
"preparedTransaction": {
"serialized": "AQID",
"blockhash": "blockhash_example",
"lastValidBlockHeight": "123456",
"requiredSigners": [
"So11111111111111111111111111111111111111112"
]
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.manage",
"details": {
"action": "recurring.manage"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Get subscription plan
Source: https://platform.solana.com/docs/reference/api/payments/get-payment-subscription-plan
> Retrieves a recurring-payment subscription plan record.
`GET /v1/payments/subscription-plans/{planId}`
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.read` action.
## Path parameters
- **`planId`** · `string` · required · min length 1 · example `psp_example`
SDP subscription plan record ID.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Subscription plan
- **`data`** · `object` · required
Subscription plan response payload.
- **`subscriptionPlan`** · `object` · required
Recurring payment subscription plan record.
- **`id`** · `string` · required · example `psp_example`
SDP subscription plan ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`ownerWalletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Provider wallet ID — the `walletId` field returned by GET /v1/wallets.
- **`ownerAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Base58-encoded Solana address.
- **`token`** · `string` · required
Token mint or normalized token identifier.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`periodHours`** · `integer` · required · greater than 0 · example `720`
- **`programPlanId`** · `string` · required · example `17014118346046923173`
Unsigned 64-bit decimal program plan identifier used in PDA derivation.
- **`planPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain plan PDA.
- **`destinationAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Allowed destination owner address.
- **`pullerWalletId`** · `string | null` · required · min length 1 · example `privy_wallet_123`
Collector wallet ID.
- **`pullerAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Collector wallet address.
- **`metadataUri`** · `string | null` · required
Optional plan metadata URI.
- **`status`** · `string` · required · one of `draft`, `active`, `archived` · example `active`
Subscription plan status.
- **`createdBy`** · `string | null` · required
Creator user ID or API key ID.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 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.
- **`401`** Unauthorized
- **`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.
- **`403`** Forbidden. Requires the `recurring.read` 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 `401`.
- **`404`** Not Found
Same fields as `401`.
- **`500`** Internal Server Error
Same fields as `401`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/payments/subscription-plans/psp_example' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/subscription-plans/psp_example", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/payments/subscription-plans/psp_example",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"subscriptionPlan": {
"id": "psp_example",
"organizationId": "org_example",
"projectId": "prj_example",
"ownerWalletId": "privy_wallet_123",
"ownerAddress": "So11111111111111111111111111111111111111112",
"token": "example",
"amount": "100.00",
"periodHours": 720,
"programPlanId": "17014118346046923173",
"planPda": "So11111111111111111111111111111111111111112",
"destinationAddress": "So11111111111111111111111111111111111111112",
"pullerWalletId": "privy_wallet_123",
"pullerAddress": "So11111111111111111111111111111111111111112",
"metadataUri": "example",
"status": "active",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.read",
"details": {
"action": "recurring.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Update subscription plan
Source: https://platform.solana.com/docs/reference/api/payments/update-payment-subscription-plan
> Updates mutable subscription plan fields and on-chain identifiers.
`PATCH /v1/payments/subscription-plans/{planId}`
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.execute` action.
## Path parameters
- **`planId`** · `string` · required · min length 1 · example `psp_example`
SDP subscription plan record ID.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
- **`Idempotency-Key`** · `string`
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
Updates mutable SDP subscription plan fields.
- **`planPda`** · `string | null` · example `7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU`
On-chain plan PDA, or null to clear it.
- **`destinationAddress`** · `string | null`
Destination owner address, or null to clear it.
- **`pullerWalletId`** · `string | null` · min length 1 · example `wal_collector`
Collection wallet ID, or null to clear it.
- **`metadataUri`** · `string | null` · format `uri` · max length 128
Plan metadata URI, or null to clear it.
- **`status`** · `string` · one of `draft`, `active`, `archived` · example `active`
Subscription plan status.
## Responses
- **`200`** Subscription plan updated
- **`data`** · `object` · required
Subscription plan response payload.
- **`subscriptionPlan`** · `object` · required
Recurring payment subscription plan record.
- **`id`** · `string` · required · example `psp_example`
SDP subscription plan ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`ownerWalletId`** · `string` · required · min length 1 · example `privy_wallet_123`
Provider wallet ID — the `walletId` field returned by GET /v1/wallets.
- **`ownerAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Base58-encoded Solana address.
- **`token`** · `string` · required
Token mint or normalized token identifier.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`periodHours`** · `integer` · required · greater than 0 · example `720`
- **`programPlanId`** · `string` · required · example `17014118346046923173`
Unsigned 64-bit decimal program plan identifier used in PDA derivation.
- **`planPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain plan PDA.
- **`destinationAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Allowed destination owner address.
- **`pullerWalletId`** · `string | null` · required · min length 1 · example `privy_wallet_123`
Collector wallet ID.
- **`pullerAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Collector wallet address.
- **`metadataUri`** · `string | null` · required
Optional plan metadata URI.
- **`status`** · `string` · required · one of `draft`, `active`, `archived` · example `active`
Subscription plan status.
- **`createdBy`** · `string | null` · required
Creator user ID or API key ID.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 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 `recurring.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`.
## Example request
```bash tab="cURL"
IDEMPOTENCY_KEY=$(uuidgen)
curl -X PATCH 'https://api.solana.com/v1/payments/subscription-plans/psp_example' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"planPda": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"pullerWalletId": "wal_collector",
"status": "active"
}'
```
```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/subscription-plans/psp_example", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
"planPda": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"pullerWalletId": "wal_collector",
"status": "active"
})
});
```
```python tab="Python"
import os, requests, uuid
# Reuse this key when you retry this request.
idempotency_key = str(uuid.uuid4())
r = requests.patch("https://api.solana.com/v1/payments/subscription-plans/psp_example",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
json={
"planPda": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"pullerWalletId": "wal_collector",
"status": "active"
})
```
## Example responses
```json tab="200"
{
"data": {
"subscriptionPlan": {
"id": "psp_example",
"organizationId": "org_example",
"projectId": "prj_example",
"ownerWalletId": "privy_wallet_123",
"ownerAddress": "So11111111111111111111111111111111111111112",
"token": "example",
"amount": "100.00",
"periodHours": 720,
"programPlanId": "17014118346046923173",
"planPda": "So11111111111111111111111111111111111111112",
"destinationAddress": "So11111111111111111111111111111111111111112",
"pullerWalletId": "privy_wallet_123",
"pullerAddress": "So11111111111111111111111111111111111111112",
"metadataUri": "example",
"status": "active",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.execute",
"details": {
"action": "recurring.execute"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
## Payments: Subscriptions
### Create subscription
Source: https://platform.solana.com/docs/reference/api/payments/create-payment-subscription
> Creates a recurring-payment subscription record tied to a counterparty.
`POST /v1/payments/subscriptions`
The customer must still sign the Solana subscription authorization transaction. The subscriber must be the customer's own wallet: a subscriber that is one of the organization's SDP wallets answers 409, because to pay on a schedule from an SDP wallet you create a recurring payment, which keeps the wallet's token approval bounded.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.manage` 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`
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
Creates a pending SDP subscription record tied to a counterparty. Lifecycle, schedule, on-chain addresses, and authorization proof are server-owned and populated only by verified flows.
- **`planId`** · `string` · required · min length 1 · example `psp_example`
SDP subscription plan ID.
- **`counterpartyId`** · `string` · required · min length 1 · example `cpty_example`
Counterparty being billed for the recurring payment.
- **`subscriberAddress`** · `string` · required · example `7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU`
Customer wallet address that authorizes the subscription.
## Responses
- **`201`** Subscription created
- **`data`** · `object` · required
Subscription response payload.
- **`subscription`** · `object` · required
Recurring payment subscription record.
- **`id`** · `string` · required · example `psub_example`
SDP subscription ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`planId`** · `string` · required
SDP subscription plan ID.
- **`counterpartyId`** · `string` · required
Counterparty ID.
- **`subscriberAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Base58-encoded Solana address.
- **`subscriberTokenAccount`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Subscriber token account address.
- **`subscriptionPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription PDA.
- **`subscriptionAuthorityAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription authority address.
- **`authorizationSignature`** · `string | null` · required
Customer authorization transaction signature.
- **`status`** · `string` · required · one of `pending_authorization`, `active`, `paused`, `canceling`, `canceled`, `expired` · example `active`
Subscription status.
- **`currentPeriodStartAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`nextCollectionDueAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`cancelAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`canceledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`createdBy`** · `string | null` · required
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 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 `recurring.manage` 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`.
## Example request
```bash tab="cURL"
IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST 'https://api.solana.com/v1/payments/subscriptions' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"planId": "psp_example",
"counterpartyId": "cpty_example",
"subscriberAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
}'
```
```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/subscriptions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
"planId": "psp_example",
"counterpartyId": "cpty_example",
"subscriberAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
})
});
```
```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/subscriptions",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
json={
"planId": "psp_example",
"counterpartyId": "cpty_example",
"subscriberAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
})
```
## Example responses
```json tab="201"
{
"data": {
"subscription": {
"id": "psub_example",
"organizationId": "org_example",
"projectId": "prj_example",
"planId": "plan_example",
"counterpartyId": "counterparty_example",
"subscriberAddress": "So11111111111111111111111111111111111111112",
"subscriberTokenAccount": "So11111111111111111111111111111111111111112",
"subscriptionPda": "So11111111111111111111111111111111111111112",
"subscriptionAuthorityAddress": "So11111111111111111111111111111111111111112",
"authorizationSignature": "example",
"status": "active",
"currentPeriodStartAt": "2025-01-01T00:00:00.000Z",
"nextCollectionDueAt": "2025-01-01T00:00:00.000Z",
"cancelAt": "2025-01-01T00:00:00.000Z",
"canceledAt": "2025-01-01T00:00:00.000Z",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.manage",
"details": {
"action": "recurring.manage"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### List subscriptions
Source: https://platform.solana.com/docs/reference/api/payments/list-payment-subscriptions
> Lists recurring-payment subscriptions.
`GET /v1/payments/subscriptions`
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.read` action.
## Query parameters
- **`planId`** · `string` · min length 1
- **`counterpartyId`** · `string` · min length 1
- **`status`** · `string` · one of `pending_authorization`, `active`, `paused`, `canceling`, `canceled`, `expired` · example `active`
Subscription status.
- **`dueBefore`** · `string` · format `date-time`
- **`page`** · `integer` · greater than 0 · default `1`
- **`pageSize`** · `integer` · min 1 · max 100 · default `20`
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Subscription list
- **`data`** · `object` · required
Subscription list response payload.
- **`subscriptions`** · `object[]` · required
- **`id`** · `string` · required · example `psub_example`
SDP subscription ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`planId`** · `string` · required
SDP subscription plan ID.
- **`counterpartyId`** · `string` · required
Counterparty ID.
- **`subscriberAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Base58-encoded Solana address.
- **`subscriberTokenAccount`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Subscriber token account address.
- **`subscriptionPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription PDA.
- **`subscriptionAuthorityAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription authority address.
- **`authorizationSignature`** · `string | null` · required
Customer authorization transaction signature.
- **`status`** · `string` · required · one of `pending_authorization`, `active`, `paused`, `canceling`, `canceled`, `expired` · example `active`
Subscription status.
- **`currentPeriodStartAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`nextCollectionDueAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`cancelAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`canceledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`createdBy`** · `string | null` · required
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`total`** · `integer` · required
- **`page`** · `integer` · required
- **`pageSize`** · `integer` · required
- **`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 `recurring.read` 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`.
- **`500`** Internal Server Error
Same fields as `400`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/payments/subscriptions' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/subscriptions", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/payments/subscriptions",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"subscriptions": [
{
"id": "psub_example",
"organizationId": "org_example",
"projectId": "prj_example",
"planId": "plan_example",
"counterpartyId": "counterparty_example",
"subscriberAddress": "So11111111111111111111111111111111111111112",
"subscriberTokenAccount": "So11111111111111111111111111111111111111112",
"subscriptionPda": "So11111111111111111111111111111111111111112",
"subscriptionAuthorityAddress": "So11111111111111111111111111111111111111112",
"authorizationSignature": "example",
"status": "active",
"currentPeriodStartAt": "2025-01-01T00:00:00.000Z",
"nextCollectionDueAt": "2025-01-01T00:00:00.000Z",
"cancelAt": "2025-01-01T00:00:00.000Z",
"canceledAt": "2025-01-01T00:00:00.000Z",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
],
"total": 1,
"page": 1,
"pageSize": 1
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.read",
"details": {
"action": "recurring.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Prepare subscription authorization
Source: https://platform.solana.com/docs/reference/api/payments/prepare-payment-subscription-authorization
> Prepares the subscriber-signed Solana transaction that initializes the subscription authority and subscribes to the plan.
`POST /v1/payments/subscriptions/{subscriptionId}/prepare-authorization`
The transaction must still be signed and submitted by the client. A subscriber that is one of the organization's SDP wallets answers 409: pay from an SDP wallet through a recurring payment.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.execute` action.
## Path parameters
- **`subscriptionId`** · `string` · required · min length 1 · example `psub_example`
SDP subscription record ID.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Request body
`application/json` · required
Inputs for preparing the subscriber-signed Solana subscription authorization transaction.
- **`subscriberTokenAccount`** · `string` · required · example `7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU`
Subscriber token account that authorizes the subscription authority delegation.
- **`expectedPlanCreatedAt`** · `string` · required · pattern `^\d+$` · example `0`
Unsigned 64-bit created-at timestamp from the on-chain plan terms that the subscriber consents to.
- **`expectedSubscriptionAuthorityInitId`** · `string` · required · pattern `^-?\d+$` · example `0`
Signed 64-bit init id from the on-chain subscription authority that the subscriber consents to.
## Responses
- **`200`** Subscription authorization prepared
- **`data`** · `object` · required
Prepared subscription authorization response payload.
- **`subscription`** · `object` · required
Recurring payment subscription record.
- **`id`** · `string` · required · example `psub_example`
SDP subscription ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`planId`** · `string` · required
SDP subscription plan ID.
- **`counterpartyId`** · `string` · required
Counterparty ID.
- **`subscriberAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Base58-encoded Solana address.
- **`subscriberTokenAccount`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Subscriber token account address.
- **`subscriptionPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription PDA.
- **`subscriptionAuthorityAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription authority address.
- **`authorizationSignature`** · `string | null` · required
Customer authorization transaction signature.
- **`status`** · `string` · required · one of `pending_authorization`, `active`, `paused`, `canceling`, `canceled`, `expired` · example `active`
Subscription status.
- **`currentPeriodStartAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`nextCollectionDueAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`cancelAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`canceledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`createdBy`** · `string | null` · required
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`subscriptionPda`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Derived subscription delegation PDA.
- **`subscriptionAuthorityAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Derived subscription authority PDA.
- **`preparedTransaction`** · `object` · required
Unsigned Solana subscriptions transaction payload for client-side signing.
- **`serialized`** · `string` · required · min length 1 · example `AQID`
Base64-encoded unsigned transaction.
- **`blockhash`** · `string` · required · example `blockhash_example`
Blockhash for the transaction.
- **`lastValidBlockHeight`** · `string` · required · example `123456`
Last valid block height.
- **`requiredSigners`** · `string[]` · required · each min length 32 · each max length 44 · each example `So11111111111111111111111111111111111111112`
Addresses that must sign the prepared transaction. This includes the sponsored fee payer when the transaction uses one.
- **`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 `recurring.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`.
- **`500`** Internal Server Error
Same fields as `400`.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
curl -X POST 'https://api.solana.com/v1/payments/subscriptions/psub_example/prepare-authorization' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subscriberTokenAccount": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"expectedPlanCreatedAt": "0",
"expectedSubscriptionAuthorityInitId": "0"
}'
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/subscriptions/psub_example/prepare-authorization", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"subscriberTokenAccount": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"expectedPlanCreatedAt": "0",
"expectedSubscriptionAuthorityInitId": "0"
})
});
```
```python tab="Python"
import os, requests
r = requests.post("https://api.solana.com/v1/payments/subscriptions/psub_example/prepare-authorization",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"},
json={
"subscriberTokenAccount": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"expectedPlanCreatedAt": "0",
"expectedSubscriptionAuthorityInitId": "0"
})
```
## Example responses
```json tab="200"
{
"data": {
"subscription": {
"id": "psub_example",
"organizationId": "org_example",
"projectId": "prj_example",
"planId": "plan_example",
"counterpartyId": "counterparty_example",
"subscriberAddress": "So11111111111111111111111111111111111111112",
"subscriberTokenAccount": "So11111111111111111111111111111111111111112",
"subscriptionPda": "So11111111111111111111111111111111111111112",
"subscriptionAuthorityAddress": "So11111111111111111111111111111111111111112",
"authorizationSignature": "example",
"status": "active",
"currentPeriodStartAt": "2025-01-01T00:00:00.000Z",
"nextCollectionDueAt": "2025-01-01T00:00:00.000Z",
"cancelAt": "2025-01-01T00:00:00.000Z",
"canceledAt": "2025-01-01T00:00:00.000Z",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"subscriptionPda": "So11111111111111111111111111111111111111112",
"subscriptionAuthorityAddress": "So11111111111111111111111111111111111111112",
"preparedTransaction": {
"serialized": "AQID",
"blockhash": "blockhash_example",
"lastValidBlockHeight": "123456",
"requiredSigners": [
"So11111111111111111111111111111111111111112"
]
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.execute",
"details": {
"action": "recurring.execute"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Prepare subscription cancellation
Source: https://platform.solana.com/docs/reference/api/payments/prepare-payment-subscription-cancel
> Prepares the subscriber-signed Solana transaction that cancels a subscription.
`POST /v1/payments/subscriptions/{subscriptionId}/prepare-cancel`
The transaction must still be signed and submitted by the client.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.manage` action.
## Path parameters
- **`subscriptionId`** · `string` · required · min length 1 · example `psub_example`
SDP subscription record ID.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Request body
`application/json`
Empty request body for preparing subscriber-signed subscription lifecycle transactions.
## Responses
- **`200`** Subscription cancellation prepared
- **`data`** · `object` · required
Prepared subscription lifecycle response payload.
- **`subscription`** · `object` · required
Recurring payment subscription record.
- **`id`** · `string` · required · example `psub_example`
SDP subscription ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`planId`** · `string` · required
SDP subscription plan ID.
- **`counterpartyId`** · `string` · required
Counterparty ID.
- **`subscriberAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Base58-encoded Solana address.
- **`subscriberTokenAccount`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Subscriber token account address.
- **`subscriptionPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription PDA.
- **`subscriptionAuthorityAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription authority address.
- **`authorizationSignature`** · `string | null` · required
Customer authorization transaction signature.
- **`status`** · `string` · required · one of `pending_authorization`, `active`, `paused`, `canceling`, `canceled`, `expired` · example `active`
Subscription status.
- **`currentPeriodStartAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`nextCollectionDueAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`cancelAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`canceledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`createdBy`** · `string | null` · required
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`preparedTransaction`** · `object` · required
Unsigned Solana subscriptions transaction payload for client-side signing.
- **`serialized`** · `string` · required · min length 1 · example `AQID`
Base64-encoded unsigned transaction.
- **`blockhash`** · `string` · required · example `blockhash_example`
Blockhash for the transaction.
- **`lastValidBlockHeight`** · `string` · required · example `123456`
Last valid block height.
- **`requiredSigners`** · `string[]` · required · each min length 32 · each max length 44 · each example `So11111111111111111111111111111111111111112`
Addresses that must sign the prepared transaction. This includes the sponsored fee payer when the transaction uses one.
- **`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 `recurring.manage` 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`.
- **`500`** Internal Server Error
Same fields as `400`.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
curl -X POST 'https://api.solana.com/v1/payments/subscriptions/psub_example/prepare-cancel' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/subscriptions/psub_example/prepare-cancel", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({})
});
```
```python tab="Python"
import os, requests
r = requests.post("https://api.solana.com/v1/payments/subscriptions/psub_example/prepare-cancel",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"},
json={})
```
## Example responses
```json tab="200"
{
"data": {
"subscription": {
"id": "psub_example",
"organizationId": "org_example",
"projectId": "prj_example",
"planId": "plan_example",
"counterpartyId": "counterparty_example",
"subscriberAddress": "So11111111111111111111111111111111111111112",
"subscriberTokenAccount": "So11111111111111111111111111111111111111112",
"subscriptionPda": "So11111111111111111111111111111111111111112",
"subscriptionAuthorityAddress": "So11111111111111111111111111111111111111112",
"authorizationSignature": "example",
"status": "active",
"currentPeriodStartAt": "2025-01-01T00:00:00.000Z",
"nextCollectionDueAt": "2025-01-01T00:00:00.000Z",
"cancelAt": "2025-01-01T00:00:00.000Z",
"canceledAt": "2025-01-01T00:00:00.000Z",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"preparedTransaction": {
"serialized": "AQID",
"blockhash": "blockhash_example",
"lastValidBlockHeight": "123456",
"requiredSigners": [
"So11111111111111111111111111111111111111112"
]
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.manage",
"details": {
"action": "recurring.manage"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Prepare subscription resume
Source: https://platform.solana.com/docs/reference/api/payments/prepare-payment-subscription-resume
> Prepares the subscriber-signed Solana transaction that resumes a canceled subscription before revocation.
`POST /v1/payments/subscriptions/{subscriptionId}/prepare-resume`
The transaction must still be signed and submitted by the client. A subscription that belongs to a recurring payment answers 409: a recurring payment's cancel is final.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.execute` action.
## Path parameters
- **`subscriptionId`** · `string` · required · min length 1 · example `psub_example`
SDP subscription record ID.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Request body
`application/json`
Empty request body for preparing subscriber-signed subscription lifecycle transactions.
## Responses
- **`200`** Subscription resume prepared
- **`data`** · `object` · required
Prepared subscription lifecycle response payload.
- **`subscription`** · `object` · required
Recurring payment subscription record.
- **`id`** · `string` · required · example `psub_example`
SDP subscription ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`planId`** · `string` · required
SDP subscription plan ID.
- **`counterpartyId`** · `string` · required
Counterparty ID.
- **`subscriberAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Base58-encoded Solana address.
- **`subscriberTokenAccount`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Subscriber token account address.
- **`subscriptionPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription PDA.
- **`subscriptionAuthorityAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription authority address.
- **`authorizationSignature`** · `string | null` · required
Customer authorization transaction signature.
- **`status`** · `string` · required · one of `pending_authorization`, `active`, `paused`, `canceling`, `canceled`, `expired` · example `active`
Subscription status.
- **`currentPeriodStartAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`nextCollectionDueAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`cancelAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`canceledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`createdBy`** · `string | null` · required
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`preparedTransaction`** · `object` · required
Unsigned Solana subscriptions transaction payload for client-side signing.
- **`serialized`** · `string` · required · min length 1 · example `AQID`
Base64-encoded unsigned transaction.
- **`blockhash`** · `string` · required · example `blockhash_example`
Blockhash for the transaction.
- **`lastValidBlockHeight`** · `string` · required · example `123456`
Last valid block height.
- **`requiredSigners`** · `string[]` · required · each min length 32 · each max length 44 · each example `So11111111111111111111111111111111111111112`
Addresses that must sign the prepared transaction. This includes the sponsored fee payer when the transaction uses one.
- **`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 `recurring.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`.
- **`500`** Internal Server Error
Same fields as `400`.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
curl -X POST 'https://api.solana.com/v1/payments/subscriptions/psub_example/prepare-resume' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/subscriptions/psub_example/prepare-resume", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({})
});
```
```python tab="Python"
import os, requests
r = requests.post("https://api.solana.com/v1/payments/subscriptions/psub_example/prepare-resume",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"},
json={})
```
## Example responses
```json tab="200"
{
"data": {
"subscription": {
"id": "psub_example",
"organizationId": "org_example",
"projectId": "prj_example",
"planId": "plan_example",
"counterpartyId": "counterparty_example",
"subscriberAddress": "So11111111111111111111111111111111111111112",
"subscriberTokenAccount": "So11111111111111111111111111111111111111112",
"subscriptionPda": "So11111111111111111111111111111111111111112",
"subscriptionAuthorityAddress": "So11111111111111111111111111111111111111112",
"authorizationSignature": "example",
"status": "active",
"currentPeriodStartAt": "2025-01-01T00:00:00.000Z",
"nextCollectionDueAt": "2025-01-01T00:00:00.000Z",
"cancelAt": "2025-01-01T00:00:00.000Z",
"canceledAt": "2025-01-01T00:00:00.000Z",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"preparedTransaction": {
"serialized": "AQID",
"blockhash": "blockhash_example",
"lastValidBlockHeight": "123456",
"requiredSigners": [
"So11111111111111111111111111111111111111112"
]
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.execute",
"details": {
"action": "recurring.execute"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Prepare subscription collection
Source: https://platform.solana.com/docs/reference/api/payments/prepare-payment-subscription-collection
> Prepares the collector-signed Solana subscriptions transfer transaction for an active subscription.
`POST /v1/payments/subscriptions/{subscriptionId}/prepare-collection`
The transaction must still be signed and submitted by the collector/fee-payer flow. A subscription that belongs to a recurring payment, or whose subscriber is one of the organization's SDP wallets, answers 409: SDP collects from an SDP wallet only through a recurring payment.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.execute` action.
## Path parameters
- **`subscriptionId`** · `string` · required · min length 1 · example `psub_example`
SDP subscription record ID.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Request body
`application/json` · required
Inputs for preparing the collector-signed Solana subscriptions transfer transaction. The amount is always derived from the stored plan.
- **`receiverTokenAccount`** · `string` · required · example `7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU`
Receiver token account for the pulled subscription payment. It must be allowed by the on-chain plan.
## Responses
- **`200`** Subscription collection prepared
- **`data`** · `object` · required
Prepared subscription collection response payload.
- **`subscription`** · `object` · required
Recurring payment subscription record.
- **`id`** · `string` · required · example `psub_example`
SDP subscription ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`planId`** · `string` · required
SDP subscription plan ID.
- **`counterpartyId`** · `string` · required
Counterparty ID.
- **`subscriberAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Base58-encoded Solana address.
- **`subscriberTokenAccount`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Subscriber token account address.
- **`subscriptionPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription PDA.
- **`subscriptionAuthorityAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription authority address.
- **`authorizationSignature`** · `string | null` · required
Customer authorization transaction signature.
- **`status`** · `string` · required · one of `pending_authorization`, `active`, `paused`, `canceling`, `canceled`, `expired` · example `active`
Subscription status.
- **`currentPeriodStartAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`nextCollectionDueAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`cancelAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`canceledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`createdBy`** · `string | null` · required
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`preparedTransaction`** · `object` · required
Unsigned Solana subscriptions transaction payload for client-side signing.
- **`serialized`** · `string` · required · min length 1 · example `AQID`
Base64-encoded unsigned transaction.
- **`blockhash`** · `string` · required · example `blockhash_example`
Blockhash for the transaction.
- **`lastValidBlockHeight`** · `string` · required · example `123456`
Last valid block height.
- **`requiredSigners`** · `string[]` · required · each min length 32 · each max length 44 · each example `So11111111111111111111111111111111111111112`
Addresses that must sign the prepared transaction. This includes the sponsored fee payer when the transaction uses one.
- **`collectionAttempt`** · `object`
Recurring payment collection attempt record.
- **`id`** · `string` · required · example `psca_example`
Collection attempt ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`subscriptionId`** · `string` · required
Subscription ID.
- **`transferId`** · `string | null` · required · min length 1 · example `xfr_example`
Payment transfer record ID when linked.
- **`token`** · `string` · required
Token mint or normalized token identifier.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`dueAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`attemptedAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`status`** · `string` · required · one of `pending`, `processing`, `confirmed`, `failed`, `skipped` · example `pending`
Collection attempt status.
- **`signature`** · `string | null` · required
Solana transaction signature.
- **`error`** · `string | null` · required · example `The wallet used for this payment does not have enough funds. Add funds and try again.`
Why the collection failed, if it did. A source wallet that cannot cover the amount records the same text the collect request returned with `400 TRANSACTION_FAILED`.
- **`metadata`** · `object` · required
Provider/job metadata for the attempt.
One of:
- **source: "manual"**
- **`recurringPaymentId`** · `string` · required
- **`initiatedByKeyId`** · `string | null` · required
- **`source`** · `string` · required · always `manual`
- **source: "automated"**
- **`recurringPaymentId`** · `string` · required
- **`initiatedByKeyId`** · `string | null` · required
- **`source`** · `string` · required · always `automated`
- **source: "retry"**
- **`recurringPaymentId`** · `string` · required
- **`initiatedByKeyId`** · `string | null` · required
- **`source`** · `string` · required · always `retry`
- **`initialSource`** · `string` · required · one of `manual`, `automated`
- **`transferId`** · `string | null` · required
- **`error`** · `string` · required
- **`retryAfterAt`** · `string` · required
- **source: "linked_transfer"**
- **`recurringPaymentId`** · `string` · required
- **`initiatedByKeyId`** · `string | null` · required
- **`source`** · `string` · required · always `linked_transfer`
- **`initialSource`** · `string` · required · one of `manual`, `automated`
- **`transferId`** · `string` · required
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 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 `recurring.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`.
- **`500`** Internal Server Error
Same fields as `400`.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
curl -X POST 'https://api.solana.com/v1/payments/subscriptions/psub_example/prepare-collection' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"receiverTokenAccount": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
}'
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/subscriptions/psub_example/prepare-collection", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"receiverTokenAccount": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
})
});
```
```python tab="Python"
import os, requests
r = requests.post("https://api.solana.com/v1/payments/subscriptions/psub_example/prepare-collection",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"},
json={
"receiverTokenAccount": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
})
```
## Example responses
```json tab="200"
{
"data": {
"subscription": {
"id": "psub_example",
"organizationId": "org_example",
"projectId": "prj_example",
"planId": "plan_example",
"counterpartyId": "counterparty_example",
"subscriberAddress": "So11111111111111111111111111111111111111112",
"subscriberTokenAccount": "So11111111111111111111111111111111111111112",
"subscriptionPda": "So11111111111111111111111111111111111111112",
"subscriptionAuthorityAddress": "So11111111111111111111111111111111111111112",
"authorizationSignature": "example",
"status": "active",
"currentPeriodStartAt": "2025-01-01T00:00:00.000Z",
"nextCollectionDueAt": "2025-01-01T00:00:00.000Z",
"cancelAt": "2025-01-01T00:00:00.000Z",
"canceledAt": "2025-01-01T00:00:00.000Z",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"preparedTransaction": {
"serialized": "AQID",
"blockhash": "blockhash_example",
"lastValidBlockHeight": "123456",
"requiredSigners": [
"So11111111111111111111111111111111111111112"
]
},
"collectionAttempt": {
"id": "psca_example",
"organizationId": "org_example",
"projectId": "prj_example",
"subscriptionId": "subscription_example",
"transferId": "xfr_example",
"token": "example",
"amount": "100.00",
"dueAt": "2025-01-01T00:00:00.000Z",
"attemptedAt": "2025-01-01T00:00:00.000Z",
"status": "pending",
"signature": "example",
"error": "The wallet used for this payment does not have enough funds. Add funds and try again.",
"metadata": {
"recurringPaymentId": "recurringpayment_example",
"initiatedByKeyId": "initiatedbykey_example",
"source": "manual"
},
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.execute",
"details": {
"action": "recurring.execute"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Get subscription
Source: https://platform.solana.com/docs/reference/api/payments/get-payment-subscription
> Retrieves a recurring-payment subscription record.
`GET /v1/payments/subscriptions/{subscriptionId}`
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.read` action.
## Path parameters
- **`subscriptionId`** · `string` · required · min length 1 · example `psub_example`
SDP subscription record ID.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Subscription
- **`data`** · `object` · required
Subscription response payload.
- **`subscription`** · `object` · required
Recurring payment subscription record.
- **`id`** · `string` · required · example `psub_example`
SDP subscription ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`planId`** · `string` · required
SDP subscription plan ID.
- **`counterpartyId`** · `string` · required
Counterparty ID.
- **`subscriberAddress`** · `string` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Base58-encoded Solana address.
- **`subscriberTokenAccount`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
Subscriber token account address.
- **`subscriptionPda`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription PDA.
- **`subscriptionAuthorityAddress`** · `string | null` · required · min length 32 · max length 44 · example `So11111111111111111111111111111111111111112`
On-chain subscription authority address.
- **`authorizationSignature`** · `string | null` · required
Customer authorization transaction signature.
- **`status`** · `string` · required · one of `pending_authorization`, `active`, `paused`, `canceling`, `canceled`, `expired` · example `active`
Subscription status.
- **`currentPeriodStartAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`nextCollectionDueAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`cancelAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`canceledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`createdBy`** · `string | null` · required
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 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.
- **`401`** Unauthorized
- **`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.
- **`403`** Forbidden. Requires the `recurring.read` 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 `401`.
- **`404`** Not Found
Same fields as `401`.
- **`500`** Internal Server Error
Same fields as `401`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/payments/subscriptions/psub_example' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/subscriptions/psub_example", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/payments/subscriptions/psub_example",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"subscription": {
"id": "psub_example",
"organizationId": "org_example",
"projectId": "prj_example",
"planId": "plan_example",
"counterpartyId": "counterparty_example",
"subscriberAddress": "So11111111111111111111111111111111111111112",
"subscriberTokenAccount": "So11111111111111111111111111111111111111112",
"subscriptionPda": "So11111111111111111111111111111111111111112",
"subscriptionAuthorityAddress": "So11111111111111111111111111111111111111112",
"authorizationSignature": "example",
"status": "active",
"currentPeriodStartAt": "2025-01-01T00:00:00.000Z",
"nextCollectionDueAt": "2025-01-01T00:00:00.000Z",
"cancelAt": "2025-01-01T00:00:00.000Z",
"canceledAt": "2025-01-01T00:00:00.000Z",
"createdBy": "example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.read",
"details": {
"action": "recurring.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### List subscription collection attempts
Source: https://platform.solana.com/docs/reference/api/payments/list-payment-subscription-collection-attempts
> Lists collection attempts for a recurring-payment subscription.
`GET /v1/payments/subscriptions/{subscriptionId}/collection-attempts`
Requires an [API key](/docs/developing-with-sdp/authentication) with the `recurring.read` action.
## Path parameters
- **`subscriptionId`** · `string` · required · min length 1 · example `psub_example`
SDP subscription record ID.
## Query parameters
- **`status`** · `string` · one of `pending`, `processing`, `confirmed`, `failed`, `skipped` · example `pending`
Collection attempt status.
- **`page`** · `integer` · greater than 0 · default `1`
- **`pageSize`** · `integer` · min 1 · max 100 · default `20`
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Collection attempt list
- **`data`** · `object` · required
Collection attempt list response payload.
- **`collectionAttempts`** · `object[]` · required
- **`id`** · `string` · required · example `psca_example`
Collection attempt ID.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`subscriptionId`** · `string` · required
Subscription ID.
- **`transferId`** · `string | null` · required · min length 1 · example `xfr_example`
Payment transfer record ID when linked.
- **`token`** · `string` · required
Token mint or normalized token identifier.
- **`amount`** · `string` · required · example `100.00`
Token amount in UI units (decimal string).
- **`dueAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`attemptedAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`status`** · `string` · required · one of `pending`, `processing`, `confirmed`, `failed`, `skipped` · example `pending`
Collection attempt status.
- **`signature`** · `string | null` · required
Solana transaction signature.
- **`error`** · `string | null` · required · example `The wallet used for this payment does not have enough funds. Add funds and try again.`
Why the collection failed, if it did. A source wallet that cannot cover the amount records the same text the collect request returned with `400 TRANSACTION_FAILED`.
- **`metadata`** · `object` · required
Provider/job metadata for the attempt.
One of:
- **source: "manual"**
- **`recurringPaymentId`** · `string` · required
- **`initiatedByKeyId`** · `string | null` · required
- **`source`** · `string` · required · always `manual`
- **source: "automated"**
- **`recurringPaymentId`** · `string` · required
- **`initiatedByKeyId`** · `string | null` · required
- **`source`** · `string` · required · always `automated`
- **source: "retry"**
- **`recurringPaymentId`** · `string` · required
- **`initiatedByKeyId`** · `string | null` · required
- **`source`** · `string` · required · always `retry`
- **`initialSource`** · `string` · required · one of `manual`, `automated`
- **`transferId`** · `string | null` · required
- **`error`** · `string` · required
- **`retryAfterAt`** · `string` · required
- **source: "linked_transfer"**
- **`recurringPaymentId`** · `string` · required
- **`initiatedByKeyId`** · `string | null` · required
- **`source`** · `string` · required · always `linked_transfer`
- **`initialSource`** · `string` · required · one of `manual`, `automated`
- **`transferId`** · `string` · required
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`total`** · `integer` · required
- **`page`** · `integer` · required
- **`pageSize`** · `integer` · required
- **`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 `recurring.read` 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`.
- **`500`** Internal Server Error
Same fields as `400`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/payments/subscriptions/psub_example/collection-attempts' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/payments/subscriptions/psub_example/collection-attempts", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/payments/subscriptions/psub_example/collection-attempts",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"collectionAttempts": [
{
"id": "psca_example",
"organizationId": "org_example",
"projectId": "prj_example",
"subscriptionId": "subscription_example",
"transferId": "xfr_example",
"token": "example",
"amount": "100.00",
"dueAt": "2025-01-01T00:00:00.000Z",
"attemptedAt": "2025-01-01T00:00:00.000Z",
"status": "pending",
"signature": "example",
"error": "The wallet used for this payment does not have enough funds. Add funds and try again.",
"metadata": {
"recurringPaymentId": "recurringpayment_example",
"initiatedByKeyId": "initiatedbykey_example",
"source": "manual"
},
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
],
"total": 1,
"page": 1,
"pageSize": 1
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: recurring.read",
"details": {
"action": "recurring.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
## Counterparties
### Counterparties
Source: https://platform.solana.com/docs/reference/api/counterparties
> A counterparty is a person or business you pay or get paid by. You save their details and payment accounts once, then reuse them in payments.
## Profiles
| Method | Endpoint | Summary |
| --- | --- | --- |
| `GET` | `/v1/counterparties/metadata` | [Get counterparty field options](/docs/reference/api/counterparties/get-counterparty-field-options.md) |
| `GET` | `/v1/counterparties` | [List counterparties](/docs/reference/api/counterparties/list-counterparties.md) |
| `POST` | `/v1/counterparties` | [Create counterparty](/docs/reference/api/counterparties/create-counterparty.md) |
| `GET` | `/v1/counterparties/{counterpartyId}` | [Get counterparty](/docs/reference/api/counterparties/get-counterparty.md) |
| `PATCH` | `/v1/counterparties/{counterpartyId}` | [Update counterparty](/docs/reference/api/counterparties/update-counterparty.md) |
| `DELETE` | `/v1/counterparties/{counterpartyId}` | [Archive counterparty](/docs/reference/api/counterparties/archive-counterparty.md) |
## Accounts
| Method | Endpoint | Summary |
| --- | --- | --- |
| `GET` | `/v1/counterparties/{counterpartyId}/accounts` | [List counterparty accounts](/docs/reference/api/counterparties/list-counterparty-accounts.md) |
| `POST` | `/v1/counterparties/{counterpartyId}/accounts` | [Create counterparty account](/docs/reference/api/counterparties/create-counterparty-account.md) |
| `GET` | `/v1/counterparties/{counterpartyId}/accounts/{counterpartyAccountId}` | [Get counterparty account](/docs/reference/api/counterparties/get-counterparty-account.md) |
| `PATCH` | `/v1/counterparties/{counterpartyId}/accounts/{counterpartyAccountId}` | [Update counterparty account](/docs/reference/api/counterparties/update-counterparty-account.md) |
| `DELETE` | `/v1/counterparties/{counterpartyId}/accounts/{counterpartyAccountId}` | [Archive counterparty account](/docs/reference/api/counterparties/archive-counterparty-account.md) |
---
## Counterparties: Profiles
### Get counterparty field options
Source: https://platform.solana.com/docs/reference/api/counterparties/get-counterparty-field-options
> Returns the enum option sets and country list needed to build a counterparty form.
`GET /v1/counterparties/metadata`
Requires an [API key](/docs/developing-with-sdp/authentication) with the `counterparties.read` action.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Counterparty field options
- **`data`** · `object` · required
Field option sets for building a counterparty form: closed enums plus the country list.
- **`fields`** · `object` · required
- **`entityTypes`** · `string[]` · required · each one of `individual`, `business`
- **`countries`** · `object[]` · required
- **`code`** · `string` · required · example `US`
ISO 3166-1 alpha-2 code.
- **`name`** · `string` · required · example `United States`
English display name.
- **`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.
- **`401`** Unauthorized
- **`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.
- **`403`** Forbidden. Requires the `counterparties.read` 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 `401`.
- **`500`** Internal Server Error
Same fields as `401`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/counterparties/metadata' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/counterparties/metadata", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/counterparties/metadata",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"fields": {
"entityTypes": [
"individual"
],
"countries": [
{
"code": "US",
"name": "United States"
}
]
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: counterparties.read",
"details": {
"action": "counterparties.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### List counterparties
Source: https://platform.solana.com/docs/reference/api/counterparties/list-counterparties
> Lists counterparties for the authenticated organization.
`GET /v1/counterparties`
Requires an [API key](/docs/developing-with-sdp/authentication) with the `counterparties.read` action.
## Query parameters
- **`page`** · `integer` · greater than 0 · default `1` · example `1`
Page number (1-based).
- **`pageSize`** · `integer` · min 1 · max 100 · default `20` · example `20`
Items per page (max 100).
- **`includeArchived`** · `boolean` · example `false`
Include archived counterparties in results.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Counterparties list
- **`data`** · `object` · required
Paginated list of counterparties.
- **`counterparties`** · `object[]` · required
Counterparties.
- **`id`** · `string` · required · min length 1 · example `cpty_example`
Counterparty identifier.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string | null` · required · min length 1 · example `prj_example`
Project scope when the counterparty is project-scoped.
- **`externalId`** · `string | null` · required · example `customer_42`
Caller-supplied opaque identifier for cross-system reference. Do not place personal data in this indexed field.
- **`entityType`** · `string` · required · one of `individual`, `business` · example `individual`
Counterparty entity type.
- **`displayName`** · `string` · required · example `Jane Doe`
Human-readable, searchable display name. Keep it minimal because this indexed field is not application-encrypted.
- **`status`** · `string` · required · one of `active`, `archived` · example `active`
Counterparty status.
- **`createdBy`** · `string | null` · required · min length 1 · example `usr_example`
User who created the counterparty. Null when created via API key.
- **`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.
- **`total`** · `integer` · required · min 0 · example `42`
Total counterparties matching the query.
- **`page`** · `integer` · required · greater than 0 · example `1`
Current page number.
- **`pageSize`** · `integer` · required · greater than 0 · example `20`
Items per page.
- **`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 `counterparties.read` 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`.
- **`500`** Internal Server Error
Same fields as `400`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/counterparties' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/counterparties", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/counterparties",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"counterparties": [
{
"id": "cpty_example",
"organizationId": "org_example",
"projectId": "prj_example",
"externalId": "customer_42",
"entityType": "individual",
"displayName": "Jane Doe",
"status": "active",
"createdBy": "usr_example",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-02T00:00:00.000Z"
}
],
"total": 42,
"page": 1,
"pageSize": 20
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: counterparties.read",
"details": {
"action": "counterparties.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Create counterparty
Source: https://platform.solana.com/docs/reference/api/counterparties/create-counterparty
> Creates a counterparty.
`POST /v1/counterparties`
If externalId is provided, it must be unique within the organization.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `counterparties.manage` 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`
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 counterparty request body.
- **`externalId`** · `string` · min length 1 · max length 256 · example `customer_42`
Caller-supplied opaque identifier for cross-system reference. Do not place personal data in this indexed field.
- **`entityType`** · `string` · required · one of `individual`, `business` · example `individual`
Counterparty entity type.
- **`displayName`** · `string` · required · min length 1 · max length 512 · example `Jane Doe`
Human-readable, searchable display name. Keep it minimal because this indexed field is not application-encrypted.
## Responses
- **`201`** Counterparty created
- **`data`** · `object` · required
Counterparty response payload.
- **`counterparty`** · `object` · required
Counterparty record.
- **`id`** · `string` · required · min length 1 · example `cpty_example`
Counterparty identifier.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string | null` · required · min length 1 · example `prj_example`
Project scope when the counterparty is project-scoped.
- **`externalId`** · `string | null` · required · example `customer_42`
Caller-supplied opaque identifier for cross-system reference. Do not place personal data in this indexed field.
- **`entityType`** · `string` · required · one of `individual`, `business` · example `individual`
Counterparty entity type.
- **`displayName`** · `string` · required · example `Jane Doe`
Human-readable, searchable display name. Keep it minimal because this indexed field is not application-encrypted.
- **`status`** · `string` · required · one of `active`, `archived` · example `active`
Counterparty status.
- **`createdBy`** · `string | null` · required · min length 1 · example `usr_example`
User who created the counterparty. Null when created via API key.
- **`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 `counterparties.manage` 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`.
- **`409`** Conflict
Same fields as `400`.
- **`422`** Unprocessable Entity
Same fields as `400`.
- **`500`** Internal Server Error
Same fields as `400`.
## Example request
```bash tab="cURL"
IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST 'https://api.solana.com/v1/counterparties' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalId": "customer_42",
"entityType": "individual",
"displayName": "Jane Doe"
}'
```
```ts tab="TypeScript"
// Reuse this key when you retry this request.
const idempotencyKey = crypto.randomUUID();
const res = await fetch("https://api.solana.com/v1/counterparties", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
"externalId": "customer_42",
"entityType": "individual",
"displayName": "Jane Doe"
})
});
```
```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/counterparties",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
json={
"externalId": "customer_42",
"entityType": "individual",
"displayName": "Jane Doe"
})
```
## Example responses
```json tab="201"
{
"data": {
"counterparty": {
"id": "cpty_example",
"organizationId": "org_example",
"projectId": "prj_example",
"externalId": "customer_42",
"entityType": "individual",
"displayName": "Jane Doe",
"status": "active",
"createdBy": "usr_example",
"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: counterparties.manage",
"details": {
"action": "counterparties.manage"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Get counterparty
Source: https://platform.solana.com/docs/reference/api/counterparties/get-counterparty
> Gets counterparty details by id.
`GET /v1/counterparties/{counterpartyId}`
Requires an [API key](/docs/developing-with-sdp/authentication) with the `counterparties.read` action.
## Path parameters
- **`counterpartyId`** · `string` · required · min length 1 · example `cpty_example`
Counterparty identifier.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Counterparty
- **`data`** · `object` · required
Counterparty response payload.
- **`counterparty`** · `object` · required
Counterparty record.
- **`id`** · `string` · required · min length 1 · example `cpty_example`
Counterparty identifier.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string | null` · required · min length 1 · example `prj_example`
Project scope when the counterparty is project-scoped.
- **`externalId`** · `string | null` · required · example `customer_42`
Caller-supplied opaque identifier for cross-system reference. Do not place personal data in this indexed field.
- **`entityType`** · `string` · required · one of `individual`, `business` · example `individual`
Counterparty entity type.
- **`displayName`** · `string` · required · example `Jane Doe`
Human-readable, searchable display name. Keep it minimal because this indexed field is not application-encrypted.
- **`status`** · `string` · required · one of `active`, `archived` · example `active`
Counterparty status.
- **`createdBy`** · `string | null` · required · min length 1 · example `usr_example`
User who created the counterparty. Null when created via API key.
- **`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.
- **`401`** Unauthorized
- **`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.
- **`403`** Forbidden. Requires the `counterparties.read` 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 `401`.
- **`404`** Not Found
Same fields as `401`.
- **`500`** Internal Server Error
Same fields as `401`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/counterparties/cpty_example' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/counterparties/cpty_example", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/counterparties/cpty_example",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"counterparty": {
"id": "cpty_example",
"organizationId": "org_example",
"projectId": "prj_example",
"externalId": "customer_42",
"entityType": "individual",
"displayName": "Jane Doe",
"status": "active",
"createdBy": "usr_example",
"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: counterparties.read",
"details": {
"action": "counterparties.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Update counterparty
Source: https://platform.solana.com/docs/reference/api/counterparties/update-counterparty
> Updates counterparty attributes.
`PATCH /v1/counterparties/{counterpartyId}`
At least one field must be provided. Use null on externalId to clear it.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `counterparties.manage` action.
## Path parameters
- **`counterpartyId`** · `string` · required · min length 1 · example `cpty_example`
Counterparty identifier.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
- **`Idempotency-Key`** · `string`
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
Update counterparty request body. At least one field must be provided.
- **`externalId`** · `string | null` · min length 1 · max length 256 · example `customer_42`
Updated opaque external ID. Do not include personal data. Use null to clear.
- **`entityType`** · `string` · one of `individual`, `business` · example `business`
Updated counterparty entity type.
- **`displayName`** · `string` · min length 1 · max length 512 · example `Jane Q. Doe`
Updated searchable display name. Keep it minimal.
## Responses
- **`200`** Counterparty updated
- **`data`** · `object` · required
Counterparty response payload.
- **`counterparty`** · `object` · required
Counterparty record.
- **`id`** · `string` · required · min length 1 · example `cpty_example`
Counterparty identifier.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string | null` · required · min length 1 · example `prj_example`
Project scope when the counterparty is project-scoped.
- **`externalId`** · `string | null` · required · example `customer_42`
Caller-supplied opaque identifier for cross-system reference. Do not place personal data in this indexed field.
- **`entityType`** · `string` · required · one of `individual`, `business` · example `individual`
Counterparty entity type.
- **`displayName`** · `string` · required · example `Jane Doe`
Human-readable, searchable display name. Keep it minimal because this indexed field is not application-encrypted.
- **`status`** · `string` · required · one of `active`, `archived` · example `active`
Counterparty status.
- **`createdBy`** · `string | null` · required · min length 1 · example `usr_example`
User who created the counterparty. Null when created via API key.
- **`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 `counterparties.manage` 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`.
## Example request
```bash tab="cURL"
IDEMPOTENCY_KEY=$(uuidgen)
curl -X PATCH 'https://api.solana.com/v1/counterparties/cpty_example' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalId": "customer_42",
"entityType": "business",
"displayName": "Jane Q. Doe"
}'
```
```ts tab="TypeScript"
// Reuse this key when you retry this request.
const idempotencyKey = crypto.randomUUID();
const res = await fetch("https://api.solana.com/v1/counterparties/cpty_example", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
"externalId": "customer_42",
"entityType": "business",
"displayName": "Jane Q. Doe"
})
});
```
```python tab="Python"
import os, requests, uuid
# Reuse this key when you retry this request.
idempotency_key = str(uuid.uuid4())
r = requests.patch("https://api.solana.com/v1/counterparties/cpty_example",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
json={
"externalId": "customer_42",
"entityType": "business",
"displayName": "Jane Q. Doe"
})
```
## Example responses
```json tab="200"
{
"data": {
"counterparty": {
"id": "cpty_example",
"organizationId": "org_example",
"projectId": "prj_example",
"externalId": "customer_42",
"entityType": "individual",
"displayName": "Jane Doe",
"status": "active",
"createdBy": "usr_example",
"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: counterparties.manage",
"details": {
"action": "counterparties.manage"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Archive counterparty
Source: https://platform.solana.com/docs/reference/api/counterparties/archive-counterparty
> Archives a counterparty and its active accounts and provider links atomically.
`DELETE /v1/counterparties/{counterpartyId}`
Archived counterparties are hidden from default lists. New transfers and batches cannot use those accounts. Recurring payments pause before their next new collection; work already in flight keeps its recovery path.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `counterparties.manage` action.
## Path parameters
- **`counterpartyId`** · `string` · required · min length 1 · example `cpty_example`
Counterparty identifier.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
- **`Idempotency-Key`** · `string`
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).
## Responses
- **`204`** Counterparty archived
- **`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 `counterparties.manage` 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`.
## Example request
```bash tab="cURL"
IDEMPOTENCY_KEY=$(uuidgen)
curl -X DELETE 'https://api.solana.com/v1/counterparties/cpty_example' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY"
```
```ts tab="TypeScript"
// Reuse this key when you retry this request.
const idempotencyKey = crypto.randomUUID();
const res = await fetch("https://api.solana.com/v1/counterparties/cpty_example", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey
}
});
```
```python tab="Python"
import os, requests, uuid
# Reuse this key when you retry this request.
idempotency_key = str(uuid.uuid4())
r = requests.delete("https://api.solana.com/v1/counterparties/cpty_example",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key})
```
## Example responses
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: counterparties.manage",
"details": {
"action": "counterparties.manage"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
## Counterparties: Accounts
### List counterparty accounts
Source: https://platform.solana.com/docs/reference/api/counterparties/list-counterparty-accounts
> Lists payment accounts for a counterparty.
`GET /v1/counterparties/{counterpartyId}/accounts`
Crypto-wallet accounts can be used by payment flows that send funds to a counterparty wallet.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `counterparties.read` action.
## Path parameters
- **`counterpartyId`** · `string` · required · min length 1 · example `cpty_example`
Counterparty identifier.
## Query parameters
- **`page`** · `integer` · greater than 0 · default `1` · example `1`
Page number (1-based).
- **`pageSize`** · `integer` · min 1 · max 100 · default `20` · example `20`
Items per page (max 100).
- **`includeArchived`** · `boolean` · example `false`
Include archived counterparty accounts in results.
- **`accountKind`** · `string` · always `crypto_wallet` · example `crypto_wallet`
Filter accounts by account kind.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Counterparty account list
- **`data`** · `object` · required
Paginated list of counterparty accounts.
- **`accounts`** · `object[]` · required
Counterparty accounts.
- **`id`** · `string` · required · example `cpacct_example`
Counterparty account identifier.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`counterpartyId`** · `string` · required · min length 1 · example `cpty_example`
Counterparty identifier.
- **`accountKind`** · `string` · required · always `crypto_wallet` · example `crypto_wallet`
Counterparty account kind.
- **`label`** · `string | null` · required · example `USDC wallet`
Optional human-readable account label.
- **`details`** · `object` · required
Account details. For crypto_wallet accounts, include network: "solana" and address as a Solana wallet address.
- **`*`** · `any`
- **`providerAccountData`** · `object` · required
Provider-specific account metadata preserved by SDP.
- **`*`** · `any`
- **`status`** · `string` · required · one of `active`, `archived` · example `active`
Counterparty account status.
- **`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.
- **`total`** · `integer` · required · min 0 · example `2`
Total counterparty accounts matching the query.
- **`page`** · `integer` · required · greater than 0 · example `1`
Current page number.
- **`pageSize`** · `integer` · required · greater than 0 · example `20`
Items per page.
- **`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 `counterparties.read` 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`.
- **`500`** Internal Server Error
Same fields as `400`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/counterparties/cpty_example/accounts' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/counterparties/cpty_example/accounts", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/counterparties/cpty_example/accounts",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"accounts": [
{
"id": "cpacct_example",
"organizationId": "org_example",
"projectId": "prj_example",
"counterpartyId": "cpty_example",
"accountKind": "crypto_wallet",
"label": "USDC wallet",
"details": {
"network": "solana",
"address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
},
"providerAccountData": {},
"status": "active",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-02T00:00:00.000Z"
}
],
"total": 2,
"page": 1,
"pageSize": 20
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: counterparties.read",
"details": {
"action": "counterparties.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Create counterparty account
Source: https://platform.solana.com/docs/reference/api/counterparties/create-counterparty-account
> Creates a payment account for a counterparty.
`POST /v1/counterparties/{counterpartyId}/accounts`
For accountKind "crypto_wallet", details.network must be "solana" and details.address must be a Solana wallet address. A counterparty holds one active account per network and address: an address already on one of its active accounts answers 409 `CONFLICT` ("This address is already on this counterparty"). An archived account does not count, so re-adding its address creates a new account.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `counterparties.execute` action.
## Path parameters
- **`counterpartyId`** · `string` · required · min length 1 · example `cpty_example`
Counterparty identifier.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
- **`Idempotency-Key`** · `string`
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 counterparty account request body.
- **`accountKind`** · `string` · required · always `crypto_wallet` · example `crypto_wallet`
Counterparty account kind.
- **`label`** · `string` · min length 1 · max length 256 · example `USDC wallet`
Optional account label.
- **`details`** · `object`
For crypto_wallet accounts, must include network: "solana" and address as a Solana wallet address.
- **`*`** · `any`
- **`providerAccountData`** · `object`
Provider-specific metadata to preserve with the account.
- **`*`** · `any`
## Responses
- **`201`** Counterparty account created
- **`data`** · `object` · required
Counterparty account response payload.
- **`account`** · `object` · required
Counterparty payment account record.
- **`id`** · `string` · required · example `cpacct_example`
Counterparty account identifier.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`counterpartyId`** · `string` · required · min length 1 · example `cpty_example`
Counterparty identifier.
- **`accountKind`** · `string` · required · always `crypto_wallet` · example `crypto_wallet`
Counterparty account kind.
- **`label`** · `string | null` · required · example `USDC wallet`
Optional human-readable account label.
- **`details`** · `object` · required
Account details. For crypto_wallet accounts, include network: "solana" and address as a Solana wallet address.
- **`*`** · `any`
- **`providerAccountData`** · `object` · required
Provider-specific account metadata preserved by SDP.
- **`*`** · `any`
- **`status`** · `string` · required · one of `active`, `archived` · example `active`
Counterparty account status.
- **`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 `counterparties.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`.
## Example request
```bash tab="cURL"
IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST 'https://api.solana.com/v1/counterparties/cpty_example/accounts' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountKind": "crypto_wallet",
"label": "USDC wallet",
"details": {
"network": "solana",
"address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
},
"providerAccountData": {}
}'
```
```ts tab="TypeScript"
// Reuse this key when you retry this request.
const idempotencyKey = crypto.randomUUID();
const res = await fetch("https://api.solana.com/v1/counterparties/cpty_example/accounts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
"accountKind": "crypto_wallet",
"label": "USDC wallet",
"details": {
"network": "solana",
"address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
},
"providerAccountData": {}
})
});
```
```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/counterparties/cpty_example/accounts",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
json={
"accountKind": "crypto_wallet",
"label": "USDC wallet",
"details": {
"network": "solana",
"address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
},
"providerAccountData": {}
})
```
## Example responses
```json tab="201"
{
"data": {
"account": {
"id": "cpacct_example",
"organizationId": "org_example",
"projectId": "prj_example",
"counterpartyId": "cpty_example",
"accountKind": "crypto_wallet",
"label": "USDC wallet",
"details": {
"network": "solana",
"address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
},
"providerAccountData": {},
"status": "active",
"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: counterparties.execute",
"details": {
"action": "counterparties.execute"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Get counterparty account
Source: https://platform.solana.com/docs/reference/api/counterparties/get-counterparty-account
> Gets a counterparty payment account by id.
`GET /v1/counterparties/{counterpartyId}/accounts/{counterpartyAccountId}`
Requires an [API key](/docs/developing-with-sdp/authentication) with the `counterparties.read` action.
## Path parameters
- **`counterpartyId`** · `string` · required · min length 1 · example `cpty_example`
Counterparty identifier.
- **`counterpartyAccountId`** · `string` · required · min length 1 · example `cpacct_example`
Counterparty account identifier.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Counterparty account
- **`data`** · `object` · required
Counterparty account response payload.
- **`account`** · `object` · required
Counterparty payment account record.
- **`id`** · `string` · required · example `cpacct_example`
Counterparty account identifier.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`counterpartyId`** · `string` · required · min length 1 · example `cpty_example`
Counterparty identifier.
- **`accountKind`** · `string` · required · always `crypto_wallet` · example `crypto_wallet`
Counterparty account kind.
- **`label`** · `string | null` · required · example `USDC wallet`
Optional human-readable account label.
- **`details`** · `object` · required
Account details. For crypto_wallet accounts, include network: "solana" and address as a Solana wallet address.
- **`*`** · `any`
- **`providerAccountData`** · `object` · required
Provider-specific account metadata preserved by SDP.
- **`*`** · `any`
- **`status`** · `string` · required · one of `active`, `archived` · example `active`
Counterparty account status.
- **`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.
- **`401`** Unauthorized
- **`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.
- **`403`** Forbidden. Requires the `counterparties.read` 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 `401`.
- **`404`** Not Found
Same fields as `401`.
- **`500`** Internal Server Error
Same fields as `401`.
## Example request
```bash tab="cURL"
curl 'https://api.solana.com/v1/counterparties/cpty_example/accounts/cpacct_example' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/counterparties/cpty_example/accounts/cpacct_example", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/counterparties/cpty_example/accounts/cpacct_example",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"account": {
"id": "cpacct_example",
"organizationId": "org_example",
"projectId": "prj_example",
"counterpartyId": "cpty_example",
"accountKind": "crypto_wallet",
"label": "USDC wallet",
"details": {
"network": "solana",
"address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
},
"providerAccountData": {},
"status": "active",
"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: counterparties.read",
"details": {
"action": "counterparties.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Update counterparty account
Source: https://platform.solana.com/docs/reference/api/counterparties/update-counterparty-account
> Updates a counterparty payment account.
`PATCH /v1/counterparties/{counterpartyId}/accounts/{counterpartyAccountId}`
At least one field must be provided. Changing details to an address already on another of the counterparty's active accounts answers 409 `CONFLICT`.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `counterparties.execute` action.
## Path parameters
- **`counterpartyId`** · `string` · required · min length 1 · example `cpty_example`
Counterparty identifier.
- **`counterpartyAccountId`** · `string` · required · min length 1 · example `cpacct_example`
Counterparty account identifier.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
- **`Idempotency-Key`** · `string`
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
Update counterparty account request body. At least one field must be provided.
- **`label`** · `string | null` · min length 1 · max length 256 · example `Primary USDC wallet`
Updated account label. Use null to clear.
- **`details`** · `object`
Updated account details. Crypto-wallet accounts must retain network: "solana" and a valid Solana wallet address.
- **`*`** · `any`
- **`providerAccountData`** · `object`
Updated provider-specific metadata.
- **`*`** · `any`
## Responses
- **`200`** Counterparty account updated
- **`data`** · `object` · required
Counterparty account response payload.
- **`account`** · `object` · required
Counterparty payment account record.
- **`id`** · `string` · required · example `cpacct_example`
Counterparty account identifier.
- **`organizationId`** · `string` · required · min length 1 · example `org_example`
Organization identifier.
- **`projectId`** · `string` · required · min length 1 · example `prj_example`
Project identifier.
- **`counterpartyId`** · `string` · required · min length 1 · example `cpty_example`
Counterparty identifier.
- **`accountKind`** · `string` · required · always `crypto_wallet` · example `crypto_wallet`
Counterparty account kind.
- **`label`** · `string | null` · required · example `USDC wallet`
Optional human-readable account label.
- **`details`** · `object` · required
Account details. For crypto_wallet accounts, include network: "solana" and address as a Solana wallet address.
- **`*`** · `any`
- **`providerAccountData`** · `object` · required
Provider-specific account metadata preserved by SDP.
- **`*`** · `any`
- **`status`** · `string` · required · one of `active`, `archived` · example `active`
Counterparty account status.
- **`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 `counterparties.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`.
## Example request
```bash tab="cURL"
IDEMPOTENCY_KEY=$(uuidgen)
curl -X PATCH 'https://api.solana.com/v1/counterparties/cpty_example/accounts/cpacct_example' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "Primary USDC wallet",
"details": {
"network": "solana",
"address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
},
"providerAccountData": {}
}'
```
```ts tab="TypeScript"
// Reuse this key when you retry this request.
const idempotencyKey = crypto.randomUUID();
const res = await fetch("https://api.solana.com/v1/counterparties/cpty_example/accounts/cpacct_example", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
"label": "Primary USDC wallet",
"details": {
"network": "solana",
"address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
},
"providerAccountData": {}
})
});
```
```python tab="Python"
import os, requests, uuid
# Reuse this key when you retry this request.
idempotency_key = str(uuid.uuid4())
r = requests.patch("https://api.solana.com/v1/counterparties/cpty_example/accounts/cpacct_example",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
json={
"label": "Primary USDC wallet",
"details": {
"network": "solana",
"address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
},
"providerAccountData": {}
})
```
## Example responses
```json tab="200"
{
"data": {
"account": {
"id": "cpacct_example",
"organizationId": "org_example",
"projectId": "prj_example",
"counterpartyId": "cpty_example",
"accountKind": "crypto_wallet",
"label": "USDC wallet",
"details": {
"network": "solana",
"address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
},
"providerAccountData": {},
"status": "active",
"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: counterparties.execute",
"details": {
"action": "counterparties.execute"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Archive counterparty account
Source: https://platform.solana.com/docs/reference/api/counterparties/archive-counterparty-account
> Archives a counterparty payment account.
`DELETE /v1/counterparties/{counterpartyId}/accounts/{counterpartyAccountId}`
Archived accounts are hidden from default lists.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `counterparties.manage` action.
## Path parameters
- **`counterpartyId`** · `string` · required · min length 1 · example `cpty_example`
Counterparty identifier.
- **`counterpartyAccountId`** · `string` · required · min length 1 · example `cpacct_example`
Counterparty account identifier.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
- **`Idempotency-Key`** · `string`
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).
## Responses
- **`204`** Counterparty account archived
- **`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 `counterparties.manage` 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`.
## Example request
```bash tab="cURL"
IDEMPOTENCY_KEY=$(uuidgen)
curl -X DELETE 'https://api.solana.com/v1/counterparties/cpty_example/accounts/cpacct_example' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY"
```
```ts tab="TypeScript"
// Reuse this key when you retry this request.
const idempotencyKey = crypto.randomUUID();
const res = await fetch("https://api.solana.com/v1/counterparties/cpty_example/accounts/cpacct_example", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey
}
});
```
```python tab="Python"
import os, requests, uuid
# Reuse this key when you retry this request.
idempotency_key = str(uuid.uuid4())
r = requests.delete("https://api.solana.com/v1/counterparties/cpty_example/accounts/cpacct_example",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key})
```
## Example responses
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: counterparties.manage",
"details": {
"action": "counterparties.manage"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
## Earn
### Earn
Source: https://platform.solana.com/docs/reference/api/earn
> Strategy reads, previews, withdrawal-route discovery, unsigned builds, submission, queued actions, and tenant tracking. Availability follows the deployment's release channel.
> **Earn is under active development.** Features, API endpoints, and docs may change.
## Discovery
| Method | Endpoint | Summary |
| --- | --- | --- |
| `GET` | `/v1/earn/strategies` | [List strategies](/docs/reference/api/earn/list-earn-strategies.md) |
| `GET` | `/v1/earn/strategies/{strategyId}` | [Get a strategy](/docs/reference/api/earn/get-earn-strategy.md) |
| `POST` | `/v1/earn/vault-deposit-previews` | [Preview a deposit](/docs/reference/api/earn/create-earn-vault-deposit-preview.md) |
## Embedded Yield
| Method | Endpoint | Summary |
| --- | --- | --- |
| `GET` | `/v1/earn/external-wallet/positions/summary` | [Get position totals](/docs/reference/api/earn/get-earn-external-wallet-position-summary.md) |
| `GET` | `/v1/earn/external-wallet/positions` | [List positions](/docs/reference/api/earn/list-earn-external-wallet-positions.md) |
| `GET` | `/v1/earn/external-wallet/movements` | [List movements](/docs/reference/api/earn/list-earn-external-wallet-movements.md) |
| `GET` | `/v1/earn/external-wallet/movements/{movementId}` | [Get a movement](/docs/reference/api/earn/get-earn-external-wallet-movement.md) |
| `GET` | `/v1/earn/external-wallet/earnings` | [Get earnings](/docs/reference/api/earn/get-earn-external-wallet-earnings.md) |
| `POST` | `/v1/earn/external-wallet/deposit-transactions` | [Build a deposit transaction](/docs/reference/api/earn/create-earn-external-wallet-deposit-transaction.md) |
| `POST` | `/v1/earn/external-wallet/deposits` | [Submit a deposit](/docs/reference/api/earn/create-earn-external-wallet-deposit.md) |
| `POST` | `/v1/earn/external-wallet/withdrawal-previews` | [Preview a withdrawal](/docs/reference/api/earn/create-earn-external-wallet-withdrawal-preview.md) |
| `POST` | `/v1/earn/external-wallet/withdrawal-transactions` | [Build a withdrawal transaction](/docs/reference/api/earn/create-earn-external-wallet-withdrawal-transaction.md) |
| `POST` | `/v1/earn/external-wallet/withdrawals` | [Submit a withdrawal](/docs/reference/api/earn/create-earn-external-wallet-withdrawal.md) |
| `POST` | `/v1/earn/external-wallet/withdrawal-options` | [List withdrawal options](/docs/reference/api/earn/get-earn-external-wallet-withdrawal-options.md) |
| `POST` | `/v1/earn/external-wallet/queued-withdrawal-previews` | [Preview a withdrawal request](/docs/reference/api/earn/create-earn-external-wallet-queued-withdrawal-preview.md) |
| `POST` | `/v1/earn/external-wallet/withdrawal-request-transactions` | [Build a withdrawal request transaction](/docs/reference/api/earn/create-earn-external-wallet-withdrawal-request-transaction.md) |
| `POST` | `/v1/earn/external-wallet/withdrawal-requests` | [Submit a withdrawal request](/docs/reference/api/earn/create-earn-external-wallet-withdrawal-request.md) |
| `GET` | `/v1/earn/external-wallet/withdrawal-requests` | [List withdrawal requests](/docs/reference/api/earn/list-earn-external-wallet-withdrawal-requests.md) |
| `GET` | `/v1/earn/external-wallet/withdrawal-requests/{withdrawalRequestId}` | [Get a withdrawal request](/docs/reference/api/earn/get-earn-external-wallet-withdrawal-request.md) |
| `POST` | `/v1/earn/external-wallet/withdrawal-request-cancel-transactions` | [Build a cancellation transaction](/docs/reference/api/earn/create-earn-external-wallet-withdrawal-request-cancel-transaction.md) |
| `POST` | `/v1/earn/external-wallet/withdrawal-request-cancellations` | [Submit a cancellation](/docs/reference/api/earn/create-earn-external-wallet-withdrawal-request-cancellation.md) |
---
## Earn: Discovery
### List strategies
Source: https://platform.solana.com/docs/reference/api/earn/list-earn-strategies
> Lists the strategies your project can see, largest first by total value locked.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`GET /v1/earn/strategies`
The list defaults to your project's cluster: devnet for sandbox, mainnet for production. Pass `cluster` to browse the other cluster's strategies; those rows are always `fundable: false`. A listed strategy is not always open for deposits, so check `fundable` and `status` before you offer it.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `earn.read` action.
## Query parameters
- **`page`** · `integer` · greater than 0 · default `1`
- **`pageSize`** · `integer` · min 1 · max 100 · default `20`
- **`sourceKind`** · `string` · one of `defi`, `rwa`
- **`apyType`** · `string` · one of `variable`, `fixed`
- **`liquidityTerm`** · `string` · one of `instant`, `delayed`
- **`cluster`** · `string` · one of `devnet`, `mainnet-beta`
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** One page of strategies
- **`data`** · `object` · required
- **`strategies`** · `object[]` · required
- **`id`** · `string` · required · example `earn_strategy_3b6a81ce-00b0-4c5a-9761-cb359932e97e`
The strategy's ID. Pass it as `strategyId` to preview or build a deposit.
- **`provider`** · `string` · required · example `kamino`
Open provider id (e.g. `kamino`, `veda`).
- **`providerReference`** · `string` · required · example `7uib8xGAwkaPz4ZGCA6t8sSEid5Yp9ty13PHUweTypx`
The strategy's onchain address at the provider, such as a vault. It identifies the instrument; never send funds to it.
- **`name`** · `string` · required · example `Allez USDC`
- **`sourceKind`** · `string` · required · one of `defi`, `rwa`
Where the yield comes from: onchain DeFi or real-world assets.
- **`underlyingSource`** · `string` · example `kamino`
Open id of the underlying protocol or fund, when the provider reports one.
- **`depositMints`** · `string[]` · required
Mints the strategy accepts directly. The first is the deposit token.
- **`shareMint`** · `string` · example `hXm2xSRF5PLKGMrTvAWqKhR76MuJX5dAabeSChkjqu2`
The share token a deposit mints, when the strategy has one.
- **`apyType`** · `string` · required · one of `variable`, `fixed`
- **`currentApy`** · `string` · example `0.062`
Latest observed APY as a decimal ratio string (`"0.062"` = 6.2%), refreshed about every five minutes. Absent when the provider reports none.
- **`liquidityTerm`** · `string` · required · one of `instant`, `delayed`
- **`redemptionDelayDays`** · `number` · example `2`
For `delayed` liquidity: the provider's stated redemption delay.
- **`riskMetadata`** · `object`
Curator and risk framework metadata, as published by the provider. Curators publish heterogeneous frameworks, so this is an open shape.
- **`curator`** · `string` · example `allez`
- **`riskTier`** · `string`
- **`frameworkUrl`** · `string`
- **`*`** · `any`
- **`status`** · `string` · required · one of `active`, `paused`, `deprecated`
The strategy's lifecycle. Only `active` strategies accept new deposits.
- **`depositSlippage`** · `object | null` · required
Not null when a deposit build for this strategy, in your environment, requires `minSharesOut`. Preview the deposit first and derive the floor from `sharesOut` minus your tolerance. Most production strategies are not null; a strategy priced later, such as a provider order, may be null because no onchain instruction can enforce a floor. Answered per request, like `fundable`.
- **`quoteRequired`** · `boolean` · required · always `true`
The live quote endpoint must be called before building this direction.
- **`defaultToleranceBps`** · `integer` · required · example `50`
Suggested tolerance in basis points, to take off a preview's quote when you compute the floor. Your customer may choose another.
- **`withdrawalSlippage`** · `object | null` · required
Non-null when this provider's withdrawal builder refuses to run without an explicit `minAmountOut`: call the withdrawal preview first and derive the floor from `assetsOut`. Null when this withdrawal cannot enforce a floor; omit `minAmountOut`.
- **`quoteRequired`** · `boolean` · required · always `true`
The live quote endpoint must be called before building this direction.
- **`defaultToleranceBps`** · `integer` · required · example `50`
Suggested tolerance in basis points, to take off a preview's quote when you compute the floor. Your customer may choose another.
- **`hostCluster`** · `string` · required · one of `devnet`, `mainnet-beta`
The cluster the strategy runs on.
- **`fundable`** · `boolean` · required
Whether the strategy exists on your project's cluster. `false` means a deposit can never work from this project. `true` is necessary but not sufficient: the strategy must also be `active`, and your organization must be activated for the provider.
- **`feeSponsored`** · `boolean` · required
Whether SDP pays network fees for Treasury movements on this strategy from your environment. It never applies to a customer's wallet: to sponsor an Embedded Yield transaction, pass `feePayer` when you build it. Always `false` when the strategy is not `fundable`, and a swap-funded deposit is never sponsored.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`total`** · `integer` · required · example `6`
Total rows visible to the caller across all pages.
- **`page`** · `integer` · required · example `1`
- **`pageSize`** · `integer` · required · example `20`
- **`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.read` 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`.
- **`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"
curl 'https://api.solana.com/v1/earn/strategies' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/earn/strategies", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/earn/strategies",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"strategies": [
{
"id": "earn_strategy_8d1f0c52-6a3e-4b7e-9f21-3c5d7a9e4b10",
"provider": "kamino",
"providerReference": "BEEfo7xwgK2ZP13Pxo7qqTPzAteKJmXjVWtMWcXSvbn2",
"name": "Steakhouse High Yield USDC",
"sourceKind": "defi",
"underlyingSource": "klend",
"depositMints": [
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
],
"shareMint": "FU2tGk7tFHEhZiZ516TKgpPqPwXYXHTeRHqvuDwa56UV",
"apyType": "variable",
"currentApy": "0.045113",
"liquidityTerm": "instant",
"riskMetadata": {
"managementFeeBps": 0,
"performanceFeeBps": 500
},
"status": "active",
"depositSlippage": {
"quoteRequired": true,
"defaultToleranceBps": 10
},
"withdrawalSlippage": null,
"hostCluster": "mainnet-beta",
"fundable": true,
"feeSponsored": false,
"createdAt": "2026-11-04T00:17:15.491Z",
"updatedAt": "2026-11-06T17:52:10.042Z"
}
],
"total": 6,
"page": 1,
"pageSize": 20
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: earn.read",
"details": {
"action": "earn.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Get a strategy
Source: https://platform.solana.com/docs/reference/api/earn/get-earn-strategy
> Gets one strategy by ID, on either cluster.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`GET /v1/earn/strategies/{strategyId}`
`fundable` still reports whether the strategy exists on your project's cluster.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `earn.read` action.
## Path parameters
- **`strategyId`** · `string` · required · min length 1 · example `earn_strategy_8d1f0c52-6a3e-4b7e-9f21-3c5d7a9e4b10`
The strategy's `id` from the catalog.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** The strategy
- **`data`** · `object` · required
- **`strategy`** · `object` · required
One strategy in the catalog.
- **`id`** · `string` · required · example `earn_strategy_3b6a81ce-00b0-4c5a-9761-cb359932e97e`
The strategy's ID. Pass it as `strategyId` to preview or build a deposit.
- **`provider`** · `string` · required · example `kamino`
Open provider id (e.g. `kamino`, `veda`).
- **`providerReference`** · `string` · required · example `7uib8xGAwkaPz4ZGCA6t8sSEid5Yp9ty13PHUweTypx`
The strategy's onchain address at the provider, such as a vault. It identifies the instrument; never send funds to it.
- **`name`** · `string` · required · example `Allez USDC`
- **`sourceKind`** · `string` · required · one of `defi`, `rwa`
Where the yield comes from: onchain DeFi or real-world assets.
- **`underlyingSource`** · `string` · example `kamino`
Open id of the underlying protocol or fund, when the provider reports one.
- **`depositMints`** · `string[]` · required
Mints the strategy accepts directly. The first is the deposit token.
- **`shareMint`** · `string` · example `hXm2xSRF5PLKGMrTvAWqKhR76MuJX5dAabeSChkjqu2`
The share token a deposit mints, when the strategy has one.
- **`apyType`** · `string` · required · one of `variable`, `fixed`
- **`currentApy`** · `string` · example `0.062`
Latest observed APY as a decimal ratio string (`"0.062"` = 6.2%), refreshed about every five minutes. Absent when the provider reports none.
- **`liquidityTerm`** · `string` · required · one of `instant`, `delayed`
- **`redemptionDelayDays`** · `number` · example `2`
For `delayed` liquidity: the provider's stated redemption delay.
- **`riskMetadata`** · `object`
Curator and risk framework metadata, as published by the provider. Curators publish heterogeneous frameworks, so this is an open shape.
- **`curator`** · `string` · example `allez`
- **`riskTier`** · `string`
- **`frameworkUrl`** · `string`
- **`*`** · `any`
- **`status`** · `string` · required · one of `active`, `paused`, `deprecated`
The strategy's lifecycle. Only `active` strategies accept new deposits.
- **`depositSlippage`** · `object | null` · required
Not null when a deposit build for this strategy, in your environment, requires `minSharesOut`. Preview the deposit first and derive the floor from `sharesOut` minus your tolerance. Most production strategies are not null; a strategy priced later, such as a provider order, may be null because no onchain instruction can enforce a floor. Answered per request, like `fundable`.
- **`quoteRequired`** · `boolean` · required · always `true`
The live quote endpoint must be called before building this direction.
- **`defaultToleranceBps`** · `integer` · required · example `50`
Suggested tolerance in basis points, to take off a preview's quote when you compute the floor. Your customer may choose another.
- **`withdrawalSlippage`** · `object | null` · required
Non-null when this provider's withdrawal builder refuses to run without an explicit `minAmountOut`: call the withdrawal preview first and derive the floor from `assetsOut`. Null when this withdrawal cannot enforce a floor; omit `minAmountOut`.
- **`quoteRequired`** · `boolean` · required · always `true`
The live quote endpoint must be called before building this direction.
- **`defaultToleranceBps`** · `integer` · required · example `50`
Suggested tolerance in basis points, to take off a preview's quote when you compute the floor. Your customer may choose another.
- **`hostCluster`** · `string` · required · one of `devnet`, `mainnet-beta`
The cluster the strategy runs on.
- **`fundable`** · `boolean` · required
Whether the strategy exists on your project's cluster. `false` means a deposit can never work from this project. `true` is necessary but not sufficient: the strategy must also be `active`, and your organization must be activated for the provider.
- **`feeSponsored`** · `boolean` · required
Whether SDP pays network fees for Treasury movements on this strategy from your environment. It never applies to a customer's wallet: to sponsor an Embedded Yield transaction, pass `feePayer` when you build it. Always `false` when the strategy is not `fundable`, and a swap-funded deposit is never sponsored.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 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 `earn.read` 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`.
- **`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"
curl 'https://api.solana.com/v1/earn/strategies/earn_strategy_8d1f0c52-6a3e-4b7e-9f21-3c5d7a9e4b10' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/earn/strategies/earn_strategy_8d1f0c52-6a3e-4b7e-9f21-3c5d7a9e4b10", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/earn/strategies/earn_strategy_8d1f0c52-6a3e-4b7e-9f21-3c5d7a9e4b10",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"strategy": {
"id": "earn_strategy_8d1f0c52-6a3e-4b7e-9f21-3c5d7a9e4b10",
"provider": "kamino",
"providerReference": "BEEfo7xwgK2ZP13Pxo7qqTPzAteKJmXjVWtMWcXSvbn2",
"name": "Steakhouse High Yield USDC",
"sourceKind": "defi",
"underlyingSource": "klend",
"depositMints": [
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
],
"shareMint": "FU2tGk7tFHEhZiZ516TKgpPqPwXYXHTeRHqvuDwa56UV",
"apyType": "variable",
"currentApy": "0.045113",
"liquidityTerm": "instant",
"riskMetadata": {
"managementFeeBps": 0,
"performanceFeeBps": 500
},
"status": "active",
"depositSlippage": {
"quoteRequired": true,
"defaultToleranceBps": 10
},
"withdrawalSlippage": null,
"hostCluster": "mainnet-beta",
"fundable": true,
"feeSponsored": false,
"createdAt": "2026-11-04T00:17:15.491Z",
"updatedAt": "2026-11-06T17:52:10.042Z"
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: earn.read",
"details": {
"action": "earn.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Preview a deposit
Source: https://platform.solana.com/docs/reference/api/earn/create-earn-vault-deposit-preview
> Quotes how many shares a deposit would receive right now, from the provider's live accounting.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`POST /v1/earn/vault-deposit-previews`
When the strategy's `depositSlippage` is not null, set `minSharesOut` from this quote: take your tolerance off `sharesOut`. Resolve every `blockingIssues` entry before you build. The preview is read-only, takes no Idempotency-Key, and applies a deposit's access checks, provider activation included. It returns 501 when the provider cannot quote deposits.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `earn.read` action.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Request body
`application/json` · required
Quote one direct vault deposit against the provider's live accounting. Read-only: nothing is built and nothing is persisted.
- **`strategyId`** · `string` · required · min length 1 · example `earn_strategy_3b6a81ce-00b0-4c5a-9761-cb359932e97e`
- **`amount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Deposit amount in deposit-token units.
## Responses
- **`200`** Live deposit quote
- **`data`** · `object` · required
- **`strategyId`** · `string` · required · example `earn_strategy_3b6a81ce-00b0-4c5a-9761-cb359932e97e`
- **`sharesOut`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `24.98`
Shares the deposit would mint at the live rate. Derive `minSharesOut` from this value minus the chosen tolerance, quantized to `shareDecimals`.
- **`shareDecimals`** · `integer` · required · example `6`
The share mint's decimals, which the floor must be quantized to.
- **`blockingIssues`** · `object[]` · required
Conditions the provider reports would block this deposit; empty when none.
- **`code`** · `string` · required · example `SHARE_LOCKED`
- **`message`** · `string` · required
- **`feeSponsored`** · `boolean` · required
Whether SDP pays network fees for Treasury movements on this cluster. It does not apply to customer wallets: to sponsor an Embedded Yield transaction, pass `feePayer` when you build it.
- **`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.read` 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`.
- **`429`** Too Many Requests
Same fields as `400`.
- **`500`** Internal Server Error
Same fields as `400`.
- **`501`** The provider does not support this operation.
- **`error`** · `object` · required
Standard error payload.
- **`code`** · `string` · required
- **`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.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
curl -X POST 'https://api.solana.com/v1/earn/vault-deposit-previews' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"strategyId": "earn_strategy_3b6a81ce-00b0-4c5a-9761-cb359932e97e",
"amount": "25"
}'
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/earn/vault-deposit-previews", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"strategyId": "earn_strategy_3b6a81ce-00b0-4c5a-9761-cb359932e97e",
"amount": "25"
})
});
```
```python tab="Python"
import os, requests
r = requests.post("https://api.solana.com/v1/earn/vault-deposit-previews",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"},
json={
"strategyId": "earn_strategy_3b6a81ce-00b0-4c5a-9761-cb359932e97e",
"amount": "25"
})
```
## Example responses
```json tab="200"
{
"data": {
"strategyId": "earn_strategy_3b6a81ce-00b0-4c5a-9761-cb359932e97e",
"sharesOut": "24.975",
"shareDecimals": 6,
"blockingIssues": [],
"feeSponsored": false
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: earn.read",
"details": {
"action": "earn.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
## Earn: Embedded Yield
### Get position totals
Source: https://platform.solana.com/docs/reference/api/earn/get-earn-external-wallet-position-summary
> Gets live position totals across every customer wallet in your project, grouped by strategy and token.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`GET /v1/earn/external-wallet/positions/summary`
Wallet addresses are left out unless you pass `includeOwnerAddresses=true`; add `includePositions=true` to also receive each wallet's positions, which saves one read per wallet. A total that depends on an unavailable live value is left out, never reported as zero or partial.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `earn.read` action.
## Query parameters
- **`includeOwnerAddresses`** · `string` · one of `true`, `false`
- **`includePositions`** · `string` · one of `true`, `false`
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Position totals
- **`data`** · `object` · required
- **`summary`** · `object` · required
- **`walletCount`** · `integer` · required · min 0
- **`positionCount`** · `integer` · required · min 0
- **`unavailablePositionCount`** · `integer` · required · min 0
- **`totalsByStrategy`** · `object[]` · required
- **`provider`** · `string` · required · example `kamino`
- **`providerReference`** · `string` · required · example `7uib8xGAwkaPz4ZGCA6t8sSEid5Yp9ty13PHUweTypx`
- **`label`** · `string` · required · example `Allez USDC`
- **`ownerAddresses`** · `string[]` · each min length 32 · each max length 44 · each pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · each example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer wallets behind this total. Absent unless the request passes `includeOwnerAddresses=true`. The list is personal data about your customers, so opt in only where you show per-customer detail.
- **`positions`** · `object[]`
Complete live positions contributing to this strategy total. Present only when the request passes `includePositions=true` together with `includeOwnerAddresses=true`; use this single drill-down instead of one live request per owner.
- **`id`** · `string` · required · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
- **`ownerAddress`** · `string` · required · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address.
- **`provider`** · `string` · required · example `kamino`
- **`providerReference`** · `string` · required · example `7uib8xGAwkaPz4ZGCA6t8sSEid5Yp9ty13PHUweTypx`
The strategy's onchain address.
- **`label`** · `string` · required · example `Allez USDC`
- **`tokenMint`** · `string` · required · example `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`
- **`shareMint`** · `string` · required · example `hXm2xSRF5PLKGMrTvAWqKhR76MuJX5dAabeSChkjqu2`
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`closedAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`shares`** · `string` · max length 128 · pattern `^\d+(\.\d+)?$` · example `25.42`
Live share balance. Absent when the live read is unavailable; never zero-filled.
- **`withdrawableShares`** · `string` · max length 128 · pattern `^\d+(\.\d+)?$` · example `25.42`
Shares you can withdraw right now. Absent when the live read is unavailable.
- **`unlockTimestamp`** · `string | null` · pattern `^\d+$` · example `1789722000`
Unix time, in seconds, when locked shares become withdrawable. Null or absent when no lock applies. To tell an unavailable read apart, check `shares`.
- **`tokenValue`** · `string` · max length 128 · pattern `^\d+(\.\d+)?$` · example `25.42`
Live value of the position in the deposit token (`tokenMint`), as the provider reports it: shares times the rate for rate-based strategies, or a withdrawal quote for the whole position for quote-based providers such as Veda and Ondo. A quote already includes spread and price impact, so it does not scale to other share amounts. It is a deposit-token amount, not a share count or a USD conversion; every Earn deposit token is a USD stablecoin, so at par it reads as dollars. Absent when the live read is unavailable.
- **`parIntermediate`** · `object`
The operator redemption's intermediate token the wallet holds outside any open request, such as Hastra wYLDS after a canceled operator redemption. It belongs to this position and is not included in `tokenValue`. Present only while non-zero.
- **`mint`** · `string` · required · example `8fr7WGTVFszfyNWRMXj6fRjZZAnDwmXwEpCrtzmUkdih`
- **`amount`** · `string` · required · max length 128 · pattern `^\d+(\.\d+)?$` · example `25.42`
Live decimal value returned by the vault provider; never a float.
- **`withdrawableAmount`** · `string` · required · max length 128 · pattern `^\d+(\.\d+)?$` · example `25.42`
Zero while the provider has frozen the token account.
- **`tokenValue`** · `string` · required · max length 128 · pattern `^\d+(\.\d+)?$` · example `25.42`
Value in the position's deposit token (`tokenMint`).
- **`walletCount`** · `integer` · required · min 0
- **`positionCount`** · `integer` · required · min 0
- **`totalsByToken`** · `object[]` · required
- **`tokenMint`** · `string` · required · example `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`
- **`walletCount`** · `integer` · required · min 0
- **`positionCount`** · `integer` · required · min 0
- **`unavailablePositionCount`** · `integer` · required · min 0
- **`tokenValue`** · `string` · max length 128 · pattern `^\d+(\.\d+)?$` · example `25.42`
Exact live total in the deposit token (`tokenMint`): the sum of the positions' `tokenValue` plus any `parIntermediate.tokenValue`, a dollar figure at par. Absent when any contributing position is unavailable, so partial money is never presented as complete.
- **`totalsByToken`** · `object[]` · required
- **`tokenMint`** · `string` · required · example `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`
- **`walletCount`** · `integer` · required · min 0
- **`positionCount`** · `integer` · required · min 0
- **`unavailablePositionCount`** · `integer` · required · min 0
- **`tokenValue`** · `string` · max length 128 · pattern `^\d+(\.\d+)?$` · example `25.42`
Exact live total in the deposit token (`tokenMint`): the sum of the positions' `tokenValue` plus any `parIntermediate.tokenValue`, a dollar figure at par. Absent when any contributing position is unavailable, so partial money is never presented as complete.
- **`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.read` 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`.
- **`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"
curl 'https://api.solana.com/v1/earn/external-wallet/positions/summary' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/earn/external-wallet/positions/summary", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/earn/external-wallet/positions/summary",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"summary": {
"walletCount": 2,
"positionCount": 2,
"unavailablePositionCount": 0,
"totalsByStrategy": [
{
"provider": "kamino",
"providerReference": "C7N2fqV2GvhFghDf5PJSwstyjedjLNCY1cm2Ak7TeU83",
"label": "Kamino Vault USDC",
"walletCount": 2,
"positionCount": 2,
"totalsByToken": [
{
"tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"walletCount": 2,
"positionCount": 2,
"unavailablePositionCount": 0,
"tokenValue": "50.062"
}
]
}
],
"totalsByToken": [
{
"tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"walletCount": 2,
"positionCount": 2,
"unavailablePositionCount": 0,
"tokenValue": "50.062"
}
]
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: earn.read",
"details": {
"action": "earn.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### List positions
Source: https://platform.solana.com/docs/reference/api/earn/list-earn-external-wallet-positions
> Lists one customer wallet's live positions, one page at a time.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`GET /v1/earn/external-wallet/positions`
`ownerAddress` is required. A wallet with no positions in your project (never deposited, fully withdrawn, or held under another organization) returns an empty `positions` list, not 404. Live fields are left out, never set to zero, when the provider read is unavailable.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `earn.read` action.
## Query parameters
- **`ownerAddress`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address: an external wallet, so SDP holds no key for it. It signs, owns the shares, and pays the network fee unless the build names a `feePayer`. It must decode to a 32-byte public key; a base58 string that matches the pattern but does not decode returns 400.
- **`limit`** · `integer` · min 1 · max 100 · default `20`
Page size, from 1 to 100. Defaults to 20.
- **`before`** · `string` · min length 1
The previous page's `nextCursor`. Omit it for the first page.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** One page of positions
- **`data`** · `object` · required
- **`ownerAddress`** · `string` · required · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address.
- **`positions`** · `object[]` · required
- **`id`** · `string` · required · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
- **`ownerAddress`** · `string` · required · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address.
- **`provider`** · `string` · required · example `kamino`
- **`providerReference`** · `string` · required · example `7uib8xGAwkaPz4ZGCA6t8sSEid5Yp9ty13PHUweTypx`
The strategy's onchain address.
- **`label`** · `string` · required · example `Allez USDC`
- **`tokenMint`** · `string` · required · example `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`
- **`shareMint`** · `string` · required · example `hXm2xSRF5PLKGMrTvAWqKhR76MuJX5dAabeSChkjqu2`
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`closedAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`shares`** · `string` · max length 128 · pattern `^\d+(\.\d+)?$` · example `25.42`
Live share balance. Absent when the live read is unavailable; never zero-filled.
- **`withdrawableShares`** · `string` · max length 128 · pattern `^\d+(\.\d+)?$` · example `25.42`
Shares you can withdraw right now. Absent when the live read is unavailable.
- **`unlockTimestamp`** · `string | null` · pattern `^\d+$` · example `1789722000`
Unix time, in seconds, when locked shares become withdrawable. Null or absent when no lock applies. To tell an unavailable read apart, check `shares`.
- **`tokenValue`** · `string` · max length 128 · pattern `^\d+(\.\d+)?$` · example `25.42`
Live value of the position in the deposit token (`tokenMint`), as the provider reports it: shares times the rate for rate-based strategies, or a withdrawal quote for the whole position for quote-based providers such as Veda and Ondo. A quote already includes spread and price impact, so it does not scale to other share amounts. It is a deposit-token amount, not a share count or a USD conversion; every Earn deposit token is a USD stablecoin, so at par it reads as dollars. Absent when the live read is unavailable.
- **`parIntermediate`** · `object`
The operator redemption's intermediate token the wallet holds outside any open request, such as Hastra wYLDS after a canceled operator redemption. It belongs to this position and is not included in `tokenValue`. Present only while non-zero.
- **`mint`** · `string` · required · example `8fr7WGTVFszfyNWRMXj6fRjZZAnDwmXwEpCrtzmUkdih`
- **`amount`** · `string` · required · max length 128 · pattern `^\d+(\.\d+)?$` · example `25.42`
Live decimal value returned by the vault provider; never a float.
- **`withdrawableAmount`** · `string` · required · max length 128 · pattern `^\d+(\.\d+)?$` · example `25.42`
Zero while the provider has frozen the token account.
- **`tokenValue`** · `string` · required · max length 128 · pattern `^\d+(\.\d+)?$` · example `25.42`
Value in the position's deposit token (`tokenMint`).
- **`hasMore`** · `boolean` · required
- **`nextCursor`** · `string | null` · required
- **`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.read` 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`.
- **`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"
curl 'https://api.solana.com/v1/earn/external-wallet/positions?ownerAddress=9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/earn/external-wallet/positions?ownerAddress=9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/earn/external-wallet/positions?ownerAddress=9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"ownerAddress": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"positions": [
{
"id": "earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b",
"ownerAddress": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"provider": "kamino",
"providerReference": "C7N2fqV2GvhFghDf5PJSwstyjedjLNCY1cm2Ak7TeU83",
"label": "Kamino Vault USDC",
"tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"shareMint": "CeirL1mAehaM6g2EccgMUQwohNfnuyicskVQ4r9ekXk6",
"createdAt": "2026-11-06T17:57:30.817Z",
"closedAt": null,
"shares": "24.975",
"withdrawableShares": "24.975",
"tokenValue": "25.031"
}
],
"hasMore": false,
"nextCursor": null
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: earn.read",
"details": {
"action": "earn.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### List movements
Source: https://platform.solana.com/docs/reference/api/earn/list-earn-external-wallet-movements
> Lists one customer wallet's deposits and withdrawals, newest first, one page at a time.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`GET /v1/earn/external-wallet/movements`
Movement statuses are `requested`, `submitted`, `confirmed`, `finalized` and `failed`; only `finalized` and `failed` are final. `ownerAddress` is required, and a wallet with no movements returns an empty `movements` list, not 404. Show `tokenAmount` in `tokenMint` units: on a withdrawal it is the observed payout once finalized, while `amount` and `denomination` carry the onchain quantity, which is shares. Reads keep working after a provider stops taking deposits.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `earn.read` action.
## Query parameters
- **`ownerAddress`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address: an external wallet, so SDP holds no key for it. It signs, owns the shares, and pays the network fee unless the build names a `feePayer`. It must decode to a 32-byte public key; a base58 string that matches the pattern but does not decode returns 400.
- **`direction`** · `string` · one of `deposit`, `withdrawal`
- **`status`** · `string` · min length 1 · max length 64
Return only movements in this status, such as `finalized`. Takes one value; an unknown value matches nothing.
- **`limit`** · `integer` · min 1 · max 100 · default `20`
Page size, from 1 to 100. Defaults to 20.
- **`before`** · `string` · min length 1
The previous page's `nextCursor`. Omit it for the first page.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** One page of movements
- **`data`** · `object` · required
- **`ownerAddress`** · `string` · required · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address.
- **`movements`** · `object[]` · required
The wallet's recorded movements, newest first.
- **`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.
- **`hasMore`** · `boolean` · required
- **`nextCursor`** · `string | null` · required
- **`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.read` 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`.
- **`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"
curl 'https://api.solana.com/v1/earn/external-wallet/movements?ownerAddress=9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/earn/external-wallet/movements?ownerAddress=9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/earn/external-wallet/movements?ownerAddress=9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"ownerAddress": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"movements": [
{
"movementId": "earn_movement_a6aed3f5-4f89-4dc6-9d4b-0864021bd80d",
"positionId": "earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b",
"provider": "kamino",
"providerReference": "C7N2fqV2GvhFghDf5PJSwstyjedjLNCY1cm2Ak7TeU83",
"direction": "deposit",
"settlement": "atomic",
"signature": "3dhkSPLiQZQPXL96fWzMdWJtdbtAffkBKXw2xhsYbV3VNSnYrVAcdx1czsnLsE1ou4SvLecGdSYe4QpAuZdfjJAf",
"ownerAddress": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"amount": "25",
"denomination": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"failureReason": null,
"createdAt": "2026-11-06T17:57:30.817Z",
"status": "finalized",
"tokenAmount": "25",
"confirmedAt": "2026-11-06T17:57:32.517Z",
"settledAt": "2026-11-06T17:57:35.623Z"
}
],
"hasMore": false,
"nextCursor": null
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: earn.read",
"details": {
"action": "earn.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Get a movement
Source: https://platform.solana.com/docs/reference/api/earn/get-earn-external-wallet-movement
> Gets one deposit or withdrawal and refreshes its onchain status when it can.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`GET /v1/earn/external-wallet/movements/{movementId}`
Use it to track a movement after submit, or to recover a submit whose response was lost. If the chain read fails, you get the last recorded status while SDP keeps reconciling in the background. An atomic movement can show as done at `confirmed`, but only `finalized` and `failed` are final. For `settlement: provider_order`, chain confirmation is not payout: the provider still has to complete the order.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `earn.read` action.
## Path parameters
- **`movementId`** · `string` · required · min length 1 · max length 128 · example `earn_movement_a6aed3f5-4f89-4dc6-9d4b-0864021bd80d`
The `movementId` a submit returned.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Recorded movement
- **`data`** · `object` · required
- **`movement`** · `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.read` 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`.
- **`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"
curl 'https://api.solana.com/v1/earn/external-wallet/movements/earn_movement_a6aed3f5-4f89-4dc6-9d4b-0864021bd80d' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/earn/external-wallet/movements/earn_movement_a6aed3f5-4f89-4dc6-9d4b-0864021bd80d", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/earn/external-wallet/movements/earn_movement_a6aed3f5-4f89-4dc6-9d4b-0864021bd80d",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"movement": {
"movementId": "earn_movement_a6aed3f5-4f89-4dc6-9d4b-0864021bd80d",
"positionId": "earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b",
"provider": "kamino",
"providerReference": "C7N2fqV2GvhFghDf5PJSwstyjedjLNCY1cm2Ak7TeU83",
"direction": "deposit",
"settlement": "atomic",
"signature": "3dhkSPLiQZQPXL96fWzMdWJtdbtAffkBKXw2xhsYbV3VNSnYrVAcdx1czsnLsE1ou4SvLecGdSYe4QpAuZdfjJAf",
"ownerAddress": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"amount": "25",
"denomination": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"failureReason": null,
"createdAt": "2026-11-06T17:57:30.817Z",
"status": "finalized",
"tokenAmount": "25",
"confirmedAt": "2026-11-06T17:57:32.517Z",
"settledAt": "2026-11-06T17:57:35.623Z"
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: earn.read",
"details": {
"action": "earn.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Get earnings
Source: https://platform.solana.com/docs/reference/api/earn/get-earn-external-wallet-earnings
> Gets one customer wallet's live balance and earnings per deposit token.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`GET /v1/earn/external-wallet/earnings`
`earned` is live value plus observed withdrawal payouts (`totalWithdrawn`) minus finalized deposits (`totalDeposited`), and appears only when it is exact. Otherwise it is left out with an `earnedUnavailableReason`, such as a live value that is unavailable, movements that are still settling, or a withdrawal payout SDP did not observe; never show it as zero. A wallet with no positions returns `positionCount: 0` and an empty `totalsByToken`, not 404. Totals cover the positions the wallet holds now: a fully withdrawn position drops out here but stays in the movements list. Live value reads the wallet's whole balance in the strategy, so shares acquired outside SDP raise it.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `earn.read` action.
## Query parameters
- **`ownerAddress`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address: an external wallet, so SDP holds no key for it. It signs, owns the shares, and pays the network fee unless the build names a `feePayer`. It must decode to a 32-byte public key; a base58 string that matches the pattern but does not decode returns 400.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** Earnings per deposit token
- **`data`** · `object` · required
- **`earnings`** · `object` · required
- **`ownerAddress`** · `string` · required · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address.
- **`positionCount`** · `integer` · required · min 0
- **`unavailablePositionCount`** · `integer` · required · min 0
- **`totalsByToken`** · `object[]` · required
- **`tokenMint`** · `string` · required · example `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`
- **`positionCount`** · `integer` · required · min 0
- **`unavailablePositionCount`** · `integer` · required · min 0
Positions whose live value could not be read.
- **`currentValue`** · `string` · max length 128 · pattern `^\d+(\.\d+)?$` · example `25.42`
Live value across the token's positions, including any par intermediate they hold. Absent when any contributing position is unavailable, so partial money is never presented as complete.
- **`totalDeposited`** · `string` · required · max length 128 · pattern `^\d+(\.\d+)?$` · example `25.42`
Sum of observed finalized SDP deposits. Excludes unobserved receipts (see `deposits_not_valued`).
- **`totalWithdrawn`** · `string` · required · max length 128 · pattern `^\d+(\.\d+)?$` · example `25.42`
Sum of the observed payouts of finalized withdrawals. Always present. Excludes withdrawals whose payout SDP did not observe (see `withdrawals_not_valued`).
- **`earned`** · `string` · max length 129 · pattern `^-?\d+(\.\d+)?$` · example `5.2`
`currentValue + totalWithdrawn - totalDeposited`, present only when exact and never zero-filled. The live value reads the wallet's whole balance in the strategy, so shares acquired outside SDP raise this figure.
- **`earnedUnavailableReason`** · `string` · one of `live_value_unavailable`, `movements_pending`, `withdrawals_pending`, `deposits_not_valued`, `withdrawals_not_valued`
Why `earned` is absent: `live_value_unavailable` (a live value could not be read), `movements_pending` (a movement is still settling), `withdrawals_pending` (a held position has an open withdrawal request, whose shares leave the wallet before its payout, which can take days), or `deposits_not_valued` and `withdrawals_not_valued` (SDP did not observe a finalized deposit or withdrawal, so `totalDeposited` or `totalWithdrawn` is incomplete).
- **`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.read` 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`.
- **`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"
curl 'https://api.solana.com/v1/earn/external-wallet/earnings?ownerAddress=9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/earn/external-wallet/earnings?ownerAddress=9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/earn/external-wallet/earnings?ownerAddress=9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"earnings": {
"ownerAddress": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"positionCount": 1,
"unavailablePositionCount": 0,
"totalsByToken": [
{
"tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"positionCount": 1,
"unavailablePositionCount": 0,
"currentValue": "25.031",
"totalDeposited": "25",
"totalWithdrawn": "0",
"earned": "0.031"
}
]
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: earn.read",
"details": {
"action": "earn.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Build a deposit transaction
Source: https://platform.solana.com/docs/reference/api/earn/create-earn-external-wallet-deposit-transaction
> Builds an unsigned deposit transaction for the customer's wallet to sign.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`POST /v1/earn/external-wallet/deposit-transactions`
Nothing moves until you submit the signed transaction. The build applies provider access checks and can name your own `feePayer`, which then signs alongside the customer. A swap-funded deposit that cannot fit in one transaction returns a separate swap and follow-up deposit instead of `transaction`, so check the response before you request a signature.
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.
## Request body
`application/json` · required
Build one unsigned deposit transaction for an external wallet.
- **`strategyId`** · `string` · required · min length 1 · example `earn_strategy_3b6a81ce-00b0-4c5a-9761-cb359932e97e`
- **`ownerAddress`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address: an external wallet, so SDP holds no key for it. It signs, owns the shares, and pays the network fee unless the build names a `feePayer`. It must decode to a 32-byte public key; a base58 string that matches the pattern but does not decode returns 400.
- **`feePayer`** · `string` · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `3nMFwZXwY1s1M5s8vYAHqd4wGs4iSxXE4LRoUMMYqEgF`
Pay for this transaction from a wallet your backend controls instead of the customer's. The named address becomes the fee payer: it pays the network fee, funds any share-account rent, and must sign alongside the customer. Co-sign on your backend before you submit. The build returns 400, naming the fee payer, when it cannot pay. Sending the customer's own address keeps the default: the customer pays and signs alone.
- **`amount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
The deposit amount as a decimal string, in deposit-token units. For a swap-funded build (`sourceTokenMint`), it is in source-token units instead: the swap spends all of it, and the deposit is sized to the swap's guaranteed output.
- **`minSharesOut`** · `string` · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `24.9`
Slippage floor in share units. Required exactly when the selected strategy's `depositSlippage` is non-null. Derive it from `POST /v1/earn/vault-deposit-previews`, never from the deposit amount.
- **`sourceTokenMint`** · `string` · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Fund the deposit in a different supported stablecoin (USDC, USDG, PYUSD or USDT on the environment's cluster): a Jupiter swap to the vault's own token is prepended inside the same transaction, so both legs land atomically or not at all. Equal to the strategy's deposit mint it is a no-op, so pickers may always send their selection.
- **`swapSlippageBps`** · `integer` · min 1 · max 500 · example `2`
Swap slippage tolerance in basis points (default 2, accepted 1-500). Valid only with `sourceTokenMint`. The deposit is sized to the swap's worst-case output, so a wider tolerance can leave more of the deposit token in the customer's account instead of failing the transaction.
## Responses
- **`200`** Unsigned deposit transaction
- **`data`** · `object` · required
Any of:
- **object**
- **`transaction`** · `object` · required
The built deposit transaction plus the strategy it targets.
- **`transactionId`** · `string` · required · example `earn_external_wallet_transaction_e7a88258-1d19-48f3-8d0e-81fefa4bf3b3`
- **`transaction`** · `string` · required
Base64 wire bytes of the unsigned transaction. Add every required signature without changing the message, then submit through SDP. The blockhash bounds when the network can first accept it; retain the original signed bytes for recovery.
- **`lastValidBlockHeight`** · `string` · required · example `361186610`
Last block height at which the network can first accept this transaction. This is a block height, not a slot or timestamp.
- **`sponsored`** · `boolean` · required · example `false`
Always false: SDP does not pay an external wallet's fees. The customer's wallet pays, or the `feePayer` you name.
- **`ownerAddress`** · `string` · required · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address.
- **`feePayer`** · `string` · example `3nMFwZXwY1s1M5s8vYAHqd4wGs4iSxXE4LRoUMMYqEgF`
The `feePayer` your build request named. When present, the transaction needs the fee payer's signature as well as the customer's: co-sign on your backend before you submit, or the submit refuses it. When omitted, the customer signs alone and pays the fee.
- **`provider`** · `string` · required · example `kamino`
- **`providerReference`** · `string` · required · example `7uib8xGAwkaPz4ZGCA6t8sSEid5Yp9ty13PHUweTypx`
The strategy's onchain address at the provider. It identifies the instrument; never send funds to it.
- **`tokenMint`** · `string` · required · example `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`
- **`shareMint`** · `string` · required · example `hXm2xSRF5PLKGMrTvAWqKhR76MuJX5dAabeSChkjqu2`
- **`amount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Positive decimal string with at least one non-zero digit; never a float.
- **`minSharesOut`** · `string | null` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Positive decimal string with at least one non-zero digit; never a float.
- **`swap`** · `object`
Present when the build was swap-funded: the swap is prepended inside this same transaction and `amount` equals `swap.depositAmount`.
- **`sourceTokenMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Mint the owner pays with.
- **`sourceAmount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
What the swap spends, in source-token units.
- **`depositAmount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
The deposit amount the transaction encodes, in deposit-token units: the swap's guaranteed minimum output. Any output above it stays in the customer's token account.
- **`quotedAmount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
The swap's quoted output at the live rate, in deposit-token units.
- **`slippageBps`** · `integer` · required · example `2`
- **`priceImpactPct`** · `string` · required · example `0.0001`
Quoted price impact as a decimal ratio string.
- **`routeLabels`** · `string[]` · required
Venue labels along the quoted route.
- **`strategy`** · `object` · required
- **`id`** · `string` · required · example `earn_strategy_3b6a81ce-00b0-4c5a-9761-cb359932e97e`
- **`name`** · `string` · required · example `Allez USDC`
- **`provider`** · `string` · required · example `kamino`
- **`providerReference`** · `string` · required · example `7uib8xGAwkaPz4ZGCA6t8sSEid5Yp9ty13PHUweTypx`
- **`hostCluster`** · `string` · required · example `devnet`
- **Returned instead of a built transaction when the swap and the deposit cannot fit in one Solana transaction (1,232 bytes), even on a compact route. SDP saves no build or movement for it.**
- **`requiresSeparateSwap`** · `boolean` · required · always `true`
Discriminates from the atomic answer, which carries `transaction`.
- **`sponsored`** · `boolean` · required · always `false`
Split swap transactions are never sponsored by SDP.
- **`swap`** · `object` · required
The Jupiter swap leg attached to (or split out of) a swap-funded deposit.
- **`sourceTokenMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Mint the owner pays with.
- **`sourceAmount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
What the swap spends, in source-token units.
- **`depositAmount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
The deposit amount the transaction encodes, in deposit-token units: the swap's guaranteed minimum output. Any output above it stays in the customer's token account.
- **`quotedAmount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
The swap's quoted output at the live rate, in deposit-token units.
- **`slippageBps`** · `integer` · required · example `2`
- **`priceImpactPct`** · `string` · required · example `0.0001`
Quoted price impact as a decimal ratio string.
- **`routeLabels`** · `string[]` · required
Venue labels along the quoted route.
- **`transaction`** · `string` · required
Base64 wire bytes of the unsigned swap-only transaction. The fee payer is the customer, or your request's `feePayer`, which then co-signs it too. You broadcast it yourself: it moves only the customer's own funds, so SDP records nothing for it.
- **`lastValidBlockHeight`** · `string` · required · example `361186610`
- **`followUp`** · `object` · required
The ordinary (unswapped) deposit build to request once the swap is confirmed.
- **`strategyId`** · `string` · required · example `earn_strategy_3b6a81ce-00b0-4c5a-9761-cb359932e97e`
- **`amount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Positive decimal string with at least one non-zero digit; never a float.
- **`minSharesOut`** · `string` · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
The original request's share floor, carried through so the follow-up build keeps the protection the caller asked for. Absent only when the original request carried none.
- **`feePayer`** · `string` · example `3nMFwZXwY1s1M5s8vYAHqd4wGs4iSxXE4LRoUMMYqEgF`
The original request's fee payer, carried through so the follow-up build does not silently bill the customer's wallet. Absent when none was named.
- **`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`.
- **`429`** Too Many Requests
Same fields as `400`.
- **`500`** Internal Server Error
Same fields as `400`.
- **`501`** The provider does not support this operation.
- **`error`** · `object` · required
Standard error payload.
- **`code`** · `string` · required
- **`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.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
curl -X POST 'https://api.solana.com/v1/earn/external-wallet/deposit-transactions' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"strategyId": "earn_strategy_3b6a81ce-00b0-4c5a-9761-cb359932e97e",
"ownerAddress": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"amount": "25"
}'
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/earn/external-wallet/deposit-transactions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"strategyId": "earn_strategy_3b6a81ce-00b0-4c5a-9761-cb359932e97e",
"ownerAddress": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"amount": "25"
})
});
```
```python tab="Python"
import os, requests
r = requests.post("https://api.solana.com/v1/earn/external-wallet/deposit-transactions",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"},
json={
"strategyId": "earn_strategy_3b6a81ce-00b0-4c5a-9761-cb359932e97e",
"ownerAddress": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"amount": "25"
})
```
## Example responses
```json tab="200"
{
"data": {
"transaction": {
"transactionId": "earn_external_wallet_transaction_e7a88258-1d19-48f3-8d0e-81fefa4bf3b3",
"transaction": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"lastValidBlockHeight": "361186610",
"sponsored": false,
"ownerAddress": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"provider": "kamino",
"providerReference": "C7N2fqV2GvhFghDf5PJSwstyjedjLNCY1cm2Ak7TeU83",
"tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"shareMint": "CeirL1mAehaM6g2EccgMUQwohNfnuyicskVQ4r9ekXk6",
"amount": "25",
"minSharesOut": null,
"strategy": {
"id": "earn_strategy_3b6a81ce-00b0-4c5a-9761-cb359932e97e",
"name": "Kamino Vault USDC",
"provider": "kamino",
"providerReference": "C7N2fqV2GvhFghDf5PJSwstyjedjLNCY1cm2Ak7TeU83",
"hostCluster": "devnet"
}
}
},
"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).
---
### Submit a deposit
Source: https://platform.solana.com/docs/reference/api/earn/create-earn-external-wallet-deposit
> Submits a deposit transaction signed by the customer's wallet.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`POST /v1/earn/external-wallet/deposits`
SDP checks that the signed bytes match the built transaction and that every required signature is valid (the customer's, plus your fee payer's when the build named one), records the movement, then broadcasts. Idempotency-Key is required: a retry with the same key returns the original movement with `replayed: true`, and each built transaction can be submitted once. Reusing a key with a different body, or submitting one build under a second key, returns `409 CONFLICT`. 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; never replace a signed transaction whose outcome is unknown.
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 deposit
- **`data`** · `object` · required
- **`deposit`** · `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/deposits' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"transactionId": "earn_external_wallet_transaction_e7a88258-1d19-48f3-8d0e-81fefa4bf3b3",
"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/deposits", {
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_e7a88258-1d19-48f3-8d0e-81fefa4bf3b3",
"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/deposits",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
json={
"transactionId": "earn_external_wallet_transaction_e7a88258-1d19-48f3-8d0e-81fefa4bf3b3",
"signedTransaction": "AUyx/sEc49/1l6jxo3nbViPAIUqdrXCo8Qx5SfDvIbtUI0zex3Bi6Cyhhn1QTVU8zWDzXTRyIX2aCmq9v7yk5AOA"
})
```
## Example responses
```json tab="200"
{
"data": {
"deposit": {
"movementId": "earn_movement_a6aed3f5-4f89-4dc6-9d4b-0864021bd80d",
"positionId": "earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b",
"provider": "kamino",
"providerReference": "C7N2fqV2GvhFghDf5PJSwstyjedjLNCY1cm2Ak7TeU83",
"direction": "deposit",
"settlement": "atomic",
"signature": "3dhkSPLiQZQPXL96fWzMdWJtdbtAffkBKXw2xhsYbV3VNSnYrVAcdx1czsnLsE1ou4SvLecGdSYe4QpAuZdfjJAf",
"ownerAddress": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"amount": "25",
"denomination": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"failureReason": null,
"createdAt": "2026-11-06T17:57:30.817Z",
"status": "submitted",
"tokenAmount": null,
"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).
---
### Preview a withdrawal
Source: https://platform.solana.com/docs/reference/api/earn/create-earn-external-wallet-withdrawal-preview
> Quotes what withdrawing these shares would pay right now, from the strategy's live accounting.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`POST /v1/earn/external-wallet/withdrawal-previews`
When the strategy's `withdrawalSlippage` is not null, set `minAmountOut` from this quote: take your tolerance off `assetsOut`, rounded down to `assetDecimals`. The preview is read-only, takes no Idempotency-Key, and still works after a strategy is deprecated or its provider stops taking deposits. It does not check the wallet's balance, so keep `shares` within the position's `withdrawableShares`. It returns 501 when the provider cannot quote withdrawals.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `earn.read` action.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Request body
`application/json` · required
Quote one withdrawal from a position against the strategy's live accounting. Read-only: nothing is built or saved.
- **`positionId`** · `string` · required · min length 1 · max length 128 · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
The position's ID, from the positions list or the deposit submit response.
- **`shares`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `10`
Shares to withdraw, as a decimal string in share units. Keep it within the position's `withdrawableShares`: the preview does not check the balance.
## Responses
- **`200`** Live withdrawal quote
- **`data`** · `object` · required
- **`positionId`** · `string` · required · min length 1 · max length 128 · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
The position's ID, from the positions list or the deposit submit response.
- **`assetsOut`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25.02`
What redeeming the shares would pay at the live rate, decimal string in the deposit token's units. A truthful `minAmountOut` floor is derived from this figure.
- **`assetDecimals`** · `integer` · required · example `6`
The deposit token's decimals, which define the scale used to quantize a floor.
- **`blockingIssues`** · `object[]` · required
Conditions the provider reports would block this withdrawal; empty when none.
- **`code`** · `string` · required · example `SHARE_LOCKED`
- **`message`** · `string` · required
- **`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.read` 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`.
- **`429`** Too Many Requests
Same fields as `400`.
- **`500`** Internal Server Error
Same fields as `400`.
- **`501`** The provider does not support this operation.
- **`error`** · `object` · required
Standard error payload.
- **`code`** · `string` · required
- **`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.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
curl -X POST 'https://api.solana.com/v1/earn/external-wallet/withdrawal-previews' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"positionId": "earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b",
"shares": "10"
}'
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/earn/external-wallet/withdrawal-previews", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"positionId": "earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b",
"shares": "10"
})
});
```
```python tab="Python"
import os, requests
r = requests.post("https://api.solana.com/v1/earn/external-wallet/withdrawal-previews",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"},
json={
"positionId": "earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b",
"shares": "10"
})
```
## Example responses
```json tab="200"
{
"data": {
"positionId": "earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b",
"assetsOut": "10.0224",
"assetDecimals": 6,
"blockingIssues": []
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: earn.read",
"details": {
"action": "earn.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Build a withdrawal transaction
Source: https://platform.solana.com/docs/reference/api/earn/create-earn-external-wallet-withdrawal-transaction
> Builds an unsigned withdrawal transaction for a position, for the customer's wallet to sign.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`POST /v1/earn/external-wallet/withdrawal-transactions`
Withdrawals keep working after a provider stops taking deposits. Send `minAmountOut` when the strategy's `withdrawalSlippage` is not null, and omit it when null. A provider-order route may not support a preview: it places an order that pays out later, not an instant redemption.
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.
## Request body
`application/json` · required
Build one unsigned withdrawal transaction for a position. SDP reads the strategy and mints from the position.
- **`positionId`** · `string` · required · min length 1 · max length 128 · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
The position's ID, from the positions list or the deposit submit response.
- **`redemptionMode`** · `string` · one of `standard`, `instant`
The redemption route to build: `standard` queues a provider-managed order for a later payout, `instant` is the provider's faster route but still waits for cross-chain delivery — chain confirmation alone is not payout, so treat the exit as complete only at the movement's verified `delivered` outcome. Defaults to `standard`; a provider without the requested route refuses the build.
- **`shares`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `10`
Shares to redeem, decimal string in share units.
- **`minAmountOut`** · `string` · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `19.98`
Withdrawal slippage floor: the minimum deposit-token amount to accept, as a decimal string. Derive it from the withdrawal preview's `assetsOut` minus your tolerance. Required when the strategy's `withdrawalSlippage` is not null; refused with 400 when it is null, because the transaction cannot enforce it.
- **`feePayer`** · `string` · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `3nMFwZXwY1s1M5s8vYAHqd4wGs4iSxXE4LRoUMMYqEgF`
Pay for this transaction from a wallet your backend controls instead of the customer's. The named address becomes the fee payer: it pays the network fee, funds any share-account rent, and must sign alongside the customer. Co-sign on your backend before you submit. The build returns 400, naming the fee payer, when it cannot pay. Sending the customer's own address keeps the default: the customer pays and signs alone.
## Responses
- **`200`** Unsigned withdrawal transaction
- **`data`** · `object` · required
- **`transaction`** · `object` · required
The common fields of one built withdrawal transaction.
- **`transactionId`** · `string` · required · example `earn_external_wallet_transaction_e7a88258-1d19-48f3-8d0e-81fefa4bf3b3`
- **`transaction`** · `string` · required
Base64 wire bytes of the unsigned transaction. Add every required signature without changing the message, then submit through SDP. The blockhash bounds when the network can first accept it; retain the original signed bytes for recovery.
- **`lastValidBlockHeight`** · `string` · required · example `361186610`
Last block height at which the network can first accept this transaction. This is a block height, not a slot or timestamp.
- **`sponsored`** · `boolean` · required · example `false`
Always false: SDP does not pay an external wallet's fees. The customer's wallet pays, or the `feePayer` you name.
- **`ownerAddress`** · `string` · required · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address.
- **`feePayer`** · `string` · example `3nMFwZXwY1s1M5s8vYAHqd4wGs4iSxXE4LRoUMMYqEgF`
The `feePayer` your build request named. When present, the transaction needs the fee payer's signature as well as the customer's: co-sign on your backend before you submit, or the submit refuses it. When omitted, the customer signs alone and pays the fee.
- **`provider`** · `string` · required · example `kamino`
- **`providerReference`** · `string` · required · example `7uib8xGAwkaPz4ZGCA6t8sSEid5Yp9ty13PHUweTypx`
The strategy's onchain address at the provider. It identifies the instrument; never send funds to it.
- **`tokenMint`** · `string` · required · example `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`
- **`shareMint`** · `string` · required · example `hXm2xSRF5PLKGMrTvAWqKhR76MuJX5dAabeSChkjqu2`
- **`shares`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Positive decimal string with at least one non-zero digit; never a float.
- **`minAmountOut`** · `string | null` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
The floor encoded in the transaction, or null when the request carried none.
- **`positionId`** · `string` · required · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
- **`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`.
- **`429`** Too Many Requests
Same fields as `400`.
- **`500`** Internal Server Error
Same fields as `400`.
- **`501`** The provider does not support this operation.
- **`error`** · `object` · required
Standard error payload.
- **`code`** · `string` · required
- **`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.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
curl -X POST 'https://api.solana.com/v1/earn/external-wallet/withdrawal-transactions' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"positionId": "earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b",
"shares": "10"
}'
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/earn/external-wallet/withdrawal-transactions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"positionId": "earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b",
"shares": "10"
})
});
```
```python tab="Python"
import os, requests
r = requests.post("https://api.solana.com/v1/earn/external-wallet/withdrawal-transactions",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"},
json={
"positionId": "earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b",
"shares": "10"
})
```
## Example responses
```json tab="200"
{
"data": {
"transaction": {
"transactionId": "earn_external_wallet_transaction_0ebe7320-4b8e-4d1a-a5f0-6c2d9e8b1f47",
"transaction": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"lastValidBlockHeight": "361186972",
"sponsored": false,
"ownerAddress": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"provider": "kamino",
"providerReference": "C7N2fqV2GvhFghDf5PJSwstyjedjLNCY1cm2Ak7TeU83",
"tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"shareMint": "CeirL1mAehaM6g2EccgMUQwohNfnuyicskVQ4r9ekXk6",
"positionId": "earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b",
"shares": "10",
"minAmountOut": null
}
},
"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).
---
### Submit a withdrawal
Source: https://platform.solana.com/docs/reference/api/earn/create-earn-external-wallet-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).
---
### List withdrawal options
Source: https://platform.solana.com/docs/reference/api/earn/get-earn-external-wallet-withdrawal-options
> Lists the withdrawal routes a position supports right now: instant, provider order, solver queue and operator redemption, each read from live provider state.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`POST /v1/earn/external-wallet/withdrawal-options`
A position offers instant or provider order, never both; the withdrawal endpoints build whichever it offers. Call this when the customer starts a withdrawal, and let them choose when more than one route is available. Never infer a route from the strategy's `liquidityTerm`.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `earn.read` action.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Request body
`application/json` · required
List the withdrawal routes a position supports right now.
- **`positionId`** · `string` · required · min length 1 · max length 128 · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
The position's ID, from the positions list or the deposit submit response.
## Responses
- **`200`** Withdrawal options and their live terms
- **`data`** · `object` · required
- **`positionId`** · `string` · required · min length 1 · max length 128 · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
The position's ID, from the positions list or the deposit submit response.
- **`instant`** · `boolean` · required
Whether the withdrawal endpoints build an instant withdrawal for this position. Never true together with `providerOrder`.
- **`providerRedemptionModes`** · `string[]` · each one of `standard`, `instant`
- **`providerOrder`** · `boolean` · required
Whether the withdrawal endpoints build a provider order that pays out later. Never true together with `instant`.
- **`queued`** · `boolean` · required
Whether a solver queue accepts requests now. Never infer it from `liquidityTerm`.
- **`withdrawAuthority`** · `string | null` · required
The withdrawal authority the queue's vault reports, or null without a queue. Informational: no request takes it.
- **`queueState`** · `string | null` · required
The provider queue account, or null without a queue.
- **`queueAsset`** · `object | null` · required
Live asset-specific queue limits. Null when this asset has no queue configuration.
- **`assetMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`allowWithdrawals`** · `boolean` · required
Whether this asset's queue currently accepts new requests.
- **`secondsToMaturity`** · `integer` · required · min 0 · example `86400`
Seconds after the request is created, by the cluster clock, before a solver may fulfill it.
- **`minimumSecondsToDeadline`** · `integer` · required · min 0 · example `3600`
Shortest solver window accepted after maturity.
- **`maximumSecondsToDeadline`** · `integer` · required · greater than 0 · max 7776000 · example `7776000`
Longest solver window accepted after maturity.
- **`minimumDiscountBps`** · `integer` · required · min 0 · max 10000 · example `0`
- **`maximumDiscountBps`** · `integer` · required · min 0 · max 10000 · example `500`
- **`minimumShares`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `1`
Smallest request accepted by the queue, in share units.
- **`shareDecimals`** · `integer` · required · min 0 · max 38 · example `6`
- **`parRedemption`** · `object | null` · required
Live operator redemption terms. Null when this strategy offers no operator redemption.
- **`intermediateMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Token created or delegated by the request transaction and later burned by the operator.
- **`assetMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Token the operator pays when the redemption completes.
- **`minimumShares`** · `string | null` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `1`
Smallest operator redemption request funded by shares, in position-share units. Null while the provider's rate is unavailable, which also blocks share-funded requests.
- **`minimumIntermediateAmount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `0.000001`
Smallest request over intermediate the owner already holds, in intermediate-token units.
- **`shareDecimals`** · `integer` · required · min 0 · max 38 · example `6`
- **`assetDecimals`** · `integer` · required · min 0 · max 38 · example `6`
- **`cancelable`** · `boolean` · required
Whether the owner may revoke an open request before the operator completes it.
- **`operatorSettled`** · `boolean` · required · always `true`
Always true: a provider operator must complete the eventual asset payout.
- **`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.read` 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`.
- **`429`** Too Many Requests
Same fields as `400`.
- **`500`** Internal Server Error
Same fields as `400`.
- **`501`** The provider does not support this operation.
- **`error`** · `object` · required
Standard error payload.
- **`code`** · `string` · required
- **`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.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
curl -X POST 'https://api.solana.com/v1/earn/external-wallet/withdrawal-options' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc"
}'
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/earn/external-wallet/withdrawal-options", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc"
})
});
```
```python tab="Python"
import os, requests
r = requests.post("https://api.solana.com/v1/earn/external-wallet/withdrawal-options",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"},
json={
"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc"
})
```
## Example responses
```json tab="200"
{
"data": {
"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc",
"instant": true,
"providerOrder": false,
"queued": true,
"withdrawAuthority": "11111111111111111111111111111111",
"queueState": "7XbSKzG8Kf1qsprNV1XE9YHNNpSV7jZBWLx1n4B6tSDf",
"queueAsset": {
"assetMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"allowWithdrawals": true,
"secondsToMaturity": 3600,
"minimumSecondsToDeadline": 60,
"maximumSecondsToDeadline": 7776000,
"minimumDiscountBps": 0,
"maximumDiscountBps": 100,
"minimumShares": "0.000001",
"shareDecimals": 6
},
"parRedemption": null
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: earn.read",
"details": {
"action": "earn.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Preview a withdrawal request
Source: https://platform.solana.com/docs/reference/api/earn/create-earn-external-wallet-queued-withdrawal-preview
> Previews a withdrawal request: a solver queue's discounted payout and timing, or an operator redemption's intermediate-token conversion and payout.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`POST /v1/earn/external-wallet/queued-withdrawal-previews`
Check the mechanism against the position's withdrawal options first, and resolve every `blockingIssues` entry before the customer confirms. The preview is read-only and safe to retry. It does not check the wallet's balance, so keep `shares` within the position's `withdrawableShares`.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `earn.read` action.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Request body
`application/json` · required
Preview a solver-queue or operator-redemption request against live provider limits. Read-only: nothing is built or persisted.
Any of:
- **object**
- **`positionId`** · `string` · required · min length 1 · max length 128 · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
The position's ID, from the positions list or the deposit submit response.
- **`shares`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `10`
Shares to place in the withdrawal request, in share units.
- **`mechanism`** · `string` · always `solverQueue`
Selects the solver queue, which is also the default when omitted. Operator redemptions must set `mechanism` explicitly.
- **`discountBps`** · `integer` · required · min 0 · max 10000 · example `25`
Discount offered to a solver, in basis points. It must stay inside the live queueAsset minimum and maximum.
- **`deadlineSeconds`** · `integer` · required · greater than 0 · max 7776000 · example `86400`
Solver window after maturity, in seconds. It must stay inside the live queueAsset minimum and maximum.
- **object**
- **`positionId`** · `string` · required · min length 1 · max length 128 · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
The position's ID, from the positions list or the deposit submit response.
- **`shares`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `10`
Shares to place in the withdrawal request, in share units.
- **`mechanism`** · `string` · required · always `operatorRedemption`
Selects an operator redemption, which takes no solver discount or deadline.
- **object**
- **`positionId`** · `string` · required · min length 1 · max length 128 · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
The position's ID, from the positions list or the deposit submit response.
- **`intermediateAmount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `2000`
Intermediate token the owner already holds outside any request (such as a cancelled request's wYLDS) to redeem at par, in that token's units. Send instead of `shares`, never with it.
- **`mechanism`** · `string` · required · always `operatorRedemption`
Selects an operator redemption, which takes no solver discount or deadline.
## Responses
- **`200`** Expected request terms
- **`data`** · `object` · required
Any of:
- **object**
- **`positionId`** · `string` · required · min length 1 · max length 128 · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
The position's ID, from the positions list or the deposit submit response.
- **`assetMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`shares`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Positive decimal string with at least one non-zero digit; never a float.
- **`shareDecimals`** · `integer` · required · min 0 · max 38
- **`assets`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `9.975`
Expected payout after the discount, in `assetMint` units.
- **`assetDecimals`** · `integer` · required · min 0 · max 38
- **`discountBps`** · `integer` · required · min 0 · max 10000
- **`maturityTimestamp`** · `string` · required · pattern `^\d+$` · example `1789722000`
Unix time, in seconds, when a solver may start fulfilling the request.
- **`deadlineTimestamp`** · `string` · required · pattern `^\d+$` · example `1789808400`
Unix time, in seconds, of the request's deadline. A solver may fulfill it until this time; cancellation is allowed only after it.
- **`blockingIssues`** · `object[]` · required
- **`code`** · `string` · required · example `SHARE_LOCKED`
- **`message`** · `string` · required
- **object**
- **`positionId`** · `string` · required · min length 1 · max length 128 · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
The position's ID, from the positions list or the deposit submit response.
- **`mechanism`** · `string` · required · always `operatorRedemption`
- **`shares`** · `string` · required · max length 128 · pattern `^\d+(\.\d+)?$` · example `10`
Position shares the request burns, in share units; "0" when it redeems intermediate the owner already holds.
- **`shareDecimals`** · `integer` · required · min 0 · max 38
- **`intermediateMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Token the request creates or delegates for later operator settlement.
- **`intermediateAmount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Expected intermediate-token amount, in that token's units.
- **`assetMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`assets`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `10`
Expected par payout when the operator completes the redemption.
- **`assetDecimals`** · `integer` · required · min 0 · max 38
- **`blockingIssues`** · `object[]` · required
- **`code`** · `string` · required · example `SHARE_LOCKED`
- **`message`** · `string` · required
- **`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.read` 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`.
- **`429`** Too Many Requests
Same fields as `400`.
- **`500`** Internal Server Error
Same fields as `400`.
- **`501`** The provider does not support this operation.
- **`error`** · `object` · required
Standard error payload.
- **`code`** · `string` · required
- **`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.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
curl -X POST 'https://api.solana.com/v1/earn/external-wallet/queued-withdrawal-previews' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc",
"mechanism": "solverQueue",
"shares": "15",
"discountBps": 10,
"deadlineSeconds": 60
}'
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/earn/external-wallet/queued-withdrawal-previews", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc",
"mechanism": "solverQueue",
"shares": "15",
"discountBps": 10,
"deadlineSeconds": 60
})
});
```
```python tab="Python"
import os, requests
r = requests.post("https://api.solana.com/v1/earn/external-wallet/queued-withdrawal-previews",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"},
json={
"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc",
"mechanism": "solverQueue",
"shares": "15",
"discountBps": 10,
"deadlineSeconds": 60
})
```
## Example responses
```json tab="200"
{
"data": {
"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc",
"assetMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"shares": "15",
"shareDecimals": 6,
"assets": "14.985",
"assetDecimals": 6,
"discountBps": 10,
"maturityTimestamp": "1793991049",
"deadlineTimestamp": "1793991109",
"blockingIssues": []
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: earn.read",
"details": {
"action": "earn.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Build a withdrawal request transaction
Source: https://platform.solana.com/docs/reference/api/earn/create-earn-external-wallet-withdrawal-request-transaction
> Builds an unsigned withdrawal request transaction for a position, for the customer's wallet to sign.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`POST /v1/earn/external-wallet/withdrawal-request-transactions`
A solver queue request escrows shares; an operator redemption converts shares into an intermediate token that the operator settles later. Neither transaction pays the customer. Save the `withdrawalRequestId` the submit returns and track the request until it is fulfilled, canceled, or failed.
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.
## Request body
`application/json` · required
Build a keyed asynchronous withdrawal request. A solver request escrows shares; an operator redemption converts them into the provider's intermediate token, or delegates intermediate the owner already holds. Neither action pays assets.
Any of:
- **object**
- **`positionId`** · `string` · required · min length 1 · max length 128 · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
The position's ID, from the positions list or the deposit submit response.
- **`shares`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `10`
Shares to place in the withdrawal request, in share units.
- **`mechanism`** · `string` · always `solverQueue`
Selects the solver queue, which is also the default when omitted. Operator redemptions must set `mechanism` explicitly.
- **`discountBps`** · `integer` · required · min 0 · max 10000 · example `25`
Discount offered to a solver, in basis points. It must stay inside the live queueAsset minimum and maximum.
- **`deadlineSeconds`** · `integer` · required · greater than 0 · max 7776000 · example `86400`
Solver window after maturity, in seconds. It must stay inside the live queueAsset minimum and maximum.
- **`feePayer`** · `string` · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `3nMFwZXwY1s1M5s8vYAHqd4wGs4iSxXE4LRoUMMYqEgF`
Pay for this transaction from a wallet your backend controls instead of the customer's. The named address becomes the fee payer: it pays the network fee, funds any share-account rent, and must sign alongside the customer. Co-sign on your backend before you submit. The build returns 400, naming the fee payer, when it cannot pay. Sending the customer's own address keeps the default: the customer pays and signs alone.
- **object**
- **`positionId`** · `string` · required · min length 1 · max length 128 · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
The position's ID, from the positions list or the deposit submit response.
- **`shares`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `10`
Shares to place in the withdrawal request, in share units.
- **`mechanism`** · `string` · required · always `operatorRedemption`
Selects an operator redemption, which takes no solver discount or deadline.
- **`feePayer`** · `string` · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `3nMFwZXwY1s1M5s8vYAHqd4wGs4iSxXE4LRoUMMYqEgF`
Pay for this transaction from a wallet your backend controls instead of the customer's. The named address becomes the fee payer: it pays the network fee, funds any share-account rent, and must sign alongside the customer. Co-sign on your backend before you submit. The build returns 400, naming the fee payer, when it cannot pay. Sending the customer's own address keeps the default: the customer pays and signs alone.
- **object**
- **`positionId`** · `string` · required · min length 1 · max length 128 · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
The position's ID, from the positions list or the deposit submit response.
- **`intermediateAmount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `2000`
Intermediate token the owner already holds outside any request (such as a cancelled request's wYLDS) to redeem at par, in that token's units. Send instead of `shares`, never with it.
- **`mechanism`** · `string` · required · always `operatorRedemption`
Selects an operator redemption, which takes no solver discount or deadline.
- **`feePayer`** · `string` · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `3nMFwZXwY1s1M5s8vYAHqd4wGs4iSxXE4LRoUMMYqEgF`
Pay for this transaction from a wallet your backend controls instead of the customer's. The named address becomes the fee payer: it pays the network fee, funds any share-account rent, and must sign alongside the customer. Co-sign on your backend before you submit. The build returns 400, naming the fee payer, when it cannot pay. Sending the customer's own address keeps the default: the customer pays and signs alone.
## Responses
- **`200`** Unsigned withdrawal request transaction
- **`data`** · `object` · required
- **`transaction`** · `object` · required
Any of:
- **mechanism: "solverQueue"**
- **`transactionId`** · `string` · required · example `earn_external_wallet_transaction_e7a88258-1d19-48f3-8d0e-81fefa4bf3b3`
- **`transaction`** · `string` · required
Base64 wire bytes of the unsigned transaction. Add every required signature without changing the message, then submit through SDP. The blockhash bounds when the network can first accept it; retain the original signed bytes for recovery.
- **`lastValidBlockHeight`** · `string` · required · example `361186610`
Last block height at which the network can first accept this transaction. This is a block height, not a slot or timestamp.
- **`sponsored`** · `boolean` · required · example `false`
Always false: SDP does not pay an external wallet's fees. The customer's wallet pays, or the `feePayer` you name.
- **`ownerAddress`** · `string` · required · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address.
- **`feePayer`** · `string` · example `3nMFwZXwY1s1M5s8vYAHqd4wGs4iSxXE4LRoUMMYqEgF`
The `feePayer` your build request named. When present, the transaction needs the fee payer's signature as well as the customer's: co-sign on your backend before you submit, or the submit refuses it. When omitted, the customer signs alone and pays the fee.
- **`provider`** · `string` · required · example `kamino`
- **`providerReference`** · `string` · required · example `7uib8xGAwkaPz4ZGCA6t8sSEid5Yp9ty13PHUweTypx`
The strategy's onchain address at the provider. It identifies the instrument; never send funds to it.
- **`tokenMint`** · `string` · required · example `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`
- **`shareMint`** · `string` · required · example `hXm2xSRF5PLKGMrTvAWqKhR76MuJX5dAabeSChkjqu2`
- **`positionId`** · `string` · required · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
- **`action`** · `string` · required · always `request`
- **`mechanism`** · `string` · required · always `solverQueue`
- **`requestAddress`** · `string` · required
Provider request account address.
- **`shares`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Positive decimal string with at least one non-zero digit; never a float.
- **`assets`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Positive decimal string with at least one non-zero digit; never a float.
- **`discountBps`** · `integer` · required · min 0 · max 10000
- **`maturityTimestamp`** · `string` · required · pattern `^\d+$`
- **`deadlineTimestamp`** · `string` · required · pattern `^\d+$`
- **mechanism: "operatorRedemption"**
- **`transactionId`** · `string` · required · example `earn_external_wallet_transaction_e7a88258-1d19-48f3-8d0e-81fefa4bf3b3`
- **`transaction`** · `string` · required
Base64 wire bytes of the unsigned transaction. Add every required signature without changing the message, then submit through SDP. The blockhash bounds when the network can first accept it; retain the original signed bytes for recovery.
- **`lastValidBlockHeight`** · `string` · required · example `361186610`
Last block height at which the network can first accept this transaction. This is a block height, not a slot or timestamp.
- **`sponsored`** · `boolean` · required · example `false`
Always false: SDP does not pay an external wallet's fees. The customer's wallet pays, or the `feePayer` you name.
- **`ownerAddress`** · `string` · required · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address.
- **`feePayer`** · `string` · example `3nMFwZXwY1s1M5s8vYAHqd4wGs4iSxXE4LRoUMMYqEgF`
The `feePayer` your build request named. When present, the transaction needs the fee payer's signature as well as the customer's: co-sign on your backend before you submit, or the submit refuses it. When omitted, the customer signs alone and pays the fee.
- **`provider`** · `string` · required · example `kamino`
- **`providerReference`** · `string` · required · example `7uib8xGAwkaPz4ZGCA6t8sSEid5Yp9ty13PHUweTypx`
The strategy's onchain address at the provider. It identifies the instrument; never send funds to it.
- **`tokenMint`** · `string` · required · example `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`
- **`shareMint`** · `string` · required · example `hXm2xSRF5PLKGMrTvAWqKhR76MuJX5dAabeSChkjqu2`
- **`positionId`** · `string` · required · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
- **`action`** · `string` · required · always `request`
- **`mechanism`** · `string` · required · always `operatorRedemption`
- **`requestAddress`** · `string` · required
Provider request account address.
- **`shares`** · `string` · required · max length 128 · pattern `^\d+(\.\d+)?$` · example `10`
Position shares the request burns, in share units; "0" when it redeems intermediate the owner already holds.
- **`assets`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Positive decimal string with at least one non-zero digit; never a float.
- **`intermediateMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`intermediateAmount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Positive decimal string with at least one non-zero digit; never a float.
- **`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`.
- **`501`** The provider does not support this operation.
- **`error`** · `object` · required
Standard error payload.
- **`code`** · `string` · required
- **`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.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
curl -X POST 'https://api.solana.com/v1/earn/external-wallet/withdrawal-request-transactions' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc",
"mechanism": "solverQueue",
"shares": "15",
"discountBps": 10,
"deadlineSeconds": 60
}'
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/earn/external-wallet/withdrawal-request-transactions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc",
"mechanism": "solverQueue",
"shares": "15",
"discountBps": 10,
"deadlineSeconds": 60
})
});
```
```python tab="Python"
import os, requests
r = requests.post("https://api.solana.com/v1/earn/external-wallet/withdrawal-request-transactions",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"},
json={
"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc",
"mechanism": "solverQueue",
"shares": "15",
"discountBps": 10,
"deadlineSeconds": 60
})
```
## Example responses
```json tab="200"
{
"data": {
"transaction": {
"transactionId": "earn_external_wallet_withdrawal_request_transaction_54c50679-1b52-494d-84fa-106df66452ff",
"transaction": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"lastValidBlockHeight": "361187340",
"sponsored": false,
"ownerAddress": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"provider": "veda",
"providerReference": "3wbKP5UGLT7gAZBAsLjvPC1NbfnWKtT3Dq7cniMWkzfU",
"tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"shareMint": "CdV7pjj6WANsdasKsBvdKAn7qJL7cQ2Q3CJMBEe13WAV",
"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc",
"requestAddress": "DKw4pYcvXNLs91RssedGFznWRpbvPttp2AAwNCG7fQRz",
"action": "request",
"mechanism": "solverQueue",
"shares": "15",
"assets": "14.985",
"discountBps": 10,
"maturityTimestamp": "1793991049",
"deadlineTimestamp": "1793991109"
}
},
"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).
---
### Submit a withdrawal request
Source: https://platform.solana.com/docs/reference/api/earn/create-earn-external-wallet-withdrawal-request
> Submits a withdrawal request transaction signed by the customer's wallet.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`POST /v1/earn/external-wallet/withdrawal-requests`
SDP checks every signature against the exact built message, records the request before broadcast, then sends it. Idempotency-Key is required: a retry with the same key returns the original request, and each built transaction can be submitted once. Reusing a key with a different body, or submitting one build under a second key, returns `409 CONFLICT`. An expired build can still be submitted late. If its reservation was released or replaced, SDP accepts it only once the exact signed transaction is finalized as successful, so after a `409 CONFLICT` retry the same bytes and key until that shows. A late operator redemption whose original cycle already closed is refused for operator reconciliation, so it cannot claim a newer request at the same address. A transaction that recovery finds finalized as failed returns `400 TRANSACTION_FAILED`; build a new one. A recovery RPC outage returns `503 PROVIDER_UNAVAILABLE`: retry the original bytes and key. An unresolved 16-signature history window returns `503` and needs support to reconcile. An expired operator redemption build whose creation history is unavailable also returns `503`: keep and retry the original signed intent rather than signing a replacement.
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 request
- **`data`** · `object` · required
- **`withdrawalRequest`** · `object` · required
Any of:
- **mechanism: "solverQueue"**
- **`withdrawalRequestId`** · `string` · required · example `earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9`
- **`positionId`** · `string` · required · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
- **`provider`** · `string` · required · example `veda`
- **`providerReference`** · `string` · required
The strategy's onchain address.
- **`ownerAddress`** · `string` · required · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address.
- **`requestAddress`** · `string` · required
Provider request account address.
- **`status`** · `string` · required · one of `creating`, `pending`, `fulfillable`, `expiredCancelable`, `cancelling`, `fulfilled`, `cancelled`, `closedOrUnknown`, `failed`
The request's status. Only `fulfilled`, `cancelled` and `failed` are final. Keep tracking every other status, `closedOrUnknown` included: SDP resolves it from a finalized close event.
- **`assetMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`shareMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`shares`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Positive decimal string with at least one non-zero digit; never a float.
- **`quotedAssets`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Expected payout at request creation, in `assetMint` units. Not a settled amount.
- **`shareDecimals`** · `integer` · required · min 0 · max 38
- **`assetDecimals`** · `integer` · required · min 0 · max 38
- **`nonce`** · `string | null` · required
- **`creationTimestamp`** · `string | null` · required · pattern `^\d+$`
- **`creationSignature`** · `string | null` · required
The request-creation transaction. Its confirmation does not prove a payout.
- **`cancelSignature`** · `string | null` · required
The cancellation transaction, once one is submitted.
- **`closingSignature`** · `string | null` · required
Provider-authenticated fulfillment or cancellation transaction, when known.
- **`assetsPaid`** · `string | null` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
The payout SDP observed at fulfillment, in `assetMint` units; null until then.
- **`failureReason`** · `string | null` · required
- **`fulfilledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`cancelledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`replayed`** · `boolean`
- **`mechanism`** · `string` · required · always `solverQueue`
- **`intermediateMint`** · `any` · required
- **`intermediateAmount`** · `any` · required
- **`discountBps`** · `integer` · required · min 0 · max 10000
- **`maturityTimestamp`** · `string` · required · pattern `^\d+$`
- **`deadlineTimestamp`** · `string` · required · pattern `^\d+$`
- **mechanism: "operatorRedemption"**
- **`withdrawalRequestId`** · `string` · required · example `earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9`
- **`positionId`** · `string` · required · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
- **`provider`** · `string` · required · example `veda`
- **`providerReference`** · `string` · required
The strategy's onchain address.
- **`ownerAddress`** · `string` · required · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address.
- **`requestAddress`** · `string` · required
Provider request account address.
- **`status`** · `string` · required · one of `creating`, `pending`, `fulfillable`, `expiredCancelable`, `cancelling`, `fulfilled`, `cancelled`, `closedOrUnknown`, `failed`
The request's status. Only `fulfilled`, `cancelled` and `failed` are final. Keep tracking every other status, `closedOrUnknown` included: SDP resolves it from a finalized close event.
- **`assetMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`shareMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`shares`** · `string` · required · max length 128 · pattern `^\d+(\.\d+)?$` · example `10`
Position shares the request burns, in share units; "0" when it redeems intermediate the owner already holds.
- **`quotedAssets`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Expected payout at request creation, in `assetMint` units. Not a settled amount.
- **`shareDecimals`** · `integer` · required · min 0 · max 38
- **`assetDecimals`** · `integer` · required · min 0 · max 38
- **`nonce`** · `string | null` · required
- **`creationTimestamp`** · `string | null` · required · pattern `^\d+$`
- **`creationSignature`** · `string | null` · required
The request-creation transaction. Its confirmation does not prove a payout.
- **`cancelSignature`** · `string | null` · required
The cancellation transaction, once one is submitted.
- **`closingSignature`** · `string | null` · required
Provider-authenticated fulfillment or cancellation transaction, when known.
- **`assetsPaid`** · `string | null` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
The payout SDP observed at fulfillment, in `assetMint` units; null until then.
- **`failureReason`** · `string | null` · required
- **`fulfilledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`cancelledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`replayed`** · `boolean`
- **`mechanism`** · `string` · required · always `operatorRedemption`
- **`intermediateMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`intermediateAmount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Positive decimal string with at least one non-zero digit; never a float.
- **`discountBps`** · `any` · required
- **`maturityTimestamp`** · `any` · required
- **`deadlineTimestamp`** · `any` · required
- **`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/withdrawal-requests' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"transactionId": "earn_external_wallet_withdrawal_request_transaction_54c50679-1b52-494d-84fa-106df66452ff",
"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/withdrawal-requests", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
"transactionId": "earn_external_wallet_withdrawal_request_transaction_54c50679-1b52-494d-84fa-106df66452ff",
"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/withdrawal-requests",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
json={
"transactionId": "earn_external_wallet_withdrawal_request_transaction_54c50679-1b52-494d-84fa-106df66452ff",
"signedTransaction": "AUyx/sEc49/1l6jxo3nbViPAIUqdrXCo8Qx5SfDvIbtUI0zex3Bi6Cyhhn1QTVU8zWDzXTRyIX2aCmq9v7yk5AOA"
})
```
## Example responses
```json tab="200"
{
"data": {
"withdrawalRequest": {
"withdrawalRequestId": "earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9",
"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc",
"provider": "veda",
"providerReference": "3wbKP5UGLT7gAZBAsLjvPC1NbfnWKtT3Dq7cniMWkzfU",
"ownerAddress": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"requestAddress": "DKw4pYcvXNLs91RssedGFznWRpbvPttp2AAwNCG7fQRz",
"status": "pending",
"mechanism": "solverQueue",
"assetMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"shareMint": "CdV7pjj6WANsdasKsBvdKAn7qJL7cQ2Q3CJMBEe13WAV",
"intermediateMint": null,
"intermediateAmount": null,
"shares": "15",
"quotedAssets": "14.985",
"shareDecimals": 6,
"assetDecimals": 6,
"discountBps": 10,
"nonce": "0",
"creationTimestamp": "1793987449",
"maturityTimestamp": "1793991049",
"deadlineTimestamp": "1793991109",
"creationSignature": "3dhkSPLiQZQPXL96fWzMdWJtdbtAffkBKXw2xhsYbV3VNSnYrVAcdx1czsnLsE1ou4SvLecGdSYe4QpAuZdfjJAf",
"cancelSignature": null,
"closingSignature": null,
"assetsPaid": null,
"failureReason": null,
"fulfilledAt": null,
"cancelledAt": null,
"createdAt": "2026-11-06T18:10:18.900Z",
"updatedAt": "2026-11-06T18:26:00.379Z"
}
},
"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).
---
### List withdrawal requests
Source: https://platform.solana.com/docs/reference/api/earn/list-earn-external-wallet-withdrawal-requests
> Lists one customer wallet's withdrawal requests, one page at a time.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`GET /v1/earn/external-wallet/withdrawal-requests`
`ownerAddress` is required. On startup and after reconnecting, pass `settled=false` to restore every request that still needs fulfillment, classification or cancellation, and page until `hasMore` is false.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `earn.read` action.
## Query parameters
- **`ownerAddress`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address: an external wallet, so SDP holds no key for it. It signs, owns the shares, and pays the network fee unless the build names a `feePayer`. It must decode to a 32-byte public key; a base58 string that matches the pattern but does not decode returns 400.
- **`limit`** · `integer` · min 1 · max 100 · default `20`
Page size, from 1 to 100. Defaults to 20.
- **`before`** · `string` · min length 1
The previous page's `nextCursor`. Omit it for the first page.
- **`status`** · `string` · one of `creating`, `pending`, `fulfillable`, `expiredCancelable`, `cancelling`, `closedOrUnknown`, `fulfilled`, `cancelled`, `failed`
- **`settled`** · `string` · one of `true`, `false`
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** One page of withdrawal requests
- **`data`** · `object` · required
- **`withdrawalRequests`** · `object[]` · required
Any of:
- **mechanism: "solverQueue"**
- **`withdrawalRequestId`** · `string` · required · example `earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9`
- **`positionId`** · `string` · required · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
- **`provider`** · `string` · required · example `veda`
- **`providerReference`** · `string` · required
The strategy's onchain address.
- **`ownerAddress`** · `string` · required · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address.
- **`requestAddress`** · `string` · required
Provider request account address.
- **`status`** · `string` · required · one of `creating`, `pending`, `fulfillable`, `expiredCancelable`, `cancelling`, `fulfilled`, `cancelled`, `closedOrUnknown`, `failed`
The request's status. Only `fulfilled`, `cancelled` and `failed` are final. Keep tracking every other status, `closedOrUnknown` included: SDP resolves it from a finalized close event.
- **`assetMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`shareMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`shares`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Positive decimal string with at least one non-zero digit; never a float.
- **`quotedAssets`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Expected payout at request creation, in `assetMint` units. Not a settled amount.
- **`shareDecimals`** · `integer` · required · min 0 · max 38
- **`assetDecimals`** · `integer` · required · min 0 · max 38
- **`nonce`** · `string | null` · required
- **`creationTimestamp`** · `string | null` · required · pattern `^\d+$`
- **`creationSignature`** · `string | null` · required
The request-creation transaction. Its confirmation does not prove a payout.
- **`cancelSignature`** · `string | null` · required
The cancellation transaction, once one is submitted.
- **`closingSignature`** · `string | null` · required
Provider-authenticated fulfillment or cancellation transaction, when known.
- **`assetsPaid`** · `string | null` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
The payout SDP observed at fulfillment, in `assetMint` units; null until then.
- **`failureReason`** · `string | null` · required
- **`fulfilledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`cancelledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`replayed`** · `boolean`
- **`mechanism`** · `string` · required · always `solverQueue`
- **`intermediateMint`** · `any` · required
- **`intermediateAmount`** · `any` · required
- **`discountBps`** · `integer` · required · min 0 · max 10000
- **`maturityTimestamp`** · `string` · required · pattern `^\d+$`
- **`deadlineTimestamp`** · `string` · required · pattern `^\d+$`
- **mechanism: "operatorRedemption"**
- **`withdrawalRequestId`** · `string` · required · example `earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9`
- **`positionId`** · `string` · required · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
- **`provider`** · `string` · required · example `veda`
- **`providerReference`** · `string` · required
The strategy's onchain address.
- **`ownerAddress`** · `string` · required · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address.
- **`requestAddress`** · `string` · required
Provider request account address.
- **`status`** · `string` · required · one of `creating`, `pending`, `fulfillable`, `expiredCancelable`, `cancelling`, `fulfilled`, `cancelled`, `closedOrUnknown`, `failed`
The request's status. Only `fulfilled`, `cancelled` and `failed` are final. Keep tracking every other status, `closedOrUnknown` included: SDP resolves it from a finalized close event.
- **`assetMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`shareMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`shares`** · `string` · required · max length 128 · pattern `^\d+(\.\d+)?$` · example `10`
Position shares the request burns, in share units; "0" when it redeems intermediate the owner already holds.
- **`quotedAssets`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Expected payout at request creation, in `assetMint` units. Not a settled amount.
- **`shareDecimals`** · `integer` · required · min 0 · max 38
- **`assetDecimals`** · `integer` · required · min 0 · max 38
- **`nonce`** · `string | null` · required
- **`creationTimestamp`** · `string | null` · required · pattern `^\d+$`
- **`creationSignature`** · `string | null` · required
The request-creation transaction. Its confirmation does not prove a payout.
- **`cancelSignature`** · `string | null` · required
The cancellation transaction, once one is submitted.
- **`closingSignature`** · `string | null` · required
Provider-authenticated fulfillment or cancellation transaction, when known.
- **`assetsPaid`** · `string | null` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
The payout SDP observed at fulfillment, in `assetMint` units; null until then.
- **`failureReason`** · `string | null` · required
- **`fulfilledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`cancelledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`replayed`** · `boolean`
- **`mechanism`** · `string` · required · always `operatorRedemption`
- **`intermediateMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`intermediateAmount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Positive decimal string with at least one non-zero digit; never a float.
- **`discountBps`** · `any` · required
- **`maturityTimestamp`** · `any` · required
- **`deadlineTimestamp`** · `any` · required
- **`hasMore`** · `boolean` · required
- **`nextCursor`** · `string | null` · required
- **`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.read` 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`.
- **`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"
curl 'https://api.solana.com/v1/earn/external-wallet/withdrawal-requests?ownerAddress=9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/earn/external-wallet/withdrawal-requests?ownerAddress=9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/earn/external-wallet/withdrawal-requests?ownerAddress=9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"withdrawalRequests": [
{
"withdrawalRequestId": "earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9",
"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc",
"provider": "veda",
"providerReference": "3wbKP5UGLT7gAZBAsLjvPC1NbfnWKtT3Dq7cniMWkzfU",
"ownerAddress": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"requestAddress": "DKw4pYcvXNLs91RssedGFznWRpbvPttp2AAwNCG7fQRz",
"status": "pending",
"mechanism": "solverQueue",
"assetMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"shareMint": "CdV7pjj6WANsdasKsBvdKAn7qJL7cQ2Q3CJMBEe13WAV",
"intermediateMint": null,
"intermediateAmount": null,
"shares": "15",
"quotedAssets": "14.985",
"shareDecimals": 6,
"assetDecimals": 6,
"discountBps": 10,
"nonce": "0",
"creationTimestamp": "1793987449",
"maturityTimestamp": "1793991049",
"deadlineTimestamp": "1793991109",
"creationSignature": "3dhkSPLiQZQPXL96fWzMdWJtdbtAffkBKXw2xhsYbV3VNSnYrVAcdx1czsnLsE1ou4SvLecGdSYe4QpAuZdfjJAf",
"cancelSignature": null,
"closingSignature": null,
"assetsPaid": null,
"failureReason": null,
"fulfilledAt": null,
"cancelledAt": null,
"createdAt": "2026-11-06T18:10:18.900Z",
"updatedAt": "2026-11-06T18:26:00.379Z"
}
],
"hasMore": false,
"nextCursor": null
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: earn.read",
"details": {
"action": "earn.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Get a withdrawal request
Source: https://platform.solana.com/docs/reference/api/earn/get-earn-external-wallet-withdrawal-request
> Gets one withdrawal request with its latest recorded status.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`GET /v1/earn/external-wallet/withdrawal-requests/{withdrawalRequestId}`
Creating a request escrows shares or converts them to an intermediate token; it does not pay out. Only `fulfilled`, `cancelled` and `failed` are final. Keep polling a `closedOrUnknown` request: SDP has not yet confirmed whether it paid out or returned the customer's tokens.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `earn.read` action.
## Path parameters
- **`withdrawalRequestId`** · `string` · required · min length 1 · max length 128 · example `earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9`
The `withdrawalRequestId` a request submit returned.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Responses
- **`200`** The withdrawal request
- **`data`** · `object` · required
- **`withdrawalRequest`** · `object` · required
Any of:
- **mechanism: "solverQueue"**
- **`withdrawalRequestId`** · `string` · required · example `earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9`
- **`positionId`** · `string` · required · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
- **`provider`** · `string` · required · example `veda`
- **`providerReference`** · `string` · required
The strategy's onchain address.
- **`ownerAddress`** · `string` · required · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address.
- **`requestAddress`** · `string` · required
Provider request account address.
- **`status`** · `string` · required · one of `creating`, `pending`, `fulfillable`, `expiredCancelable`, `cancelling`, `fulfilled`, `cancelled`, `closedOrUnknown`, `failed`
The request's status. Only `fulfilled`, `cancelled` and `failed` are final. Keep tracking every other status, `closedOrUnknown` included: SDP resolves it from a finalized close event.
- **`assetMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`shareMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`shares`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Positive decimal string with at least one non-zero digit; never a float.
- **`quotedAssets`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Expected payout at request creation, in `assetMint` units. Not a settled amount.
- **`shareDecimals`** · `integer` · required · min 0 · max 38
- **`assetDecimals`** · `integer` · required · min 0 · max 38
- **`nonce`** · `string | null` · required
- **`creationTimestamp`** · `string | null` · required · pattern `^\d+$`
- **`creationSignature`** · `string | null` · required
The request-creation transaction. Its confirmation does not prove a payout.
- **`cancelSignature`** · `string | null` · required
The cancellation transaction, once one is submitted.
- **`closingSignature`** · `string | null` · required
Provider-authenticated fulfillment or cancellation transaction, when known.
- **`assetsPaid`** · `string | null` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
The payout SDP observed at fulfillment, in `assetMint` units; null until then.
- **`failureReason`** · `string | null` · required
- **`fulfilledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`cancelledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`replayed`** · `boolean`
- **`mechanism`** · `string` · required · always `solverQueue`
- **`intermediateMint`** · `any` · required
- **`intermediateAmount`** · `any` · required
- **`discountBps`** · `integer` · required · min 0 · max 10000
- **`maturityTimestamp`** · `string` · required · pattern `^\d+$`
- **`deadlineTimestamp`** · `string` · required · pattern `^\d+$`
- **mechanism: "operatorRedemption"**
- **`withdrawalRequestId`** · `string` · required · example `earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9`
- **`positionId`** · `string` · required · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
- **`provider`** · `string` · required · example `veda`
- **`providerReference`** · `string` · required
The strategy's onchain address.
- **`ownerAddress`** · `string` · required · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address.
- **`requestAddress`** · `string` · required
Provider request account address.
- **`status`** · `string` · required · one of `creating`, `pending`, `fulfillable`, `expiredCancelable`, `cancelling`, `fulfilled`, `cancelled`, `closedOrUnknown`, `failed`
The request's status. Only `fulfilled`, `cancelled` and `failed` are final. Keep tracking every other status, `closedOrUnknown` included: SDP resolves it from a finalized close event.
- **`assetMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`shareMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`shares`** · `string` · required · max length 128 · pattern `^\d+(\.\d+)?$` · example `10`
Position shares the request burns, in share units; "0" when it redeems intermediate the owner already holds.
- **`quotedAssets`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Expected payout at request creation, in `assetMint` units. Not a settled amount.
- **`shareDecimals`** · `integer` · required · min 0 · max 38
- **`assetDecimals`** · `integer` · required · min 0 · max 38
- **`nonce`** · `string | null` · required
- **`creationTimestamp`** · `string | null` · required · pattern `^\d+$`
- **`creationSignature`** · `string | null` · required
The request-creation transaction. Its confirmation does not prove a payout.
- **`cancelSignature`** · `string | null` · required
The cancellation transaction, once one is submitted.
- **`closingSignature`** · `string | null` · required
Provider-authenticated fulfillment or cancellation transaction, when known.
- **`assetsPaid`** · `string | null` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
The payout SDP observed at fulfillment, in `assetMint` units; null until then.
- **`failureReason`** · `string | null` · required
- **`fulfilledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`cancelledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`replayed`** · `boolean`
- **`mechanism`** · `string` · required · always `operatorRedemption`
- **`intermediateMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`intermediateAmount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Positive decimal string with at least one non-zero digit; never a float.
- **`discountBps`** · `any` · required
- **`maturityTimestamp`** · `any` · required
- **`deadlineTimestamp`** · `any` · required
- **`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.read` 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`.
- **`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"
curl 'https://api.solana.com/v1/earn/external-wallet/withdrawal-requests/earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9' \
-H "Authorization: Bearer $SDP_API_KEY"
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/earn/external-wallet/withdrawal-requests/earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
```python tab="Python"
import os, requests
r = requests.get("https://api.solana.com/v1/earn/external-wallet/withdrawal-requests/earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"})
```
## Example responses
```json tab="200"
{
"data": {
"withdrawalRequest": {
"withdrawalRequestId": "earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9",
"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc",
"provider": "veda",
"providerReference": "3wbKP5UGLT7gAZBAsLjvPC1NbfnWKtT3Dq7cniMWkzfU",
"ownerAddress": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"requestAddress": "DKw4pYcvXNLs91RssedGFznWRpbvPttp2AAwNCG7fQRz",
"status": "pending",
"mechanism": "solverQueue",
"assetMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"shareMint": "CdV7pjj6WANsdasKsBvdKAn7qJL7cQ2Q3CJMBEe13WAV",
"intermediateMint": null,
"intermediateAmount": null,
"shares": "15",
"quotedAssets": "14.985",
"shareDecimals": 6,
"assetDecimals": 6,
"discountBps": 10,
"nonce": "0",
"creationTimestamp": "1793987449",
"maturityTimestamp": "1793991049",
"deadlineTimestamp": "1793991109",
"creationSignature": "3dhkSPLiQZQPXL96fWzMdWJtdbtAffkBKXw2xhsYbV3VNSnYrVAcdx1czsnLsE1ou4SvLecGdSYe4QpAuZdfjJAf",
"cancelSignature": null,
"closingSignature": null,
"assetsPaid": null,
"failureReason": null,
"fulfilledAt": null,
"cancelledAt": null,
"createdAt": "2026-11-06T18:10:18.900Z",
"updatedAt": "2026-11-06T18:26:00.379Z"
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: earn.read",
"details": {
"action": "earn.read"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Build a cancellation transaction
Source: https://platform.solana.com/docs/reference/api/earn/create-earn-external-wallet-withdrawal-request-cancel-transaction
> Builds an unsigned transaction that cancels a withdrawal request, for the customer's wallet to sign.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`POST /v1/earn/external-wallet/withdrawal-request-cancel-transactions`
A solver queue request can be canceled only at `expiredCancelable`, because a solver may fulfill it until its deadline; before then the build returns `409 CONFLICT`. An operator redemption that allows cancellation can be canceled while `pending`, which leaves the customer holding the intermediate token, not the original shares. Requires `earn.manage`. Cancellation stays available without production access and while Earn is disabled; a suspended organization still cannot authenticate.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `earn.manage` action.
## Headers
- **`x-project-id`** · `string` · min length 1 · example `prj_example`
Required for dashboard sessions. API keys use their assigned project.
## Request body
`application/json` · required
Build a cancellation for the customer's wallet to sign. A solver queue request becomes cancelable at `expiredCancelable`; an operator redemption that allows cancellation can be canceled while `pending`.
- **`withdrawalRequestId`** · `string` · required · min length 1 · max length 128 · example `earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9`
- **`feePayer`** · `string` · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `3nMFwZXwY1s1M5s8vYAHqd4wGs4iSxXE4LRoUMMYqEgF`
Pay for this transaction from a wallet your backend controls instead of the customer's. The named address becomes the fee payer: it pays the network fee, funds any share-account rent, and must sign alongside the customer. Co-sign on your backend before you submit. The build returns 400, naming the fee payer, when it cannot pay. Sending the customer's own address keeps the default: the customer pays and signs alone.
## Responses
- **`200`** Unsigned cancellation transaction
- **`data`** · `object` · required
- **`transaction`** · `object` · required
A cancellation for the customer's wallet to sign.
- **`transactionId`** · `string` · required · example `earn_external_wallet_transaction_e7a88258-1d19-48f3-8d0e-81fefa4bf3b3`
- **`transaction`** · `string` · required
Base64 wire bytes of the unsigned transaction. Add every required signature without changing the message, then submit through SDP. The blockhash bounds when the network can first accept it; retain the original signed bytes for recovery.
- **`lastValidBlockHeight`** · `string` · required · example `361186610`
Last block height at which the network can first accept this transaction. This is a block height, not a slot or timestamp.
- **`sponsored`** · `boolean` · required · example `false`
Always false: SDP does not pay an external wallet's fees. The customer's wallet pays, or the `feePayer` you name.
- **`ownerAddress`** · `string` · required · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address.
- **`feePayer`** · `string` · example `3nMFwZXwY1s1M5s8vYAHqd4wGs4iSxXE4LRoUMMYqEgF`
The `feePayer` your build request named. When present, the transaction needs the fee payer's signature as well as the customer's: co-sign on your backend before you submit, or the submit refuses it. When omitted, the customer signs alone and pays the fee.
- **`provider`** · `string` · required · example `kamino`
- **`providerReference`** · `string` · required · example `7uib8xGAwkaPz4ZGCA6t8sSEid5Yp9ty13PHUweTypx`
The strategy's onchain address at the provider. It identifies the instrument; never send funds to it.
- **`tokenMint`** · `string` · required · example `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`
- **`shareMint`** · `string` · required · example `hXm2xSRF5PLKGMrTvAWqKhR76MuJX5dAabeSChkjqu2`
- **`positionId`** · `string` · required · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
- **`withdrawalRequestId`** · `string` · required · example `earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9`
- **`action`** · `string` · required · always `cancel`
- **`mechanism`** · `string` · required · one of `solverQueue`, `operatorRedemption`
- **`requestAddress`** · `string` · required
Provider request account address.
- **`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.manage` 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`.
- **`501`** The provider does not support this operation.
- **`error`** · `object` · required
Standard error payload.
- **`code`** · `string` · required
- **`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.
- **`503`** Service Unavailable
Same fields as `400`.
## Example request
```bash tab="cURL"
curl -X POST 'https://api.solana.com/v1/earn/external-wallet/withdrawal-request-cancel-transactions' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"withdrawalRequestId": "earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9"
}'
```
```ts tab="TypeScript"
const res = await fetch("https://api.solana.com/v1/earn/external-wallet/withdrawal-request-cancel-transactions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"withdrawalRequestId": "earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9"
})
});
```
```python tab="Python"
import os, requests
r = requests.post("https://api.solana.com/v1/earn/external-wallet/withdrawal-request-cancel-transactions",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}"},
json={
"withdrawalRequestId": "earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9"
})
```
## Example responses
```json tab="200"
{
"data": {
"transaction": {
"transactionId": "earn_external_wallet_withdrawal_request_transaction_9a3c1e27-5d84-4f0b-b6e2-7f19c4a8d035",
"transaction": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"lastValidBlockHeight": "361187340",
"sponsored": false,
"ownerAddress": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"provider": "veda",
"providerReference": "3wbKP5UGLT7gAZBAsLjvPC1NbfnWKtT3Dq7cniMWkzfU",
"tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"shareMint": "CdV7pjj6WANsdasKsBvdKAn7qJL7cQ2Q3CJMBEe13WAV",
"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc",
"requestAddress": "DKw4pYcvXNLs91RssedGFznWRpbvPttp2AAwNCG7fQRz",
"withdrawalRequestId": "earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9",
"action": "cancel",
"mechanism": "solverQueue"
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: earn.manage",
"details": {
"action": "earn.manage"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
### Submit a cancellation
Source: https://platform.solana.com/docs/reference/api/earn/create-earn-external-wallet-withdrawal-request-cancellation
> Submits a cancellation transaction signed by the customer's wallet.
> **Earn is under active development.** Features, API endpoints, and docs may change.
`POST /v1/earn/external-wallet/withdrawal-request-cancellations`
SDP verifies it, records it before broadcast, and returns the withdrawal request. Idempotency-Key is required, with the same `409 CONFLICT` answers as the other submits. Requires `earn.manage`, and stays available without production access and while Earn is disabled; a suspended organization still cannot authenticate. Keep polling until the request is `cancelled`, `fulfilled` or `failed`, because a fulfillment can race the cancellation.
Requires an [API key](/docs/developing-with-sdp/authentication) with the `earn.manage` 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 withdrawal request with its cancellation recorded
- **`data`** · `object` · required
- **`withdrawalRequest`** · `object` · required
Any of:
- **mechanism: "solverQueue"**
- **`withdrawalRequestId`** · `string` · required · example `earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9`
- **`positionId`** · `string` · required · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
- **`provider`** · `string` · required · example `veda`
- **`providerReference`** · `string` · required
The strategy's onchain address.
- **`ownerAddress`** · `string` · required · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address.
- **`requestAddress`** · `string` · required
Provider request account address.
- **`status`** · `string` · required · one of `creating`, `pending`, `fulfillable`, `expiredCancelable`, `cancelling`, `fulfilled`, `cancelled`, `closedOrUnknown`, `failed`
The request's status. Only `fulfilled`, `cancelled` and `failed` are final. Keep tracking every other status, `closedOrUnknown` included: SDP resolves it from a finalized close event.
- **`assetMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`shareMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`shares`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Positive decimal string with at least one non-zero digit; never a float.
- **`quotedAssets`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Expected payout at request creation, in `assetMint` units. Not a settled amount.
- **`shareDecimals`** · `integer` · required · min 0 · max 38
- **`assetDecimals`** · `integer` · required · min 0 · max 38
- **`nonce`** · `string | null` · required
- **`creationTimestamp`** · `string | null` · required · pattern `^\d+$`
- **`creationSignature`** · `string | null` · required
The request-creation transaction. Its confirmation does not prove a payout.
- **`cancelSignature`** · `string | null` · required
The cancellation transaction, once one is submitted.
- **`closingSignature`** · `string | null` · required
Provider-authenticated fulfillment or cancellation transaction, when known.
- **`assetsPaid`** · `string | null` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
The payout SDP observed at fulfillment, in `assetMint` units; null until then.
- **`failureReason`** · `string | null` · required
- **`fulfilledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`cancelledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`replayed`** · `boolean`
- **`mechanism`** · `string` · required · always `solverQueue`
- **`intermediateMint`** · `any` · required
- **`intermediateAmount`** · `any` · required
- **`discountBps`** · `integer` · required · min 0 · max 10000
- **`maturityTimestamp`** · `string` · required · pattern `^\d+$`
- **`deadlineTimestamp`** · `string` · required · pattern `^\d+$`
- **mechanism: "operatorRedemption"**
- **`withdrawalRequestId`** · `string` · required · example `earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9`
- **`positionId`** · `string` · required · example `earn_position_2f8b6b04-7147-487e-b760-b1caa4fbcb8b`
- **`provider`** · `string` · required · example `veda`
- **`providerReference`** · `string` · required
The strategy's onchain address.
- **`ownerAddress`** · `string` · required · example `9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM`
The customer's wallet address.
- **`requestAddress`** · `string` · required
Provider request account address.
- **`status`** · `string` · required · one of `creating`, `pending`, `fulfillable`, `expiredCancelable`, `cancelling`, `fulfilled`, `cancelled`, `closedOrUnknown`, `failed`
The request's status. Only `fulfilled`, `cancelled` and `failed` are final. Keep tracking every other status, `closedOrUnknown` included: SDP resolves it from a finalized close event.
- **`assetMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`shareMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`shares`** · `string` · required · max length 128 · pattern `^\d+(\.\d+)?$` · example `10`
Position shares the request burns, in share units; "0" when it redeems intermediate the owner already holds.
- **`quotedAssets`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Expected payout at request creation, in `assetMint` units. Not a settled amount.
- **`shareDecimals`** · `integer` · required · min 0 · max 38
- **`assetDecimals`** · `integer` · required · min 0 · max 38
- **`nonce`** · `string | null` · required
- **`creationTimestamp`** · `string | null` · required · pattern `^\d+$`
- **`creationSignature`** · `string | null` · required
The request-creation transaction. Its confirmation does not prove a payout.
- **`cancelSignature`** · `string | null` · required
The cancellation transaction, once one is submitted.
- **`closingSignature`** · `string | null` · required
Provider-authenticated fulfillment or cancellation transaction, when known.
- **`assetsPaid`** · `string | null` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
The payout SDP observed at fulfillment, in `assetMint` units; null until then.
- **`failureReason`** · `string | null` · required
- **`fulfilledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`cancelledAt`** · `string | null` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`createdAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`updatedAt`** · `string` · required · format `date-time` · example `2025-01-01T00:00:00.000Z`
ISO 8601 timestamp.
- **`replayed`** · `boolean`
- **`mechanism`** · `string` · required · always `operatorRedemption`
- **`intermediateMint`** · `string` · required · min length 32 · max length 44 · pattern `^[1-9A-HJ-NP-Za-km-z]{32,44}$` · example `2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo`
Base58 Solana mint address.
- **`intermediateAmount`** · `string` · required · max length 128 · pattern `^(?=.*[1-9])\d+(\.\d+)?$` · example `25`
Positive decimal string with at least one non-zero digit; never a float.
- **`discountBps`** · `any` · required
- **`maturityTimestamp`** · `any` · required
- **`deadlineTimestamp`** · `any` · required
- **`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.manage` 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/withdrawal-request-cancellations' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"transactionId": "earn_external_wallet_withdrawal_request_transaction_9a3c1e27-5d84-4f0b-b6e2-7f19c4a8d035",
"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/withdrawal-request-cancellations", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
"transactionId": "earn_external_wallet_withdrawal_request_transaction_9a3c1e27-5d84-4f0b-b6e2-7f19c4a8d035",
"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/withdrawal-request-cancellations",
headers={"Authorization": f"Bearer {os.environ['SDP_API_KEY']}", "Idempotency-Key": idempotency_key},
json={
"transactionId": "earn_external_wallet_withdrawal_request_transaction_9a3c1e27-5d84-4f0b-b6e2-7f19c4a8d035",
"signedTransaction": "AUyx/sEc49/1l6jxo3nbViPAIUqdrXCo8Qx5SfDvIbtUI0zex3Bi6Cyhhn1QTVU8zWDzXTRyIX2aCmq9v7yk5AOA"
})
```
## Example responses
```json tab="200"
{
"data": {
"withdrawalRequest": {
"withdrawalRequestId": "earn_vault_withdrawal_request_3c905ad2-c92c-4258-9675-1899c41569b9",
"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc",
"provider": "veda",
"providerReference": "3wbKP5UGLT7gAZBAsLjvPC1NbfnWKtT3Dq7cniMWkzfU",
"ownerAddress": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"requestAddress": "DKw4pYcvXNLs91RssedGFznWRpbvPttp2AAwNCG7fQRz",
"status": "cancelling",
"mechanism": "solverQueue",
"assetMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"shareMint": "CdV7pjj6WANsdasKsBvdKAn7qJL7cQ2Q3CJMBEe13WAV",
"intermediateMint": null,
"intermediateAmount": null,
"shares": "15",
"quotedAssets": "14.985",
"shareDecimals": 6,
"assetDecimals": 6,
"discountBps": 10,
"nonce": "0",
"creationTimestamp": "1793987449",
"maturityTimestamp": "1793991049",
"deadlineTimestamp": "1793991109",
"creationSignature": "3dhkSPLiQZQPXL96fWzMdWJtdbtAffkBKXw2xhsYbV3VNSnYrVAcdx1czsnLsE1ou4SvLecGdSYe4QpAuZdfjJAf",
"cancelSignature": "q7g4PK7KtYN8aUWuWL3VAGeecsLWdS4KeSPVpL5YJGLHt5HAz5Ajgr9pwFWTTfK4dh6XjCjuCp3gdex678CPU8h",
"closingSignature": null,
"assetsPaid": null,
"failureReason": null,
"fulfilledAt": null,
"cancelledAt": null,
"createdAt": "2026-11-06T18:10:18.900Z",
"updatedAt": "2026-11-06T19:12:41.020Z"
}
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}
```
```json tab="403"
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Missing action: earn.manage",
"details": {
"action": "earn.manage"
}
},
"meta": {
"requestId": "req_example"
}
}
```
Error responses use the [standard error shape](/docs/reference/conventions/errors).
---
## Docs
### AI resources
Source: https://platform.solana.com/docs/reference/docs-for-ai
> Machine-readable SDP docs, the agent skill, and setup for coding agents.
Give your coding agent these entry points. They cover the supported public surface only.
## Machine-readable resources
- [llms.txt](/docs/ai/llms.txt): canonical URLs, supported surfaces, the public endpoint families, and the key pages to start from. The site root serves it too, at `/llms.txt`.
- [llms-full.txt](/docs/ai/llms-full.txt): the full text of every published page, the API reference included, in one file. Also at `/llms-full.txt`.
- Markdown pages: add `.md` to any docs URL (`/docs/reference/docs-for-ai.md`), or request the page with `Accept: text/markdown`. `/docs.md` lists every page. Each page's **Copy page** menu copies the same Markdown.
- [SKILL.md](/SKILL.md): an agent skill with SDP's rules for environments, API key grants, idempotency, and prepare versus execute. The sidebar's **Agent setup** menu has the install command for your agent. The agent-neutral one is `npx skills add solana-foundation/solana-developer-platform --skill sdp-docs`.
- [API reference](/docs/reference/api): every public operation, generated from the public OpenAPI contract.
- [OpenAPI JSON](https://api.solana.com/openapi.json): the public API contract itself.
- [API llms.txt](https://api.solana.com/llms.txt): the API's own discovery file, with its endpoint families and how to authenticate.
- [Postman collection](/docs/reference/postman-collection): the public API as a collection, generated from the same contract.
## Recommended ingestion order
1. Start with [llms.txt](/docs/ai/llms.txt).
2. Load [llms-full.txt](/docs/ai/llms-full.txt) when you need every page.
3. Use the [API reference](/docs/reference/api) and [OpenAPI JSON](https://api.solana.com/openapi.json) for exact request and response schemas.
4. Use the product guides for workflow order, operational limits, and supported patterns.
## Scope
Treat the public docs as the source of truth over internal implementation details. Hidden and internal-only route families are left out of every resource on this page.
## Coverage focus
- Wallets and custody
- API key management and projects
- Payments, transfers, and recurring payments
- DeFi and RWA strategies, customer-signed transactions, and positions
---
## Getting Started
### Set up your workspace
Source: https://platform.solana.com/docs/guides/setup-organization
> Create your account and organization, which comes with a Sandbox and a Production project.
Everything you do in SDP belongs to an **organization**, your team's workspace. An organization holds **projects**, and each project has its own API keys, wallets, and members. This guide creates your account and organization.
### 1. Create your account
Go to [platform.solana.com](https://platform.solana.com) and click **Create account**. The sign-in screen has the same button at the top right. When sign-up is closed, these buttons read **Join the waitlist** and **Request access** instead.
Enter your email address and a password to create your account.
### 2. Create your organization
After you sign in, **Name your organization** asks for an **Organization name**. Enter it and click **Create organization**. You become the organization's admin. If you've been invited to an organization, accept the invitation or pick the organization from the list instead.
To switch organizations or projects later, click your organization's name at the top of the sidebar.
### 3. Work in a project
Your organization comes with two projects: **Default Sandbox Project** on devnet and **Default Production Project** on mainnet. Build in the sandbox project. Production can't move money until SDP approves your organization; see [Go to mainnet](/docs/guides/go-to-mainnet). To read projects with an API key, see the [Projects API reference](/docs/reference/api/projects).
### 4. Next steps
- [Set up wallets](/docs/guides/setup-wallets) for signing transactions
- [Create API keys](/docs/developing-with-sdp/manage-api-keys) for programmatic access
- [Send your first payment](/docs/payments/send-basic-payment)
- [Go to mainnet](/docs/guides/go-to-mainnet) when your sandbox integration is ready
---
### Create a wallet
Source: https://platform.solana.com/docs/guides/setup-wallets
> Set up a custody provider and create wallets for signing transactions.
A **custody wallet** is a Solana keypair whose key a custody provider holds. SDP sends each signing request to the provider that holds the wallet's key. Set up a provider, then create wallets for transfers and other onchain operations.
## Supported providers
| Provider | Description |
| --- | --- |
| **Privy** | Embedded wallet infrastructure platform |
| **[Fireblocks](/docs/wallet-operations/fireblocks)** | Digital asset infrastructure company |
| **Coinbase CDP** | Embedded wallets for developers |
| **Para** | Wallet and authentication suite |
| **Turnkey** | Non-custodial wallet infrastructure platform |
| **DFNS** | Digital asset wallet infrastructure |
| **IBM Digital Asset Haven** | Institutional custody on IBM Digital Asset Haven |
| **Anchorage** | Regulated institutional crypto custody |
| **Utila** | Vault-backed wallet signing through service accounts |
- [Connect Fireblocks](/docs/wallet-operations/fireblocks): Connect your own Fireblocks workspace for production custody.
**Dashboard**
### 1. Open wallet setup
Open **Wallets** in the sidebar and click **Create wallet**. Once the project has wallets, the button reads **Create a wallet**. Only organization admins can create wallets. Complete [organization setup](/docs/guides/setup-organization) first if needed.
### 2. Choose a custody provider
Under **Which provider holds the keys?**, select a provider. You can't change it after the wallet exists. When your project can use only one provider, it's already selected and the question isn't shown.
### 3. Choose whose account holds the keys
If you can connect your own account for the provider, the page asks whose account holds the keys, such as **Whose Privy account holds the keys?** When there's a choice, nothing is preselected.
- **SDP's Privy account**: for trying SDP out in Sandbox. It has no wallet policy and nothing to set up.
- **Your own Privy account**: connect it with its app ID and secret. Production accepts only your own account; see [Go to mainnet](/docs/guides/go-to-mainnet).
### 4. Create the first wallet
With SDP's account, enter a **Wallet label** and click **Create wallet**. The wallet's page opens with its address and balance.
With your own account, keep or rename the **Credential label**, and enter your **Privy app ID** and **Privy app secret**. Under **Wallet details**, enter a **Wallet label**. Click **Connect and create wallet**: SDP checks the credentials with Privy, then creates the wallet, which appears on **Wallets**.

The first wallet in a provider account is a transfer wallet.
### 5. Create more wallets
On **Wallets**, click **Create a wallet** and select the provider if asked. The account question lists SDP's account where it's offered, each of your connections, and **Connect another Privy account**. A connection that isn't ready shows what it needs, with a link to fix it.
For SDP's account or an existing connection, enter a **Wallet label**, pick a **Purpose** (it starts on **Transfers**), and click **Create wallet**. **Connect another Privy account** works as in step 4.
A connection signs only with its current credentials. If you rotate or remove them while a request is signing, that request returns `409 CONFLICT` with `details.reason` set to `runtime_execution_unavailable`. Retry it as [Idempotency](/docs/developing-with-sdp/idempotency) describes for that operation. A transfer that recorded the failure replays it under the same `Idempotency-Key`, so send the transfer again with a new key.
**API**
#### 1. Set up the provider in the dashboard
Setting up a custody provider takes `wallets.configure`, and no API key can hold a `configure` action. An organization admin sets it up once per project, signed in to the dashboard (see the **Dashboard** tab), which calls `POST /v1/wallets/initialize` with that session. The same call with an API key returns `403 INSUFFICIENT_PERMISSIONS` with `details.action` set to `wallets.configure`.
Production projects take only your own provider account. `POST /v1/wallets/initialize` sets up SDP's account of the provider (Managed custody), so in a Production project it returns `403` with `details.reason` set to `custody_mode_not_allowed`. Connect your own account to the project in the dashboard instead. Then create wallets in that connection with `connectionId`, using an API key that holds `wallets.manage`:
```bash title="Terminal"
export IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST https://api.solana.com/v1/wallets \
-H "Authorization: Bearer sf_..." \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"connectionId": "cconn_123",
"label": "Treasury wallet"
}'
```
#### 2. Create more wallets
Every wallet names the provider account it lives under: `provider` for SDP's account of that provider (Managed), or `connectionId` for a connection your project added. Send exactly one of them; a body with both or neither returns `400`. There is no default account to fall back to.
**curl**
```bash
IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST 'https://api.solana.com/v1/wallets' \
-H "Authorization: Bearer $SDP_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "privy",
"label": "Treasury wallet",
"purpose": "transfer"
}'
```
**TypeScript**
```typescript
// Reuse this key when you retry this request.
const idempotencyKey = crypto.randomUUID();
const res = await fetch("https://api.solana.com/v1/wallets", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
"provider": "privy",
"label": "Treasury wallet",
"purpose": "transfer"
})
});
```
**Java**
```java title="CreateWallet.java"
String idempotencyKey = java.util.UUID.randomUUID().toString();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.solana.com/v1/wallets"))
.header("Authorization", "Bearer sf_...")
.header("Idempotency-Key", idempotencyKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("""
{
"provider": "privy",
"label": "Treasury wallet",
"purpose": "transfer"
}"""))
.build();
```
`purpose` is optional and defaults to `transfer`. Only a `transfer` wallet can be the source of a transfer, transfer batch, or recurring payment; any other purpose is refused with `403` and `details.reason` set to `wallet_purpose`. Reuse the same `Idempotency-Key` when you retry a wallet creation.
#### 3. List wallets
**curl**
```bash title="Terminal"
curl "https://api.solana.com/v1/wallets?view=summary" \
-H "Authorization: Bearer sf_..."
```
**TypeScript**
```typescript title="list-wallets.ts"
const response = await fetch("https://api.solana.com/v1/wallets?view=summary", {
headers: { "Authorization": "Bearer sf_..." },
});
const { data } = await response.json();
// data.wallets includes id, walletId, publicKey, provider, label, purpose, and status
```
**Java**
```java title="ListWallets.java"
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.solana.com/v1/wallets?view=summary"))
.header("Authorization", "Bearer sf_...")
.GET()
.build();
```
Every operation that signs names its wallet; there is no default wallet. Transfers take the wallet's `id` (`cwlt_…`) as `sourceCustodyWalletId`, and other modules' pages name their field. An API key limited to selected wallets lists the same `id` in `walletIds`.
```typescript title="transfer-request.ts"
const body = {
sourceCustodyWalletId: "cwlt_123", // `id` from GET /v1/wallets
destination: "7xKXz...9fGh",
token: "9aBCd...4eEf",
amount: "100.00",
};
```
---
## Tutorials
### End-to-end Payment Flow
Source: https://platform.solana.com/docs/tutorials/end-to-end-payment-flow
> Send a payment from wallet setup through settlement and reconciliation.
Follow one payment through its lifecycle:
1. **Create or pick the source wallet.** Create a custody wallet with [Set Up Wallets](/docs/guides/setup-wallets), or find one with `GET /v1/wallets`. Note its `id` (`cwlt_…`).
2. **Build the transfer.** Collect the destination address, token and amount, and check them before you send. See [Basic payment](/docs/payments/send-basic-payment).
3. **Send it.** Call `POST /v1/payments/transfers` with a new `Idempotency-Key`. SDP builds the transaction, has the source wallet's custody provider sign it, and submits it.
4. **Keep the identifiers.** Store the transfer `id` (`xfr_…`) and its `signature` with your own record.
5. **Wait for finality.** A successful request answers `finalized`. If it answers `processing`, read `GET /v1/payments/transfers/{transferId}` until it settles. See [Verifying a payment](/docs/payments/accept-verification).
6. **Reconcile.** Match the transfer to your ledger and update what your users see. See [Indexing and reconciliation](/docs/payments/accept-indexing).
The [Payments API reference](/docs/reference/api/payments) and [Wallets API reference](/docs/reference/api/wallets) have each endpoint's request and response.
---
## Developing with SDP
### Idempotency
Source: https://platform.solana.com/docs/developing-with-sdp/idempotency
> Retry mutations safely with Idempotency-Key, especially when moving money.
Clients time out, connections drop and workers die mid-run. Send an `Idempotency-Key` header so a retry returns the first attempt's answer instead of doing the work twice.
```bash title="Terminal"
curl -X POST https://api.solana.com/v1/payments/transfers \
-H "Authorization: Bearer sf_..." \
-H "Idempotency-Key: 0192f8a4-6c1e-7d4b-8f3a-b7e52c8d9f0a" \
-H "Content-Type: application/json" \
-d '{ ... }'
```
## Where keys apply
**Required.** These routes move money. A request without a key returns `400 IDEMPOTENCY_KEY_REQUIRED`, and nothing runs.
- `POST /v1/payments/transfers`
- `POST /v1/payments/transfer-batches`
- `POST /v1/payments/transfer-batches/{batchId}/retry`
- `POST /v1/payments/recurring-payments`
- `PATCH /v1/payments/recurring-payments/{id}` (an edit to an active schedule signs and broadcasts a replacement subscription)
- `POST /v1/payments/recurring-payments/{id}/activate`
- `POST /v1/payments/recurring-payments/{id}/cancel`
- `POST /v1/payments/recurring-payments/{id}/collect`
- `POST /v1/payments/recurring-payments/{id}/resume`
**Accepted.** The key is optional, and honored when sent.
- Wallets: `POST /v1/wallets`, `POST /v1/wallets/signer-check`, `PATCH /v1/wallets/{walletId}`, `DELETE /v1/wallets`
- Counterparties: `POST`, `PATCH` and `DELETE` on `/v1/counterparties` and on counterparty accounts
- Payment requests: `POST /v1/payments/requests` and `POST /v1/payments/requests/{paymentRequestId}/cancel`
**Not used.** `POST /v1/payments/transfer-batches/estimate` moves and stores nothing, so it does not read the key.
Modules with their own key handling say so on their pages, and each operation's API reference page lists the `Idempotency-Key` header when the route takes one.
## Choosing a key
- A key is 1–255 printable ASCII characters. Anything else returns `400`.
- Generate one key per logical operation (a UUIDv4 or UUIDv7 works well), store it with the operation before you send it, and send the same key on every retry of that operation. The API reference samples mint the key on its own line before the call: to retry, run the call again, not the line that mints the key.
- A new operation needs a new key, even when its request is identical to an earlier one.
- Keys are scoped to your organization, project, HTTP method and route, and bound to the credential that sent them. The same key value on two different routes is two separate keys.
A money request, saved with its key before the first send and retried under that key:
```typescript title="send-transfer.ts"
// Save the key with the operation first, so a restarted worker retries under it.
const payout = await db.payouts.create({ ...input, idempotencyKey: crypto.randomUUID() });
async function sendTransfer(payout: Payout): Promise {
for (let attempt = 1; attempt <= 8; attempt++) {
let response: Response;
try {
response = await fetch("https://api.solana.com/v1/payments/transfers", {
method: "POST",
headers: {
Authorization: "Bearer sf_...",
"Content-Type": "application/json",
"Idempotency-Key": payout.idempotencyKey,
},
body: JSON.stringify(payout.request),
});
} catch {
// The connection dropped: the transfer may have run, so retry under the same key.
await backoff(attempt);
continue;
}
if (!(await isRetryable(response))) return response;
await backoff(attempt, response.headers.get("Retry-After"));
}
throw new Error(`Transfer ${payout.id} is still unanswered: retry later with the same key`);
}
async function isRetryable(response: Response): Promise {
// A money route answers a same-key retry after a 5xx from the record the first attempt wrote.
if (response.status >= 500 || response.status === 429) return true;
if (response.status !== 409) return false;
const { error } = await response.clone().json();
return error?.code === "IDEMPOTENCY_KEY_IN_FLIGHT";
}
function backoff(attempt: number, retryAfter?: string | null): Promise {
const ms = Math.max(Number(retryAfter ?? 0) * 1000, 250 * 2 ** attempt);
return new Promise((resolve) => setTimeout(resolve, ms));
}
```
Every retry above sends the same body and the same key. Don't change the request to "fix" a retry: a different request under the key is `422 IDEMPOTENCY_KEY_REUSED`.
## What a retry returns
Access is checked before the key on every request, so a retry passes exactly the checks a new request passes, and a request refused for access never claims the key.
| Situation | Response |
| --- | --- |
| Same key, same request, original finished | The original status and body, with the response header `Idempotent-Replayed: true`. |
| Same key, original still running | `409 IDEMPOTENCY_KEY_IN_FLIGHT` with `Retry-After`. Wait, then retry with the same key. |
| Same key, original never recorded its outcome (it crashed) | Until 15 minutes plus the route's lease have passed since the original was sent (about 16 minutes, or about 20 for transfer batches), `409 IDEMPOTENCY_KEY_IN_FLIGHT` with `Retry-After`, as if it were still running. After that, transfers, transfer batches, recurring payments and `POST /v1/wallets` take over and answer from the record the original wrote, and a payment request cancel runs again. A batch whose original stopped before sending some recipients answers with those recipients `failed`: they were never sent, so send them again with `POST /v1/payments/transfer-batches/{batchId}/retry`. Every other route returns `409 IDEMPOTENCY_KEY_OUTCOME_UNKNOWN` with no `Retry-After`, to this and every later retry with the key: read the resource with `GET` to see whether it ran, then send again with a new key. |
| Same key, different request or different credential | `422 IDEMPOTENCY_KEY_REUSED`. Nothing runs. |
| Same key, but the credential has since lost access to the wallet or resource | `403`. The stored response is not returned. |
| Original was refused with `400`, `401`, `403`, `409` or `429` before it wrote anything | Nothing was stored. Fix the cause and retry with the same key; the request runs again. |
| A transfer or transfer batch failed after writing its record (for example `400 TRANSACTION_FAILED`, or `409` for a signer mismatch) | Stored: the same error, with `Idempotent-Replayed: true`. After 24 hours the record answers with the same status and code. The key is spent: send the request again with a new key to try again. |
| A recipient is archived before recurring-payment activation | A fresh activation returns `404` before writing an attempt or signing. A completed activation still replays its saved response. Recovery can confirm saved transactions, but refuses to sign a new step for an unavailable recipient. |
| A recurring-payment action failed | An action refused before it sent anything leaves the schedule as it was, except that a collection with an unavailable counterparty or account pauses it with `pausedReason: "destination_unavailable"` and returns `409` without signing. If a cancel sent its transaction and confirming it then failed, SDP keeps the attempt and the schedule stays `canceling` until the attempt is recovered; poll `GET /v1/payments/recurring-payments/{id}`. A `400`, `401`, `403`, `409` or `429` (such as `TRANSACTION_FAILED`) is not stored, and a `5xx` is not replayed, so a same-key retry runs the action again. Any other `4xx`, such as `422 SIGNING_REJECTED` or `404`, is stored and replayed with `Idempotent-Replayed: true` for 24 hours; send a new key to try again sooner. |
| A recurring-payment action meets another key's unfinished attempt on the schedule | Cancel and collect return `409` (already processing), not stored; read the schedule with `GET`. An interrupted collect finishes on its own from the transaction SDP saved before sending it. Resume sends nothing on chain and commits in one step, so it is never left half-done. An interrupted activate or cancel is resumed in the background from the last step SDP recorded; SDP first reads each step it may already have sent from the chain, and a step is not sent again when the chain shows it landed. An interrupted `PATCH` resumes only when that `PATCH` is retried with its key (see below). A `PATCH` may finish that attempt and then applies its own change. |
| A keyed recurring-payment cancel, resume or `PATCH` changes nothing (such as canceling a canceled schedule) | It still records its attempt, so a same-key retry, even after 24 hours, is answered from it and never runs over a later change. |
| Original returned a `5xx` | Stored and replayed: use a new key to try again. The routes that move money are the exception: a same-key retry runs again and answers from the record the first attempt wrote, so it never sends twice. That answer also carries `Idempotent-Replayed: true`. A same-key retry of a payment request cancel also runs again, and so does a `POST /v1/wallets` whose provider dedupes on the request ID (see [Creating wallets](#creating-wallets)). |
"Same request" means the same path, query parameters, body and credential. Recipient order in a transfer batch, and `"1.0"` versus `"1"` for an amount, do not change the request.
Keys are scoped to the credential that sent them. After API key rotation, the replacement key is a new credential: for 24 hours, a retry with the old `Idempotency-Key` gets `422`. Once the old record expires, that retry can run again and move money. A new `Idempotency-Key` can do the same. Before resending after rotation, read the resource with `GET` (for example, list transfers or read the recurring payment) to learn whether the original ran.
An organization that has been deleted or has lost production access is refused before the key is read: `401` or `403`, even for a retry of a request that completed.
## Replays are snapshots
A replay returns the response as it was first sent: a transfer stored as `processing` replays as `processing`. To see current state, read the resource with `GET`. Never re-send a request to learn its outcome.
An answer from the original record (a same-key retry after a `5xx` or the 24-hour window) reflects its current state. A recurring-payment lifecycle action may still be mid-transition; poll `GET /v1/payments/recurring-payments/{id}` until it settles.
An update left `updating` does not finish in the background. Retry its `PATCH` with the same key. Until the interrupted attempt is 15 minutes old, the retry gets `409 IDEMPOTENCY_KEY_IN_FLIGHT` with `Retry-After`. After that, it resumes from the last recorded step without resending one the chain shows landed.
## Retention
Keys are kept for 24 hours after the original request. After that, a same-key retry on most accepted routes starts a new request. On the routes that move money, the record the first attempt wrote still holds its key, so a late retry is answered from that record, or refused if the request differs; a failed transfer answers with the status and code it failed with.
## Creating wallets
One `POST /v1/wallets` key maps to one recorded wallet. Same-key retries return it, even after 24 hours; a different request under that key returns `422`. A provider that accepts a request ID also receives one derived from the key.
On the providers below, which dedupe on the request ID, a `5xx` is not stored. A same-key retry runs again and gets back the wallet the first attempt made, even when SDP failed to record it, for as long as the provider keeps the request ID:
| Provider | Wallets | Same-key retry after a `5xx` returns the first wallet |
| --- | --- | --- |
| **Fireblocks** | Custody Connection (`connectionId`) | Within 24 hours of the first attempt |
| **Privy** | Managed (`provider`) | Always |
| **Privy** | Custody Connection (`connectionId`) | Always |
| **Coinbase CDP** | Managed (`provider`) | Always |
On any other provider, a provider failure is stored as `503` for 24 hours. A new key is a new provider request.
An ambiguous `503` may mean the provider created a wallet before SDP received the result. Other providers do not dedupe on the request ID. If SDP wrote no wallet row, a same-key retry after 24 hours can create another wallet. Before retrying with any key, check `GET /v1/wallets` and, if needed, the provider account.
Provider request IDs and recorded-wallet lookups include the credential. After API key rotation and expiry of the 24-hour record, the old `Idempotency-Key` under the new API key becomes a new provider request and can create a second wallet. List wallets and check the provider account first.
## The dashboard and the Postman collection
The dashboard sends a key on every request to the routes that require one, and keeps it until it gets a definitive answer, so a retried click never moves money twice.
The [Postman collection](/docs/reference/postman-collection) saves one key per required operation in `idempotencyKey.`. It creates the key only when the variable is empty and clears it only after a final-payment `2xx`, a replayed refusal, or a `422` other than `IDEMPOTENCY_KEY_REUSED`.
While a transfer, batch, or schedule remains `pending` or `processing`, Postman keeps the key. Poll with `GET` instead of resending, then clear the variable when final. It also keeps the key after timeouts, `5xx`, `409`, `429`, and `400`, `401`, or `403` refusals; the original request may still have run.
`422 IDEMPOTENCY_KEY_REUSED` means the request differs from the original. Restore that request to retry. To start a new operation, first check with `GET` that the original settled, then clear the variable yourself.
## Recommendations
- Send a key on every mutation that accepts one, not only where it is required.
- Retry timeouts, connection errors and `409 IDEMPOTENCY_KEY_IN_FLIGHT` with backoff, reusing the key.
- Treat `409 IDEMPOTENCY_KEY_OUTCOME_UNKNOWN` as final for that key: check with `GET`, then use a new key.
- After a `5xx`, retry with the same key on the routes that move money, and on `POST /v1/wallets` when the provider dedupes on the request ID ([Creating wallets](#creating-wallets)); elsewhere, use a new key.
- Outside a key rotation, treat `422 IDEMPOTENCY_KEY_REUSED` as a bug in your client: the key was attached to two different operations.
- Persist the returned resource ID before moving to the next operation, so a restarted worker can resume without guessing.
---
### Authentication
Source: https://platform.solana.com/docs/developing-with-sdp/authentication
> Authenticate requests with a project-scoped API key, limited by its grants, wallet scope, and IP allowlist.
Send your API key in the `Authorization` header. The example reads it from `SDP_API_KEY`:
```bash
curl 'https://api.solana.com/v1/wallets' \
-H "Authorization: Bearer $SDP_API_KEY"
```
The `Bearer` prefix is optional; the header also accepts the bare key.
Each key carries three independent controls: grants, a wallet scope, and an IP allowlist. Together they give the key exactly the access its integration needs.
## Key scope
A key belongs to one **project** and inherits that project's environment: a key from a sandbox project reaches devnet, one from a production project mainnet-beta. The key itself doesn't name its environment; the dashboard shows it next to each key. A key reaches only its own project's resources. There is no cross-project or organization-wide key.
## Authorization layers
A request must clear every layer that is configured on its key:
| Layer | What it controls |
| --- | --- |
| **Grants** | The actions (`module.verb`) the key may perform. Each route needs exactly one; see [Grants by module](/docs/developing-with-sdp/manage-api-keys#grants-by-module) |
| **Wallet scope** | Which custody wallets SDP may sign with for the key: `all` in its project, or only the `selected` wallets in `walletIds`; see [Wallet access](/docs/developing-with-sdp/manage-api-keys#wallet-access) |
| **IP allowlist** | Optional IPv4 or IPv6 addresses and CIDR ranges the key may be used from. An organization-wide list under **Settings** > **API key access** applies to every key on top. Neither restricts signed-in dashboard members |
## Errors
Authentication and authorization failures are always machine-distinguishable: the code says what kind of refusal it is, and a `403` names what is missing in `error.details`.
| Condition | Status | Code | `error.details` |
| --- | --- | --- | --- |
| No API key on a keyed-only request | 401 | `UNAUTHORIZED` | |
| Malformed or unknown key | 401 | `INVALID_API_KEY` | |
| Key has been revoked, its organization was deleted, or its project was archived | 401 | `REVOKED_API_KEY` | |
| Key is live but its organization is suspended; the key works again once SDP resumes it | 403 | `FORBIDDEN` | `reason: "organization_suspended"` |
| Key past its expiry or rotation deadline | 401 | `EXPIRED_API_KEY` | |
| Request origin not in the key's or the organization's IP allowlist | 403 | `FORBIDDEN` | `reason: "ip_not_allowed"` |
| Route only a signed-in dashboard member may use, such as managing keys, members or the organization | 403 | `FORBIDDEN` | `reason: "human_session_required"` |
| Key lacks the route's action, or the action a signature it asks for needs | 403 | `INSUFFICIENT_PERMISSIONS` | `action` names the missing action |
| SDP refused to sign for the request (see below) | 403 | `FORBIDDEN` | `reason` names why |
| Per-key rate limit exceeded | 429 | `RATE_LIMITED` | |
### Signing refusals
SDP decides again each time it signs on a key's behalf, so a change to the key or the organization applies to the next signature, including one later in the same request. Nothing is signed or sent on refusal. `error.details.reason` is one of:
| `reason` | Why |
| --- | --- |
| `wallet_out_of_scope` | The custody wallet is outside the key's wallet scope |
| `wallet_purpose` | The custody wallet's purpose may not send payments: only a `transfer` wallet can be the source of a transfer, transfer batch, or recurring payment |
| `principal_not_live` | The key was revoked, expired, or passed its rotation deadline during the request |
| `organization_inactive` | The organization is suspended or deleted |
| `production_not_enabled` | The project is a production project and the organization does not have production access |
| `project_not_found` | The project no longer exists |
| `principal_missing` | No credential is attached to the movement |
| `movement_disabled` | SDP has disabled this kind of money movement for the project |
A key without the action a signature needs is refused with `INSUFFICIENT_PERMISSIONS` and `error.details.action`, exactly as at the route.
Each key is rate limited by its tier. [Rate limits](/docs/reference/conventions/rate-limits) lists the limits.
See [Manage API Keys](/docs/developing-with-sdp/manage-api-keys) for creating, rotating, and revoking keys.
---
### Manage API Keys
Source: https://platform.solana.com/docs/developing-with-sdp/manage-api-keys
> Create, rotate, and revoke API keys with explicit grants by module, a signing wallet scope, and zero-downtime rotation.
API keys authenticate SDP API requests. Each key belongs to a project, inherits its environment, and holds an explicit list of grants plus optional controls. [Authentication](/docs/developing-with-sdp/authentication) covers how SDP enforces them.
Keys are credentials, not administrators. Only a signed-in organization admin creates, changes, rotates, or revokes a key, in the dashboard. An API key can't manage keys, the organization, its members, or project members, whatever it's granted: it's refused with `403 FORBIDDEN` and `error.details.reason` set to `human_session_required`.
SDP returns the full secret once, at creation and at rotation, and stores only a keyed hash of it. A lost key can't be recovered, only rotated or replaced. Every key records `lastUsedAt`, so you can find stale keys before you revoke them.
## Key format
Keys use the shared Solana Foundation format, `sf__`:
| Part | What it is |
| --- | --- |
| `` | 16 hex characters. Not secret: safe to log, and shown in the dashboard to tell keys apart |
| `` | 43 base62 characters (256 random bits) |
| `` | 6 base62 characters, so a mistyped key is refused at once and secret scanners can match a leaked one |
The format doesn't name an environment: a key reaches the network of the project it was created in (sandbox: devnet, production: mainnet-beta). Keys created before this format, prefixed `sk_test_` or `sk_live_`, keep working until they are revoked.
## Grants by module
Every API route performs exactly one **action**, named `module.verb`, and a key may call a route only when it holds that action. A key holds an explicit list of grants: there is no role and no wildcard.
| Verb | What it allows |
| --- | --- |
| `read` | View the module's records |
| `manage` | Create and edit records reversibly: drafts, pausing, canceling |
| `execute` | Anything irreversible or that leaves SDP: onchain transactions, provider calls, moving money, and changing where money goes, such as a payout destination or a recurring amount |
`manage` and `execute` each include the same module's `read`. Nothing else implies another action: `execute` doesn't include `manage`, and a grant on one module gives nothing on another. Some modules add verbs of their own. These are the actions a key can be granted on this deployment:
| Module | Actions | Covers |
| --- | --- | --- |
| `transfers` | `transfers.read`, `transfers.execute` | Single transfers from a custody wallet |
| `transfer_batches` | `transfer_batches.read`, `transfer_batches.execute` | Batches of transfers, and their estimates |
| `payment_requests` | `payment_requests.read`, `payment_requests.manage` | Payment requests and their payment links |
| `counterparties` | `counterparties.read`, `counterparties.manage`, `counterparties.execute` | Counterparties, their accounts, and provider requirements |
| `transactions` | `transactions.read` | The unified transaction list; each row also needs the read of its own module |
| `recurring` | `recurring.read`, `recurring.manage`, `recurring.execute` | Recurring payments and their collection attempts; on beta, also subscription plans and subscriptions |
| `wallets` | `wallets.read`, `wallets.manage`, `wallets.execute` | Custody wallets and their balances |
| `projects` | `projects.read` | Projects and provider availability |
| `earn` | `earn.read`, `earn.manage`, `earn.execute` | Earn strategies, deposits, withdrawals and programs |
Some actions are never granted to a key, because keys are credentials, not administrators: key, member, organization, and project administration, and every provider, credential, or connection setup (`configure`). A create or update naming one is refused with `400`. So is a grant on a module this deployment doesn't run; a key that already holds one keeps it when an update sends it back.
A request without the route's action is refused with `403 INSUFFICIENT_PERMISSIONS`, and `error.details.action` names the missing action. Nothing runs or is recorded on refusal. Grants are read on every request, so a change applies to the key's next request.
You can grant a key only actions you hold yourself. **Everything** in the dashboard writes out every action available when the key is created; an action added to SDP later isn't granted to existing keys.
## Wallet access
A key's wallet scope decides which custody wallets SDP may sign with on its behalf:
| `walletScope` | Signs with |
| --- | --- |
| `all` | Every custody wallet in the key's project |
| `selected` | Only the wallets listed in `walletIds`. With none listed, the key signs with no custody wallet; SDP's fee sponsor is outside wallet scope |
`walletIds` are custody wallet IDs, the `id` (`cwlt_…`) that `GET /v1/wallets` returns, not a provider's wallet ID. A key keeps the exact wallets you listed: a wallet created later, even one reusing a provider ID, is never added.
SDP checks the scope each time it signs, together with the key's grants and whether it's still active. A narrowed scope or a revoked key stops the next signature, including one later in the same request. A signature outside the scope is refused with `403 FORBIDDEN` and `error.details.reason` set to `wallet_out_of_scope`; nothing is signed or sent.
The scope narrows what a key may sign with, not what it may read. A `selected` key's wallet list (`GET /v1/wallets`) and wallet detail reads show only its wallets; every other read, such as transfers, batches, and recurring payments, covers the whole project. Give a separate project to an integration that must not see another's records.
For recurring payments the scope also bounds writes that sign nothing. A `selected` key may create, change, cancel, activate, collect, or resume only a recurring payment whose source wallet it lists, and may move one only to another listed wallet, because whoever activates a pending payment signs the terms it was last given. Anything else is refused with `wallet_out_of_scope` before it is recorded. Reading and listing recurring payments still cover the whole project.
The dashboard starts a key that holds any `execute` action on **Selected wallets**, so it can sign with nothing you didn't give it. Choose **All wallets** to widen it.
## Recurring payments outlive the key
A recurring payment is collected on schedule as its own mandate, not as the key or member that activated it. SDP checks that the payment is active, that it pays from its own source wallet, that its counterparty and account are active in the project, and that the organization may move money. Revoking or narrowing the key that activated it doesn't stop it, so rotating a key never interrupts a customer's schedule.
Archiving the counterparty archives its active accounts too, and new transfers
and batches can't use them. Archival that commits before a new collection is
admitted prevents it from signing and pauses the recurring payment with
`pausedReason: "destination_unavailable"`. A collection admitted earlier may
still finish or recover, and earlier requests still replay their recorded
outcomes.
The dashboard shows the pause reason and offers Cancel. Paused schedules can't
be edited, so cancel the schedule and create a new one with an active recipient.
## Create a key
1. Open **API keys** in the sidebar. You start with an empty list.

2. Click **Create new API key**. Once the project has keys, the button reads **New API key**. Fill in each step, then click **Continue**:
- **Details**: a **Name** (for example, "CI deploy key"), an optional **Expiration**, and optional **Allowed IP addresses**, one IPv4 or IPv6 address or CIDR range per line. An empty list allows any address. The **Environment** is the project's.
- **Permissions**: a checkbox per verb for each module (Read, Manage, Execute); ticking Manage or Execute includes Read. **Everything** ticks every offered action, and **Clear all** clears them. Grant at least one.
- **Access**: **All wallets**, or **Selected wallets** with at least one wallet picked.

3. On **Review API key**, check the summary and click **Create key**.

4. In the **API key generated** dialog, click **Copy** and save the key. It isn't shown again.
5. Click **Dismiss**. The key appears in the table with its prefix, permissions, wallet access, environment, and status.

To change a key's details, permissions, or wallets later, open its **Actions** menu and click **Edit API key**.
## Rotate a key
Rotation replaces the secret without downtime. The new key is a fresh record with the old key's grants, IP allowlist, wallet scope, and expiry. The old key keeps working until its grace deadline, 24 hours after a dashboard rotation; after that, it fails with `EXPIRED_API_KEY`.
1. In the API keys table, open the key's **Actions** menu, click **Rotate key (24h grace)**, then confirm with **Rotate key**.

2. Copy the new key from the **API key generated** dialog. It's shown once.
## Revoke a key
Open the key's **Actions** menu and click **Delete key**. Type the key's name to confirm, then click **Delete key**. The key stops working on its next request and can't be restored. Deleting a rotated key during its grace period ends the overlap early: the replacement keeps working.
If the key may be compromised, tick **Also cancel recurring payments this key started** (`"cancelRecurringPayments": true` in the API). Otherwise its recurring payments keep collecting. The response lists canceled schedules and any you must cancel individually. A recurring payment answers to the key that activated or last resumed it, or last changed its source wallet, recipient, token, amount, or period. Metadata and next collection date changes don't change that control.
---
## Wallet operations
### Wallet balances
Source: https://platform.solana.com/docs/wallet-operations/balances
> Per-wallet token balances with optional USD valuation via SDP.
`GET /v1/payments/wallets/{walletId}/balances` returns a custody wallet's native **SOL** balance and every **SPL token** it holds, with raw and human-readable amounts, decimals, and a USD valuation where SDP has a price. SOL always comes first, as `token: "SOL"` with `mint: "So11111111111111111111111111111111111111112"`. Token accounts with a zero balance are left out.
## Request
`walletId` is the provider wallet ID: the `walletId` field from `GET /v1/wallets`, not the wallet's `id` (`cwlt_…`) or its address. The request needs an API key with `wallets.read`; a key limited to selected wallets reads only those.
**curl**
```bash
curl 'https://api.solana.com/v1/payments/wallets/privy_wallet_123/balances' \
-H "Authorization: Bearer $SDP_API_KEY"
```
**TypeScript**
```typescript
const res = await fetch("https://api.solana.com/v1/payments/wallets/privy_wallet_123/balances", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`
}
});
```
**Java**
```java title="GetWalletBalances.java"
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.solana.com/v1/payments/wallets/privy_wallet_123/balances"))
.header("Authorization", "Bearer sf_...")
.GET()
.build();
```
If SDP can't read the SOL balance or any token balance, the request returns `503` instead of partial or zero balances. To read state no older than a slot you already saw, pass `minimumSlot`; the response echoes it in `data.balanceReadContext.minimumSlot`.
## Response shape
The payload sits in the standard `data` / `meta` envelope, with the balances in `data.walletBalances.balances`:
```json title="response.json"
{
"data": {
"walletBalances": {
"walletId": "privy_wallet_123",
"address": "3xYZa...2aBc",
"balances": [
{
"token": "SOL",
"mint": "So11111111111111111111111111111111111111112",
"amount": "2500000000",
"uiAmount": "2.5",
"decimals": 9
},
{
"token": "USDC",
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "1500000000",
"uiAmount": "1500",
"decimals": 6,
"usdPrice": 1,
"usdValue": 1500
}
]
}
},
"meta": { "requestId": "req_...", "timestamp": "2026-05-18T00:00:00.000Z" }
}
```
Per-balance fields:
| Field | Notes |
| --- | --- |
| `token` | Token symbol if known, otherwise the onchain mint. |
| `mint` | Onchain mint address. |
| `amount` | Raw amount in the smallest unit (a string, to preserve precision). |
| `uiAmount` | Human-readable decimal string: `amount / 10^decimals`. |
| `decimals` | The token's decimal count. |
| `usdPrice` | Per-token USD price; omitted when no price is available. |
| `usdValue` | Total USD value (`usdPrice * uiAmount`); omitted when no price is available. |
| `confidential` | Reserved for confidential transfer balances; absent for standard balances. |
## Use cases
- **Account dashboards:** show a custody wallet's contents to its operator.
- **Pre-transfer balance checks:** confirm a wallet holds enough of a token before you start a payout batch.
- **Treasury accounting:** take periodic snapshots into an internal ledger.
## Pricing freshness
SDP prices balances only on mainnet. In a sandbox project on devnet, `usdPrice` and `usdValue` are always absent.
`usdPrice` is a display estimate from Tokens.xyz. SDP caches each price for up to 60 seconds. Some source snapshots refresh hourly; SDP omits snapshots older than two hours or without a timestamp. The snapshot timestamp doesn't prove when its underlying price was observed.
Use an executable quote for a trade and an independently verified valuation for accounting. The onchain `amount` and `uiAmount` remain the balance values. When pricing is unavailable, `usdPrice` and `usdValue` are absent: the omission means "unknown," not "$0.00."
## Compared with onchain RPC
You can also list SPL token balances by calling `getTokenAccountsByOwner` on a Solana RPC node and decoding the token-account data. Use SDP's balances endpoint for:
- **Token resolution:** SDP resolves the mint to a symbol when it knows one; raw RPC returns mints only.
- **USD pricing:** RPC has no prices.
- **One fewer dependency:** no separate RPC integration when you already call SDP.
Go to RPC directly for:
- **State at an exact slot:** for an audit that needs the wallet as of one slot. SDP's `minimumSlot` sets only a lower bound.
- **Confidential transfer balances:** full extension support is RPC-side.
- **Latency-sensitive paths:** RPC is one hop closer to the cluster.
## Related
- [Payouts and disbursements](/docs/payments/send-payouts): check balances before running a batch.
- [Create a wallet](/docs/guides/setup-wallets): provision the wallets whose balances this endpoint reports.
---
## Payments
### How payments work on SDP
Source: https://platform.solana.com/docs/payments/concepts
> Custody wallets, tokens, amounts, fees and the transfer record behind every SDP payment.
An SDP payment moves tokens from one of your custody wallets to a Solana address, and SDP records it as a transfer. This page covers the pieces the rest of the section builds on.
## Custody wallets
Every payment leaves from a **custody wallet** in your organization, optionally scoped to a project. A custody provider holds the wallet's key, and SDP asks that provider to sign. Limit what an API key can do with a wallet through [grants](/docs/developing-with-sdp/manage-api-keys#grants-by-module) and [wallet access](/docs/developing-with-sdp/manage-api-keys#wallet-access).
Create wallets first: see [Set Up Wallets](/docs/guides/setup-wallets). A transfer request names its source by the wallet's SDP `id` (`cwlt_…`) in `sourceCustodyWalletId`. Responses return that id as `custodyWalletId` and keep the provider's own wallet id in `providerWalletId`. The **destination** is the recipient's Solana address.
## Tokens and amounts
Pass `token` as `SOL` for native SOL, a well-known symbol such as `USDC`, or an SPL mint address. Pass a custom token by its mint. SDP resolves a symbol to its mint on your project's cluster and stores the mint on the transfer. The `token` filter on `GET /v1/payments/transfers` takes the same forms, and a well-known token matches whether you filter by symbol or by mint.
Amounts are decimal strings in whole-token units: `"100.00"` is 100 tokens, whatever the mint's decimals. Every amount you send SDP takes this form, and SDP converts it to base units when it builds the transaction. Wallet balance responses return both forms: `amount` in base units and `uiAmount` as a decimal string.
## Transfer execution
`POST /v1/payments/transfers` runs the whole transfer on the server. SDP builds the transaction, has the source wallet's custody provider sign it, submits it to your project's cluster, and waits for the cluster to finalize it before it answers.
## Fees and sponsorship
SDP pays the network fee, and the rent for any new recipient token account, on each transfer it signs from a Privy wallet. Those wallets need only the tokens they send, with no SOL for fees. Sponsorship has limits: when the sponsor can't pay, SDP refuses the transfer rather than charge the source wallet. A provider that submits its own transfers pays their fees from its own account, and SDP does not sponsor them.
## The transfer data model
Every transfer SDP sends or records is a `Transfer`. The fields most flows use:
| Field | Meaning |
| --- | --- |
| `id` | The SDP transfer id (`xfr_…`). Use it in API paths. |
| `status` | `processing`, `confirmed`, `finalized` or `failed` for a wallet transfer. See [Verifying a payment](/docs/payments/accept-verification). |
| `direction` | `outbound` (you paid) or `inbound` (you were paid). |
| `source`, `destination` | Solana addresses. |
| `counterpartyId` | The counterparty (`cpty_…`) the transfer is tied to, such as a [transfer batch](/docs/payments/send-payouts#native-transfer-batches) recipient or a transfer sent with `counterpartyAccountId`. |
| `token`, `amount` | What moved and how much. `token` is a mint address (some older records hold a symbol); `amount` is a decimal string such as `"100.00"`. |
| `memo` | Your note, up to 256 characters. SDP stores it; it is not written on chain. See [Payment with memo](/docs/payments/send-payment-with-memo). |
| `signature` | The Solana transaction signature, once there is one. Unique across SDP transfers. |
| `slot`, `blockTime`, `confirmedAt` | The slot the transaction landed in, its block time, and when SDP saw it land. `blockTime` is often `null` on transfers SDP sends. |
| `fee` | The network fee in lamports, when known. |
| `error` | Why the transfer failed. |
An outbound transfer starts at `processing`, and a successful `POST` usually answers `finalized`. See [Verifying a payment](/docs/payments/accept-verification#status-lifecycle) for every status and what it means.
## What SDP does and does not give you
You get what you need to **send**, **track** and **reconcile** payments transfer by transfer: status reads, filtered lists, wallet balances, `Idempotency-Key` replay, and a unique `signature` per transfer (see [Basic payment](/docs/payments/send-basic-payment#deduplication)). The public API has no checkout session or invoice object. [Accept payments](/docs/payments/accept-overview) covers the dashboard's payment links and how to build checkout on transfers.
## Related
- [Payments API reference](/docs/reference/api/payments): every payments endpoint.
- [End-to-end payment flow](/docs/tutorials/end-to-end-payment-flow): the same lifecycle as a step-by-step tutorial.
---
## Payments: Send Payments
### Basic payment
Source: https://platform.solana.com/docs/payments/send-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
Specify the source, destination, token, and amount.
**UI**
Go to **Payments** in the sidebar. The overview shows your **Available balance**, recent **Activity**, and the **Move money** actions.

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.


**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",
};
```
Submit the transfer. SDP signs with the source custody wallet and submits the transaction.
**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).
Check that the transfer settled.
**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"
```
## 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.
---
### Payment with memo
Source: https://platform.solana.com/docs/payments/send-payment-with-memo
> Attach an order id, invoice number or customer reference to an SDP transfer with the memo field.
Set `memo` on `POST /v1/payments/transfers` to store a note of up to 256 characters with the transfer. SDP returns it whenever you read or list that transfer, so your reconciliation can map each transfer back to a row in your own database.
## When to use a memo
Use a memo for a correlation value your backend already has when it sends the payment:
- **Order or invoice ids**: `order_2026-05-14_4837`, so the transfer ties to a checkout row.
- **Customer references**: your customer or account id, for support and reconciliation.
- **Run ids**: one payout run's id across many outbound transfers.
SDP stores the memo on its own record. It does **not** add a Solana memo instruction to the transaction, so block explorers don't show it. See [Memos and Solana Pay references](#memos-and-solana-pay-references) for on-chain correlation.
## API usage
**curl**
```bash title="Terminal"
curl -X POST https://api.solana.com/v1/payments/transfers \
-H "Authorization: Bearer sf_..." \
-H "Idempotency-Key: order-2026-05-14-4837" \
-H "Content-Type: application/json" \
-d '{
"sourceCustodyWalletId": "cwlt_123",
"destination": "7xKXz...9fGh",
"token": "9aBCd...4eEf",
"amount": "100.00",
"memo": "order_2026-05-14_4837"
}'
```
**TypeScript**
```typescript title="send-payment-with-memo.ts"
const response = await fetch(
"https://api.solana.com/v1/payments/transfers",
{
method: "POST",
headers: {
"Authorization": "Bearer sf_...",
"Idempotency-Key": "order-2026-05-14-4837",
"Content-Type": "application/json",
},
body: JSON.stringify({
sourceCustodyWalletId: "cwlt_123",
destination: "7xKXz...9fGh",
token: "9aBCd...4eEf",
amount: "100.00",
memo: "order_2026-05-14_4837",
}),
}
);
const { data } = await response.json();
```
**Java**
```java title="SendPaymentWithMemo.java"
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.solana.com/v1/payments/transfers"))
.header("Authorization", "Bearer sf_...")
.header("Idempotency-Key", "order-2026-05-14-4837")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("""
{
"sourceCustodyWalletId": "cwlt_123",
"destination": "7xKXz...9fGh",
"token": "9aBCd...4eEf",
"amount": "100.00",
"memo": "order_2026-05-14_4837"
}"""))
.build();
```
The memo comes back on the transfer:
```json title="response.json"
{
"data": {
"transfer": {
"id": "xfr_...",
"memo": "order_2026-05-14_4837",
"status": "finalized",
"signature": "5xR1...",
...
}
}
}
```
`memo` takes up to 256 characters of UTF-8 text. Sanitize it on the way in if your downstream systems are stricter.
## Memos and Solana Pay references
A **Solana Pay reference** is the other way to correlate a payment: a unique public key per order, added to the transfer instruction. Anyone can then find and verify the payment on chain without SDP's database, and the payer can build the transfer from a Solana Pay URL.
SDP uses references for dashboard [payment requests](/docs/payments/accept-overview#payment-requests): each request gets its own, and SDP watches it to see when the request is paid. A transfer you send through the API carries no reference, and transfer records don't expose one. To read a reference from any transaction, fetch it from Solana RPC by its `signature`.
Rule of thumb: use a memo when **you** send the transfer (payouts, internal moves), and a reference when **someone else** does (customer checkout, donations).
## Finding transfers by memo
`GET /v1/payments/transfers` and `GET /v1/payments/transfers/{transferId}` return `memo`. To find transfers by memo, pass `search` with 3 or more characters: it matches part of the memo, as well as ids, signatures, addresses and counterparty names. See [Indexing and reconciliation](/docs/payments/accept-indexing) for query patterns.
## Related
- [Basic payment](/docs/payments/send-basic-payment): the transfer endpoint.
- [Payouts and disbursements](/docs/payments/send-payouts): tag every transfer in a run with one memo.
- [Accept overview](/docs/payments/accept-overview): payment requests and references for inbound payments.
---
### Payouts and disbursements
Source: https://platform.solana.com/docs/payments/send-payouts
> Pay many recipients in one batch, retry without paying anyone twice, and reconcile a payout run with SDP.
A payout run pays many recipients at once: marketplace sellers, payroll, affiliates, royalties. Use `POST /v1/payments/transfer-batches` when every recipient is a saved counterparty account. To pay arbitrary Solana addresses, call `POST /v1/payments/transfers` once per recipient and reconcile the transfer ids it returns.
## Native transfer batches
One request creates a durable batch, chunks its recipients into Solana transactions, and returns the batch, recipient, and transfer records:
```bash
curl -X POST https://api.solana.com/v1/payments/transfer-batches \
-H "Authorization: Bearer sf_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f0c9e7a-5b1d-4c2e-9a8f-6d4b2e1c0a97" \
-d '{
"externalId": "payroll-2026-05",
"sourceCustodyWalletId": "cwlt_123",
"token": "9aBCd...4eEf",
"recipients": [
{
"externalId": "row_4837",
"counterpartyId": "cpty_4837",
"counterpartyAccountId": "cpacct_4837",
"amount": "150.00"
}
]
}'
```
Each recipient names a counterparty account (`cpacct_…`) that is an active `crypto_wallet` account in the project. `counterpartyId` is optional; if you pass it, it must be that account's counterparty. An unknown, archived or mismatched account returns `404`. An account whose address changes while the batch is being prepared returns `409 TRANSFER_BATCH_DESTINATION_CHANGED` naming it in `error.details.counterpartyAccountIds`; nothing is sent, so send the request again. A batch takes up to 500 recipients.
The two ids do different jobs:
- **`Idempotency-Key`** (required) identifies this one request. Reuse it to retry that exact request after a timeout; recipient order does not change the request. Any unique value works, such as a UUID.
- **`externalId`** (required: a batch without one returns `400`) identifies the payout run. Derive it from your own data, such as the pay period, so every attempt at the run carries the same value. See [One batch per payout run](#one-batch-per-payout-run).
Use `GET /v1/payments/transfer-batches/{batchId}` to inspect recipient and chunk outcomes.
Batch execution does not add a priority fee. The estimate reports
`priorityFeeLamports: "0"`.
### One batch per payout run
The batch `externalId` names the payout run, and it is unique per project and source wallet. Whatever `Idempotency-Key` it carries, a batch with an `externalId` already used on that wallet never pays anyone again:
- The same request, from the same API key, returns the existing batch as it stands. A worker that restarts with a fresh key gets the original run back.
- Any other request (different recipients or amounts, or another API key) returns `409 TRANSFER_BATCH_RUN_EXISTS` with the existing batch in `error.details.batchId`. A replay only goes to the API key that sent the run, so after a key rotation the new key gets the 409: read the batch by its id instead of resending.
Derive the `externalId` from data you already have, so a worker that crashes and restarts computes the same value without storing anything: `payroll-2026-05`, `invoice-run-8812`. Never generate it at random: a random value is new on every attempt and protects nothing. New recipients go in a new run with a new `externalId`, never in a renamed copy of a run that was already sent.
```ts title="payroll-worker.ts"
// Safe to run again after a crash: the run's id comes from the period, not from storage.
const externalId = `payroll-${period}`;
const response = await fetch("https://api.solana.com/v1/payments/transfer-batches", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SDP_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({ externalId, sourceCustodyWalletId, token, recipients }),
});
const body = await response.json();
// 200: this run's batch, new or replayed. 409 TRANSFER_BATCH_RUN_EXISTS: the run was
// already sent with a different body; read error.details.batchId instead of resending.
// A 5xx may come after the run was sent: its outcome is unknown, so run again with the
// same externalId, which returns the batch if it exists. A 4xx (a 409
// AMOUNT_BELOW_RENT_MINIMUM, say) is refused before anything is sent: report it.
let batchId: string;
if (response.ok) {
batchId = body.data.batch.id;
} else if (body.error.code === "TRANSFER_BATCH_RUN_EXISTS") {
batchId = body.error.details.batchId;
} else if (response.status >= 500) {
throw new Error(`Payout run ${externalId} has an unknown outcome: run it again`);
} else {
throw new Error(`Payout run ${externalId} was refused: ${body.error.code}`);
}
```
### Recovering from a partial failure
A batch with `failed` recipients paid some recipients and not others. A `failed` recipient was not paid: its transaction never reached the network, or the network rejected it. Its `error` says why; when the source wallet could not cover the amount, it reads "The wallet used for this payment does not have enough funds", so fund the wallet before you retry. To finish the run, retry the batch:
```bash
curl -X POST https://api.solana.com/v1/payments/transfer-batches/{batchId}/retry \
-H "Authorization: Bearer sf_..." \
-H "Idempotency-Key: 8b2e4f1c-7d3a-4e9b-a6c5-1f0d9e8c7b6a"
```
The retry sends only the batch's `failed` recipients, to the addresses the batch recorded for them, from the same wallet, and returns the batch with those recipients `processing` again. Recipients still `pending` or `processing` may still land, so they are never sent again. Each resent recipient keeps its row in the batch and links to its new transfer.
- Use a new `Idempotency-Key` for each retry you mean to send. The same key never sends again, including after a timeout or a server error, so resend a lost answer under the same key. It returns the saved response while that is stored, which can still show recipients `processing`: read the batch with `GET /v1/payments/transfer-batches/{batchId}` for its current state. If recipients fail again later, a retry under that old key sends nothing.
- Any API key that may send from the batch's wallet can retry it, not only the one that created it.
- A batch with nothing `failed` returns `409 TRANSFER_BATCH_NOTHING_TO_RETRY`, and a retry that loses to a concurrent one returns `409 TRANSFER_BATCH_RETRY_CLAIMED`. Nothing is sent. While a batch is `processing`, a recipient its request never sent becomes `failed` only once that request can no longer send it, so retry after the batch settles.
- A failed recipient whose counterparty account changed its address since the batch was sent is not paid either address: the retry returns `409 TRANSFER_BATCH_DESTINATION_CHANGED` naming it in `error.details.recipientIds`, and nothing is sent. Send that recipient in a new batch.
- A native SOL recipient paid less than the rent-exempt minimum at an address with no account fails again for the same reason, so the retry is refused with `409 AMOUNT_BELOW_RENT_MINIMUM` and that recipient stays `failed`. Send it in a new batch with a larger amount.
- Only recipients that are still `failed` are ever sent, so a retry under a new key never pays anyone twice.
Never resend the whole batch under a new `Idempotency-Key` to finish it: that pays the recipients who already succeeded again.
### The SOL rent-exempt minimum
A Solana wallet holds either nothing or at least the rent-exempt minimum (about 0.00089 SOL), and the network refuses a native SOL transfer that would break that, along with every recipient in the same transaction. SDP checks before it sends:
- A recipient whose address has no account must receive at least the minimum.
- The source wallet must end with nothing or at least the minimum. Chunks land in any order, so a batch that sends the wallet's whole balance also needs every chunk to move at least the minimum.
`POST /v1/payments/transfer-batches/estimate` lists what would fail in `estimate.warnings` (`AMOUNT_BELOW_RENT_MINIMUM` per recipient, `SOURCE_BELOW_RENT_MINIMUM` for the source). `POST /v1/payments/transfer-batches` refuses the batch with `409 AMOUNT_BELOW_RENT_MINIMUM`, naming `details.recipientIndexes` and `details.sourceRemainder`, before anything is written or sent. The 409 is not stored under the `Idempotency-Key`, so the same request runs again once it would clear. Token batches are unaffected: SDP funds a missing token account's rent (`tokenAccountRentLamports`).
### Token account rent per transaction
SDP funds new recipient token accounts up to a per-transaction rent limit. A chunk includes only as many new accounts as that limit covers; existing accounts are packed by count and size. The estimate's `transactionCount` is the number of transactions create submits. If one account exceeds the limit, create is refused with `409 SPONSORED_RENT_OVER_CAP` (`details.recipientIndex`) before anything is written or sent. Create that token account first.
## Paying recipients one transfer at a time
To pay destinations that are not saved counterparty accounts, send one transfer per recipient instead of a batch:
1. **Resolve recipients** in your application: the rows to pay, each with a destination address and an amount.
2. **Pick a run id** you control, such as `payout_2026-05-14_marketplace_sellers`.
3. **Send** each recipient a `POST /v1/payments/transfers` with the destination, amount, token, and a memo that holds the run id and the row (`payout_2026-05-14_marketplace_sellers/row_4837`).
4. **Store** the returned transfer `id` and `status` with the recipient row, so you can read the status again later.
5. **Reconcile** once every row is sent: list `GET /v1/payments/transfers?direction=outbound&from=…&to=…` and check that every row has a matching `finalized` transfer.
Send transfers sequentially or in modest concurrency (4–8 in flight); high concurrency increases the chance of priority-fee contention and blockhash expiry without much throughput gain.
To tie a single transfer to a saved counterparty account, pass `counterpartyAccountId` with it. See [Pay a counterparty](/docs/payments/send-basic-payment#pay-a-counterparty).
### Dedup and retries for single transfers
Two layers keep a worker that dies mid-run from paying anyone twice:
1. **Idempotency keys**: `POST /v1/payments/transfers` requires an `Idempotency-Key`. Give each recipient row one stable key and reuse it for retries. A retry with the same key and request returns the original transfer. Reusing the key for a different request returns `422 IDEMPOTENCY_KEY_REUSED`, and a retry while the original is still running returns `409 IDEMPOTENCY_KEY_IN_FLIGHT`.
2. **Application mapping**: store each row's transfer `id` before you move to the next row. On restart, retry with the row's same key, or skip rows that already have a transfer `id`.
A new key is a new payment, even with an identical body. The `signature` is unique across SDP transfers once set: use it to cross-check another indexer, not to retry.
**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: payout-row-4837" \
-d '{
"sourceCustodyWalletId": "cwlt_123",
"destination": "7xKXz...9fGh",
"token": "9aBCd...4eEf",
"amount": "150.00",
"memo": "payout_2026-05-14_marketplace_sellers/row_4837"
}'
```
**TypeScript**
```typescript title="payout-recipient.ts"
async function payoutRecipient(row: PayoutRow): Promise {
if (row.transferId) return row.transferId; // already submitted
const response = await fetch(
"https://api.solana.com/v1/payments/transfers",
{
method: "POST",
headers: {
"Authorization": "Bearer sf_...",
"Content-Type": "application/json",
"Idempotency-Key": row.idempotencyKey,
},
body: JSON.stringify({
sourceCustodyWalletId: row.sourceCustodyWalletId,
destination: row.destination,
token: row.token,
amount: row.amount,
memo: `${row.batchId}/row_${row.id}`,
}),
}
);
const { data } = await response.json();
// The transfer record is under data.transfer (standard SDP success envelope).
await row.persistTransferId(data.transfer.id); // persist BEFORE returning
return data.transfer.id;
}
```
**Java**
```java title="PayoutRecipient.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", idempotencyKey)
.POST(HttpRequest.BodyPublishers.ofString("""
{
"sourceCustodyWalletId": "%s",
"destination": "%s",
"token": "%s",
"amount": "%s",
"memo": "%s/row_%s"
}""".formatted(sourceCustodyWalletId, destination, token, amount, batchId, rowId)))
.build();
```
## Reconciling a run
After the loop finishes, list all outbound transfers in the run's time window and tally against your recipient table:
**curl**
```bash title="Terminal"
curl "https://api.solana.com/v1/payments/transfers?direction=outbound&from=2026-05-14T00:00:00Z&to=2026-05-14T23:59:59Z&pageSize=100" \
-H "Authorization: Bearer sf_..."
```
**TypeScript**
```typescript title="list-outbound-transfers.ts"
async function* listOutboundTransfers(from: string, to: string) {
let page = 1;
while (true) {
const url = new URL("https://api.solana.com/v1/payments/transfers");
url.searchParams.set("direction", "outbound");
url.searchParams.set("from", from);
url.searchParams.set("to", to);
url.searchParams.set("page", String(page));
url.searchParams.set("pageSize", "100");
const res = await fetch(url, {
headers: { Authorization: "Bearer sf_..." },
});
const { data, meta } = await res.json();
yield* data;
if (!meta.hasMore) return;
page += 1;
}
}
```
To scope the list to one run, add `search=`, which matches part of the memo, or filter on `memo.startsWith(runId)` yourself. Match each recipient row by its transfer `id`.
## Failure modes
Handle these outcomes for each transfer:
- **`failed`**: the transfer never reached the network, or the network rejected it (insufficient funds, frozen account, expired blockhash). `error` says why. The recipient was not paid; to retry, send a new transfer with a new `Idempotency-Key`.
- **`processing` for longer than you expect**: the request stopped waiting before the chain settled the transfer. Read it with `GET /v1/payments/transfers/{transferId}` instead of sending it again, and send a new transfer only once `status` is `failed`.
- **Mid-run worker crash**: resume with each row's original `Idempotency-Key`, and store the returned transfer `id` before you move on.
- **Fee sponsor unavailable**: SDP pays the network fee on every transfer it signs, so the source wallet never needs SOL for fees. When the sponsor can't pay, SDP refuses the transfer without charging the source wallet.
## Related
- [Basic payment](/docs/payments/send-basic-payment): the transfer endpoint.
- [Payment with memo](/docs/payments/send-payment-with-memo): the memo field that tags a run.
- [Indexing and reconciliation](/docs/payments/accept-indexing): the same list endpoint, used for inbound payments.
- [Manage API keys](/docs/developing-with-sdp/manage-api-keys#grants-by-module): limit a payout key to the actions payouts need.
---
## Payments: Accept Payments
### Accept payments
Source: https://platform.solana.com/docs/payments/accept-overview
> Get paid into a wallet you control, through a dashboard payment link or a shared address, and find each payment.
To accept a payment, the payer sends tokens to one of your custody wallets and you find that payment. Send a payment link from the dashboard, or share a wallet address and detect payments yourself. [Verifying a payment](/docs/payments/accept-verification) covers when a payment is final, and [Indexing and reconciliation](/docs/payments/accept-indexing) covers listing and matching.
## Payment requests
In the dashboard, open **Payments** > **Requests**, click **New**, and set the amount, token, destination wallet and when the link expires. You get a link and a Solana Pay QR code that anyone can pay, and SDP pays the network fee for the payer's transaction. Each request gets its own Solana Pay reference, which SDP watches to see when the request is paid.
When a request is paid, SDP records an inbound `finalized` transfer with `kind: request_deposit`, which `GET /v1/payments/transfers?direction=inbound` returns. Create one in the dashboard or with `POST /v1/payments/requests`, and cancel it with `POST /v1/payments/requests/{paymentRequestId}/cancel`.
## Share a wallet address
Give the payer a custody wallet's Solana address. Copy it from **Payments** > **Deposit**, or read `publicKey` from [`GET /v1/wallets/{walletId}`](/docs/reference/api/wallets) (add `includeBalance=false` to skip the balance read). The payer sends an ordinary SOL or SPL transfer from any wallet. To offer a QR code, build a Solana Pay URL from the address, token mint and amount yourself.
SDP does not record these payments. You see them through `GET /v1/payments/transfers` with that wallet's `custodyWalletId` and `includeObserved=true`, which reads the wallet's recent on-chain history. These observed rows carry no memo or reference, and they never reach `finalized`; see [Observed transfers](/docs/payments/accept-indexing#observed-transfers).
### Match payments to orders
An observed payment's on-record keys are the wallet it landed in and its `destination`. To tell payments apart, give each order (or each customer) its own receiving wallet. One wallet per merchant, seller or ledger account works when you don't need to match single payments, such as treasury or settlement flows. To correlate by memo or Solana Pay reference instead, read the transaction from Solana RPC by its `signature`.
## What SDP gives you
- **Payment links**: payment requests, from the dashboard or `POST /v1/payments/requests`, recorded as inbound transfers once paid.
- **Observed payments**: a wallet's recent on-chain activity, from `GET /v1/payments/transfers?custodyWalletId=…&includeObserved=true`. See [Indexing and reconciliation](/docs/payments/accept-indexing).
- **Status reads**: `GET /v1/payments/transfers/{transferId}` for one recorded transfer. See [Verifying a payment](/docs/payments/accept-verification).
- **Balances**: `GET /v1/payments/wallets/{walletId}/balances`, where `walletId` is the provider wallet id (`walletId` from `GET /v1/wallets`). See [Wallet balances](/docs/wallet-operations/balances).
## What SDP does not give you
- **No checkout or payment-intent API**: a payment request is the only object you create ahead of a payment. Match payments to a shared wallet address against the order records you keep.
- **No settlement webhooks**: poll for status. See [Polling cadence](/docs/payments/accept-verification#polling-cadence).
## Example: a marketplace checkout
A minimal accept flow on per-order wallets:
1. At checkout, create a custody wallet for the order, or take one from a pool you keep. Store its `id`, its `publicKey`, the token and the expected amount on your `orders` row.
2. Show the payer the address, token and amount, or a Solana Pay QR code you build from them.
3. The payer's wallet sends the transfer.
4. Your worker polls each open order's wallet with `custodyWalletId` and `includeObserved=true`, and looks for an inbound transfer whose `token` and `amount` fit the order. Observed reads have their own quota, so space the polls out.
5. Check with Solana RPC that the transfer's `signature` is `finalized`, then mark the order paid. Store the `signature` so a rerun of the worker never applies the payment twice.
Treat your `orders` table as the checkout state machine and SDP as the wallet and settlement backend.
## Next steps
- [Verifying a payment](/docs/payments/accept-verification): statuses, and when a payment is safe to act on.
- [Indexing and reconciliation](/docs/payments/accept-indexing): list transfers, dedupe, and run a polling worker.
---
### Verifying a payment
Source: https://platform.solana.com/docs/payments/accept-verification
> Transfer statuses, finality on Solana, polling, and how to check a payment against the chain yourself.
A transfer's `status` tells you whether a payment landed and whether it can still be undone. This page covers each status, when a payment is safe to act on, and how to check it against the chain.
## Status lifecycle
A wallet transfer, sent or received, has one of these statuses:
| Status | Meaning |
| --- | --- |
| `processing` | Outbound and in flight: SDP is signing and submitting it, or waiting for the chain. `signature` can still be `null`. |
| `confirmed` | The transaction landed in a block that a supermajority of the cluster voted for. `signature` and `slot` are set. Not yet irreversible. |
| `finalized` | The block is finalized. Irreversible. |
| `failed` | The transfer never reached the network, or the network rejected it. The recipient was not paid; `error` says why. |
`POST /v1/payments/transfers` waits for finality, so a successful request usually answers `finalized`. If the wait runs out, it answers `processing`, and SDP moves the transfer to `confirmed`, `finalized` or `failed` as the chain settles. A `processing` transfer that never got a `signature` fails after 5 minutes, unless it is waiting on a custody approval.
Inbound payments differ. A paid payment request is recorded at `finalized`. An observed row (from `includeObserved=true`) shows `confirmed` or `failed` and never `finalized`, because SDP rebuilds it from chain history on each read. Check its finality yourself; see [Independent verification](#independent-verification).
`finalized` and `failed` are final. Transfers that other modules create document their own lifecycle.
## Finality on Solana
Act on `finalized` for anything irreversible: releasing custody, shipping high-value goods, posting to your general ledger. A `confirmed` transaction has a supermajority vote but can, rarely, still be dropped. Use it for low-stakes steps, such as a status email or a "paid" badge.
## Polling cadence
SDP sends no settlement webhooks, so poll:
- `GET /v1/payments/transfers/{transferId}` for one recorded transfer.
- `GET /v1/payments/transfers` for a time window or one wallet's activity. See [Indexing and reconciliation](/docs/payments/accept-indexing).
Start from these ranges:
- **Active checkout** (someone is watching the payment screen): every 2–5 seconds for up to a minute, then back off.
- **Background reconciliation** (no one is waiting): every 15–30 seconds, or on each worker tick.
- **After the fact**: one read when your application needs the answer, with no loop.
Every poll counts against your key's [rate limit](/docs/reference/conventions/rate-limits). Reads with `includeObserved=true` also count against their own quota of 30 a minute per API key, so spread them across your open orders.
## Independent verification
Once a transfer has a `signature`, you can check it against the network yourself:
- **`signature`**: open it on [Solana Explorer](https://explorer.solana.com/), or look it up with `getSignatureStatuses` on your own RPC.
- **`slot`**: the slot the transaction landed in.
- **`blockTime`**: the block's timestamp (ISO 8601). It is often `null` on transfers SDP sends; `confirmedAt` records when SDP saw the transaction land.
- **`fee`**: the network fee in lamports, when known.
If you run your own indexer, compare its view of each `signature` with SDP's `status`.
## Failure handling
When `status` is `failed`:
- Read `error` for the reason.
- Decide whether to try again. The failed transfer stays failed, and its `Idempotency-Key` replays the failure. To retry, send a new request with a new key; it creates a new transfer with its own `id` and `signature`.
- Tell whoever needs to know: the merchant, the customer, your operations team.
## Related
- [Accept overview](/docs/payments/accept-overview): payment requests and shared addresses.
- [Indexing and reconciliation](/docs/payments/accept-indexing): list and match many transfers.
- [Concepts: the transfer data model](/docs/payments/concepts#the-transfer-data-model): every transfer field.
---
### Indexing and reconciliation
Source: https://platform.solana.com/docs/payments/accept-indexing
> List transfers, read a wallet's observed payments, dedupe, and reconcile against your own ledger.
`GET /v1/payments/transfers` lists the transfers SDP has recorded in your project, including what you sent (single, batch and recurring) and paid payment requests. Filter by direction, status, token, wallet, counterparty and date. To also see payments SDP never recorded, such as a customer's direct transfer to your address, pass one wallet's `custodyWalletId` with `includeObserved=true`.
## Listing inbound transfers
The minimal inbound request:
**curl**
```bash title="Terminal"
curl "https://api.solana.com/v1/payments/transfers?direction=inbound&pageSize=50" \
-H "Authorization: Bearer sf_..."
```
**TypeScript**
```typescript title="list-inbound-transfers.ts"
const url = new URL("https://api.solana.com/v1/payments/transfers");
url.searchParams.set("direction", "inbound");
url.searchParams.set("pageSize", "50");
const res = await fetch(url, {
headers: { Authorization: "Bearer sf_..." },
});
const { data, meta } = await res.json();
// data: Transfer[]
// meta: { total, page, pageSize, hasMore, requestId }
```
Without `includeObserved`, this returns recorded inbound transfers only, such as paid payment requests.
### Query parameters
| Parameter | Type | Notes |
| --- | --- | --- |
| `direction` | `inbound` \| `outbound` | Inbound is a payment into one of your wallets. |
| `status` | string | One or more of `processing`, `confirmed`, `finalized`, `failed`, comma-separated. |
| `custodyWalletId` | string | One wallet's SDP `id` (`cwlt_…` from `GET /v1/wallets`). |
| `includeObserved` | boolean | Default `false`. With `custodyWalletId`, adds the wallet's recent on-chain activity; see [Observed transfers](#observed-transfers). |
| `token` | string | A well-known symbol (`SOL`, `USDC`) or a mint address. A well-known token matches either form. |
| `counterpartyId` | string | Transfers tied to one counterparty (`cpty_…`). |
| `search` | string | 3 or more characters, matched against ids, signatures, addresses, memo and counterparty names. |
| `from`, `to` | ISO 8601 datetime | Window on `createdAt`. Include a time zone, such as `2026-05-14T00:00:00Z`. |
| `sortBy`, `sortDirection` | string | Default `createdAt`, `desc`. |
| `page`, `pageSize` | integer | Default `1` and `20`. `pageSize` is at most `100`. |
[List transfers](/docs/reference/api/payments/list-payment-transfers) has every parameter.
## Observed transfers
With `custodyWalletId` and `includeObserved=true`, the list adds what the wallet's recent on-chain history shows and SDP has no record of:
- SDP builds these rows from the wallet's latest signatures (at most 200) on each read, and does not store them. Use them for recent activity, not history.
- Their `id` starts with `xfr_observed_`. It stays the same across reads, but `GET /v1/payments/transfers/{transferId}` does not find it.
- `status` is `confirmed` or `failed`, never `finalized`. `custodyWalletId` is `null`, and there is no `memo`.
- Reads with `includeObserved=true` have their own quota: 30 a minute per API key and 120 per organization. Past it they return `429 RATE_LIMITED`.
## Deduplication
- **Recorded transfers**: key them by `id` (`xfr_…`), which exists before any `signature` does. Once set, `signature` is unique across SDP's recorded transfers, so it also dedupes against another chain indexer.
- **Observed rows**: key them by `id`, not `signature`. One transaction can produce more than one row.
## Reconciling an order
For a customer's direct payment, the on-record keys are the receiving wallet and `destination`: observed rows carry no memo or reference. Give each order its own receiving wallet (see [Accept payments](/docs/payments/accept-overview#match-payments-to-orders)), then for each open order:
1. **Read** every page of the wallet's inbound activity, while `meta.hasMore` is `true`: `GET /v1/payments/transfers?custodyWalletId=&includeObserved=true&direction=inbound&pageSize=100`.
2. **Match** a transfer whose `token` and `amount` fit the order.
3. **Check finality**: observed rows stop at `confirmed`, so check with Solana RPC (`getSignatureStatuses`) that the `signature` is `finalized` before any irreversible step.
4. **Apply** the payment once: mark the order paid, store the `signature`, and start downstream work (email, shipment, ledger entry).
5. **Leave unmatched transfers** for the next pass, or flag them for review after a grace period.
A paid payment request needs none of this: SDP records it as an inbound `finalized` transfer.
## A polling worker
A worker that checks each open order's wallet on every tick:
**TypeScript**
```typescript title="reconcile-tick.ts"
// Observed reads have their own quota (30 a minute per API key), and each
// page is one read. Past it, the worker waits out Retry-After and reads the
// same page again, so no order is skipped.
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function reconcileTick(openOrders: Order[]) {
orders: for (const order of openOrders) {
let page = 1;
while (true) {
const url = new URL("https://api.solana.com/v1/payments/transfers");
url.searchParams.set("custodyWalletId", order.custodyWalletId);
url.searchParams.set("includeObserved", "true");
url.searchParams.set("direction", "inbound");
url.searchParams.set("pageSize", "100");
url.searchParams.set("page", String(page));
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.SDP_API_KEY}` },
});
if (res.status === 429) {
await sleep(Number(res.headers.get("Retry-After")) * 1000);
continue; // the same page again
}
const { data, meta } = await res.json();
for (const tr of data) {
if (tr.status === "failed" || (await alreadyApplied(tr.id))) continue;
if (tr.token !== order.mint || !sameAmount(tr.amount, order.amount)) continue;
// Observed rows stop at "confirmed": check finality with your own RPC.
if (!(await isFinalized(tr.signature))) continue;
await markOrderPaid(order, tr); // stores tr.id and tr.signature
continue orders; // paid: apply it once, then move to the next order
}
if (!meta.hasMore) break;
page++;
}
}
}
```
Stop polling an order's wallet once the order is paid or expired, so the quota goes to open orders. `sameAmount` compares decimal strings by value: `"100"` and `"100.00"` are the same amount.
## Related
- [Verifying a payment](/docs/payments/accept-verification): statuses and finality.
- [Accept overview](/docs/payments/accept-overview): payment requests and shared addresses.
- [Payouts and disbursements](/docs/payments/send-payouts): the same list endpoint, used for outbound runs.
- [Payments API reference](/docs/reference/api/payments): every payments endpoint.
---
## Self-Hosting
### Quickstart
Source: https://platform.solana.com/docs/self-hosting/quickstart
> Install the self-hosted stack, generate a .env, and bring it up with Docker Compose.
Go from an empty host to a running Solana Developer Platform. You need Docker Engine and Docker Compose v2, with the Docker daemon running.
## 1. Install
Run the install script. It downloads the latest release's `compose.yml` and `.env.example` into `~/sdp` and checks them against the release's `SHA256SUMS`. When `cosign` is installed, it also verifies the signature on `SHA256SUMS`.
```bash title="Terminal"
curl -fsSL https://github.com/solana-foundation/solana-developer-platform/releases/latest/download/install.sh | bash
```
The script is open source: read it before you run it. Its header shows how to verify the checksums and signature yourself, and lists the variables you can override, such as `SDP_INSTALL_DIR` and `INSTALL_VERSION` (a release tag to install instead of the latest).
To install by hand, download `compose.yml` and `default.env.example` from the [latest release](https://github.com/solana-foundation/solana-developer-platform/releases/latest) into a working directory, and save the second file as `.env.example`.
## 2. Generate your `.env`
The stack reads its configuration from a `.env` next to `compose.yml`. The [configurator](/docs/self-hosting/configurator) fills it in, generates the app secrets, and validates your answers.
- **In your browser:** open the [configurator](/docs/self-hosting/configurator), answer the prompts, and save the `.env` into `~/sdp`.
- **In your terminal:** run the configurator from the API image. Put the release tag you installed in place of `latest`, so the configurator matches your `compose.yml`. The install script prints this command with your tag filled in.
```bash title="Terminal"
docker run --rm -it -v "$HOME/sdp:/out" \
ghcr.io/solana-foundation/sdp/sdp-api:latest \
node configure.js --out /out/.env
```
The [environment reference](/docs/self-hosting/env-reference) explains each variable.
## 3. Bring up the stack
```bash title="Terminal"
cd ~/sdp
docker compose up -d
```
`sdp-migrate` applies the database migrations first. The API starts next, and the dashboard and docs follow once it's healthy. The first run pulls the images, so it can take a few minutes.
## 4. Verify
```bash title="Terminal"
docker compose ps
docker compose logs -f sdp-api
```
Once the services report healthy, the dashboard is at `http://localhost:3000`, the API at `http://localhost:8787`, and these docs at `http://localhost:3001`. Point uptime checks at the API's [health endpoints](/docs/reference/api/health): `/health` says the API is up, and `/health/ready` also checks the database and Redis.
If a service fails to start or a required variable is missing, see [Troubleshooting](/docs/self-hosting/troubleshooting).
## Next steps
- [First devnet deployment](/docs/self-hosting/first-devnet-deployment): sign in to the dashboard, create a local wallet, and call the API.
- [Upgrade and backup](/docs/self-hosting/upgrade-and-backup): keep the stack current and protect your data.
- [Environment reference](/docs/self-hosting/env-reference): tune ports, RPC, signing, and authentication.
---
### First Devnet Deployment
Source: https://platform.solana.com/docs/self-hosting/first-devnet-deployment
> Run self-hosted SDP on devnet, sign in to the dashboard, create a local wallet, and verify API access.
Take a fresh self-hosted install from healthy containers to a first devnet workflow: dashboard sign-in, a local custody wallet, a wallet-scoped API key, and an authenticated API call.
Start after the [Quickstart](/docs/self-hosting/quickstart). This path runs on devnet with the local signer. Set up a provider's signing later, when you harden the deployment.
**warn**
The public devnet RPC endpoint is fine for smoke tests, but it can rate-limit you or miss data that balance-heavy wallet views need. Use a dedicated devnet endpoint from your RPC provider before you rely on this flow.
## Confirm the stack is healthy
From the directory that holds `compose.yml` and `.env`:
```bash title="Terminal"
cd ~/sdp
docker compose up -d
docker compose ps
curl http://localhost:8787/health
```
The health check returns JSON with `"status": "ok"`. The dashboard is at `http://localhost:3000`, the API at `http://localhost:8787`, and these docs at `http://localhost:3001`.
## Configure Clerk for dashboard sign-in
In your Clerk development application, follow the [Clerk setup guide](https://github.com/solana-foundation/solana-developer-platform/blob/main/apps/sdp-api/docs/self-hosting/clerk-setup.md):
- Turn on **Organizations**.
- Customize the session token (**Sessions** → **Customize session token**) with the guide's claims. Without `org_id`, the API refuses every dashboard request.
- Copy the publishable key, secret key, and issuer for the configurator.
- Give the API a public HTTPS URL, because Clerk can't reach `localhost`. To test locally, reserve a free static domain at [dashboard.ngrok.com/domains](https://dashboard.ngrok.com/domains) and run `ngrok http --url= 8787`.
- Add a webhook endpoint at `https:///webhooks/clerk/link-orgs`, subscribed to the events the guide lists. Copy its signing secret for `CLERK_WEBHOOK_SECRET`.
The API starts without `CLERK_WEBHOOK_SECRET`, but this tutorial needs it. The webhook is how SDP learns the settings you add to the organization in Clerk, including the one that turns on the local signer.
## Generate the devnet `.env`
Open the [configurator](/docs/self-hosting/configurator), keep its defaults, and fill in:
- `SOLANA_RPC_DEFAULT_DEVNET_API_KEY_URL`: your devnet RPC endpoint.
- The Clerk publishable key, secret key, issuer, and webhook secret.
The signing step keeps the local signer and generates its `CUSTODY_PRIVATE_KEY` in your browser. SDP offers the local signer only while that key is set. Save the file as `~/sdp/.env`, then restart the stack:
```bash title="Terminal"
cd ~/sdp
docker compose up -d
```
## Sign in to the dashboard
Open `http://localhost:3000`, sign in through Clerk, and create an organization when prompted. SDP creates it with you as its admin, along with a sandbox project and a production project.
If requests fail after sign-in, read the failing request's `401` response. `Clerk token missing organization` means the session token lacks the `org_id` claim, and `Clerk token has no usable email claim` means its `email` claim is wrong: fix the session token customization.
If Clerk's webhook deliveries fail, check the API logs:
```bash title="Terminal"
docker compose logs -f sdp-api
```
`webhooks: refused a Clerk webhook with an invalid signature` means `CLERK_WEBHOOK_SECRET` doesn't match the endpoint's signing secret. Clerk's delivery log shows the same refusal as `Invalid webhook signature`.
## Turn on the local signer
The local signer needs a per-organization activation. In the Clerk dashboard, open your organization's private metadata (**Organizations** → your organization → **Metadata** → **Private**) and add:
```json
{ "sdp": { "providerOverrides": { "custody": { "local": true } } } }
```
The `organization.updated` webhook brings it into SDP. Until it does, choosing the local signer returns `403` with `Local requires manual activation for this organization.`
## Create a local custody wallet
In the dashboard, open **Wallets**, choose the local provider, and create a wallet named `Devnet local wallet`.
This step needs the dashboard. Setting up a custody provider takes `wallets.configure`, which no API key can hold, so an organization admin does it signed in. After that, a key with `wallets.manage` can create more wallets with `POST /v1/wallets`.
## Create a wallet-scoped API key
Open **API keys** and create a key in the sandbox project. Under **Permissions**, grant at least `wallets.read`. Under **Access**, choose **Selected wallets** and select `Devnet local wallet`: it's the only wallet the key can sign with.
Copy the full key when the dashboard shows it. SDP shows it only once.
## Verify the key can read wallets
Call your self-hosted API with the new key:
```bash title="Terminal"
export SDP_API_KEY="sf_..."
curl "http://localhost:8787/v1/wallets?view=summary" \
-H "Authorization: Bearer $SDP_API_KEY"
```
The response includes `Devnet local wallet`. You now have a self-hosted devnet stack with dashboard sign-in, local custody signing, and an API key bound to the wallet it may sign with.
## Next steps
- Use the [Environment reference](/docs/self-hosting/env-reference) to move from localhost defaults to real domains, an external Postgres, or an external Redis.
- Read [Upgrade and backup](/docs/self-hosting/upgrade-and-backup) before you keep data you care about in the stack.
- Use [External co-signers](/docs/self-hosting/external-co-signers) when a provider needs a signing process outside the default stack.
---
### Configurator
Source: https://platform.solana.com/docs/self-hosting/configurator
> Generate a complete .env for a self-hosted Solana Developer Platform, entirely in your browser.
Answer the steps below to generate a ready-to-use `.env` for the default devnet path. Everything runs in your browser, and nothing you enter is sent anywhere. Save the file next to `compose.yml`, then run `docker compose up -d`.
To continue after you download it, follow [First devnet deployment](/docs/self-hosting/first-devnet-deployment).
---
### Environment Reference
Source: https://platform.solana.com/docs/self-hosting/env-reference
> The configuration variables a self-hosted Solana Developer Platform reads from .env.
The stack reads one `.env` file next to `compose.yml`. The [configurator](/docs/self-hosting/configurator) writes it, with the defaults and validation of the release you installed. This page covers how the file is organized and the variables you're most likely to set by hand.
The configurator groups the variables into sections:
| Section | What it covers |
|---|---|
| Basic | `ENVIRONMENT`. The deployment mode is always `self_hosted`. |
| Database | Bundled Postgres or an external database. |
| Cache | Bundled Redis or an external one. |
| Solana RPC | The devnet and mainnet RPC endpoints. |
| Authentication (Clerk) | Clerk keys, issuer, dashboard URL, and webhook secret. |
| Signing provider | Which signers to enable, the default one, and their credentials. |
| Fee payment | The Kora that sponsors each cluster's transaction fees, if any. |
| Secrets | App secrets, generated where you run the configurator. |
| Advanced | Image source, ports, public URLs, proxy trust, and optional features. |
## Required variables
`compose.yml` refuses to start without these:
| Variable | Description |
|---|---|
| `POSTGRES_PASSWORD` | Password for the bundled Postgres. Required even with an external `DATABASE_URL`, because the bundled `postgres` service still starts. |
| `API_KEY_PEPPER` | Server-side pepper for API key hashes. Generate it with `openssl rand -hex 32`. |
| `CUSTODY_ENCRYPTION_KEY` | Encrypts custody secrets at rest. Generate it with `openssl rand -base64 32`. |
| `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | Clerk publishable key for the dashboard. |
| `CLERK_SECRET_KEY` | Clerk secret key. |
The configurator also requires these. The stack starts without them, but most leave a core flow broken:
| Variable | Description |
|---|---|
| `CLERK_ISSUER` | Clerk issuer URL. Without it, dashboard sessions can't be verified, so sign-in fails. |
| `FRONTEND_URL` | The dashboard URL the browser loads. With `ENVIRONMENT=production`, the compose default, the API accepts only Clerk sessions issued to this origin, and won't start if it isn't an `http(s)` URL. |
| `SOLANA_RPC_DEFAULT_DEVNET_API_KEY_URL` | The devnet endpoint Sandbox projects read and send through. Without it, they get `503 RPC_UNAVAILABLE`. |
| `SIGNING_PROVIDER` | The configurator writes `local` unless you pick another provider. The API reads it only at startup, where `local` (also its value when unset) requires `SDP_DEPLOYMENT_MODE=self_hosted`. |
| `CUSTODY_PRIVATE_KEY` | With local signing selected: SDP offers the local signer only while this is set. |
| `CREDENTIAL_FINGERPRINT_PEPPER` | Without it, saving or rotating provider credentials in the dashboard fails. Keep it stable. |
`SOLANA_RPC_DEFAULT_MAINNET_API_KEY_URL` is optional: without it, SDP refuses Production projects' chain reads and sends.
The configurator generates `API_KEY_PEPPER`, `CREDENTIAL_FINGERPRINT_PEPPER`, and `CUSTODY_ENCRYPTION_KEY` for you, and can generate `POSTGRES_PASSWORD`.
## Database
The configurator's database choice (`DATABASE_MODE`) shapes the file, but isn't written to it.
- **Bundled:** set `POSTGRES_DB`, `POSTGRES_USER`, and `POSTGRES_PASSWORD`. The API and `sdp-migrate` build their connection string from them.
- **External:** set `DATABASE_URL` to your `postgresql://…` string, and the API and migrations connect there. The bundled `postgres` container still starts, unused, so it still needs `POSTGRES_PASSWORD`. The configurator hides that field and generates a value for it.
## Cache
The cache choice (`CACHE_MODE`) isn't written to the file either. For an external cache, set `REDIS_URL` to your `redis://…` or `rediss://…` endpoint. The bundled `redis` container still starts, unused.
## Signing providers
The configurator offers `local`, `fireblocks`, `privy`, `coinbase_cdp`, `para`, `turnkey`, and `utila`. Its list of providers to enable (`SIGNING_PROVIDERS`) only decides which credential fields it shows, and isn't written to the file. Each provider other than `local` needs its own credentials, such as an API key and secret for Fireblocks. Each project picks its signer when you set up custody in the dashboard.
Privy, Coinbase CDP, Para, and Turnkey are open to every organization. The others, `local` included, also need a per-organization activation: [First devnet deployment](/docs/self-hosting/first-devnet-deployment) shows how for the local signer.
For provider co-signers that run outside the stack, see [External co-signers](/docs/self-hosting/external-co-signers).
## Fee payment
SDP pays transaction fees through a Kora on each project's cluster, and the stack doesn't bundle one. The configurator's choice of clusters (`FEE_SPONSOR_CLUSTERS`) isn't written to the file: it shows each chosen cluster's fields. Without a Kora, sends SDP sponsors, such as payment transfers, answer `503 PROVIDER_UNAVAILABLE`.
| Variable | Description |
|---|---|
| `KORA_DEVNET_URL` | The Kora that pays fees for Sandbox projects. |
| `KORA_DEVNET_RPC_URL` | The Solana RPC endpoint that Kora reads. SDP prices and settles sponsored fees through it. Set it with `KORA_DEVNET_URL`: the API won't start with only one of the two. |
| `KORA_DEVNET_API_KEY` | Optional. The API key your Kora requires, if any. |
| `KORA_DEVNET_CLOUD_RUN_AUDIENCE` | Optional. For a private Kora on Cloud Run: SDP sends it a Google identity token for this audience, which needs the API on Google Cloud and the Kora URL on the audience's `https` origin. |
The `KORA_MAINNET_*` variables do the same for Production projects.
## Advanced
| Variable | Default | Description |
|---|---|---|
| `SDP_IMAGE_REGISTRY` | `ghcr.io/solana-foundation/sdp` | Registry the images come from. |
| `SDP_VERSION` | `latest` | Image tag for every SDP service. Pin it to a release tag, such as `vX.Y.Z`, in production. |
| `SDP_API_PORT` | `8787` | Host port for the API. |
| `SDP_WEB_PORT` | `3000` | Host port for the dashboard. |
| `SDP_DOCS_PORT` | `3001` | Host port for these docs. |
| `TRUST_PROXY_HEADERS` | `false` | How to read `X-Forwarded-For`. `true` trusts the first address: use it only behind an ingress that replaces caller-supplied forwarding headers. `gclb` is for ingress restricted to a Google Cloud external Application Load Balancer, which appends the verified client address. With `false`, SDP can't see caller addresses: per-IP limits count every caller as one, and an IP allowlist refuses every request. |
`SDP_API_BASE_URL` (default `http://sdp-api:8787`) is how the dashboard's server reaches the API inside the Docker network. The browser-facing `NEXT_PUBLIC_*` URLs default to `localhost`, such as `http://localhost:8787` for the API and `http://localhost:3000` for the dashboard. Change them, and `FRONTEND_URL`, when you put the services behind your own domains.
`compose.yml` also reads `SDP_RELEASE_CHANNEL`, which the configurator doesn't set. It decides which modules and providers the API and dashboard run (`experimental`, `beta`, or `stable`), and defaults to `experimental`.
---
### Upgrade & Backup
Source: https://platform.solana.com/docs/self-hosting/upgrade-and-backup
> Move a self-hosted Solana Developer Platform to a new release and protect its data.
Upgrade to a new release, roll back, and back up the data that matters.
## Upgrade
A release ships new images and an updated `compose.yml`.
1. **Refresh the install files.** Re-run the install script. It replaces `compose.yml` with the new release's, verified against its checksums. It never touches `.env`, keeps an existing `.env.example`, and prints a link to the new release's template so you can check for new variables.
```bash title="Terminal"
curl -fsSL https://github.com/solana-foundation/solana-developer-platform/releases/latest/download/install.sh | bash
```
To install a specific release instead of the latest, set `INSTALL_VERSION` to its tag:
```bash title="Terminal"
curl -fsSL https://github.com/solana-foundation/solana-developer-platform/releases/latest/download/install.sh | INSTALL_VERSION=vX.Y.Z bash
```
2. **Pin the version.** Set `SDP_VERSION` in `.env` to the same release tag, not `latest`, so every service runs one known build.
3. **Pull and restart.**
```bash title="Terminal"
cd ~/sdp
docker compose pull
docker compose up -d
```
`sdp-migrate` applies any new database migrations before the API starts.
4. **Verify** that the services are healthy, as in the [Quickstart](/docs/self-hosting/quickstart#4-verify).
### Roll back
To run the previous release again, re-run the install script with its tag in `INSTALL_VERSION`, set `SDP_VERSION` to that tag, and run `docker compose up -d`.
Migrations don't reverse. If the newer release applied any, restore a backup taken before the upgrade instead of only switching versions.
## Backup
Two things hold your state:
- **The Postgres database:** all platform data. The bundled database lives in the `sdp-postgres-data` volume (`sdp_sdp-postgres-data` in `docker volume ls`). Back up an external `DATABASE_URL` with your database provider's tools.
- **Your `.env`:** configuration and secrets you can't regenerate. Without the same `API_KEY_PEPPER`, no existing API key matches. Without the same `CUSTODY_ENCRYPTION_KEY`, SDP can't decrypt the custody secrets it stored, such as local wallet keys. Keep the file in a secret manager.
Redis is a cache and doesn't need a backup.
### Back up the database
Take a logical dump with `pg_dump` inside the running container. Running it through `sh -c` lets it read `POSTGRES_USER` and `POSTGRES_DB` there, so custom values work:
```bash title="Terminal"
cd ~/sdp
docker compose exec -T postgres \
sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > sdp-backup.sql
```
Keep dumps off the host, on a schedule that fits your recovery needs.
### Restore the database
A plain `pg_dump` holds the full schema and the migration history, so restore it into an **empty** database and let the stack start on top. Restoring over an already migrated database collides with the schema `sdp-migrate` created.
```bash title="Terminal"
cd ~/sdp
docker compose down # stop every service
docker volume rm sdp_sdp-postgres-data # discard the current database (destructive)
docker compose up -d postgres # start a fresh, empty Postgres
until docker compose exec -T postgres \
sh -c 'pg_isready -U "$POSTGRES_USER"' >/dev/null 2>&1; do sleep 1; done
docker compose exec -T postgres \
sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"' < sdp-backup.sql
docker compose up -d # start the rest
```
On that final `up`, `sdp-migrate` skips every migration the dump already records, so an older dump gets only the newer migrations.
Restore the matching `.env` too, so secrets such as `CUSTODY_ENCRYPTION_KEY` match the data they protect. Restore an external `DATABASE_URL` through your database provider instead of the bundled `postgres` service.
---
### Troubleshooting
Source: https://platform.solana.com/docs/self-hosting/troubleshooting
> Diagnose common startup and configuration failures on a self-hosted stack.
Most problems show up at startup. Start with the service state and the API logs:
```bash title="Terminal"
cd ~/sdp
docker compose ps
docker compose logs -f sdp-api
```
## Docker is not ready
The install script checks Docker first. If it reports that Docker isn't installed, that Compose v2 is unavailable, or that the daemon isn't reachable, install or start Docker and retry. See the [Docker Engine](https://docs.docker.com/engine/install/) and [Compose](https://docs.docker.com/compose/install/) install guides.
## A required variable is missing
`compose.yml` stops with a message such as `set POSTGRES_PASSWORD in .env` when a required variable is unset. Make sure `.env` sits next to `compose.yml` and sets every required variable. The [configurator](/docs/self-hosting/configurator) writes a complete file, and the [environment reference](/docs/self-hosting/env-reference) lists what's required.
## Images won't pull
If `docker compose pull` reports access denied or a missing manifest:
- Check that `SDP_IMAGE_REGISTRY` and `SDP_VERSION` name a registry and tag you can reach. Release tags look like `vX.Y.Z`.
- Pull one image directly to see the underlying error, for example `docker pull ghcr.io/solana-foundation/sdp/sdp-api:latest`.
- If the registry is private, run `docker login` for it first.
## Port already in use
The API, dashboard, and docs use ports `8787`, `3000`, and `3001` by default. If another process holds one, that service can't bind. Set `SDP_API_PORT`, `SDP_WEB_PORT`, or `SDP_DOCS_PORT` in `.env`, update the browser-facing URLs that name that port (the `NEXT_PUBLIC_*` URLs, and `FRONTEND_URL` for the dashboard), and run `docker compose up -d` again.
## Migrations failed
`sdp-migrate` runs before the API starts. If it fails, the API stays down. Read its logs:
```bash title="Terminal"
docker compose logs sdp-migrate
```
A failure here is usually a database connection problem. Check `POSTGRES_*` (or `DATABASE_URL`), and that `postgres` shows as healthy in `docker compose ps`.
## Database connection refused
The API and migrations wait for Postgres to report healthy before they start. If they still can't connect, check the credentials in `.env`. With an external database (`DATABASE_URL` set), check that it's reachable from inside the Docker network.
## Dashboard sign-in fails
Most sign-in failures are Clerk configuration. Check `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`, `CLERK_SECRET_KEY`, and `CLERK_ISSUER` against your Clerk application, and that the keys belong to the instance (development or production) you mean to use. Then look for these messages:
- `Clerk token missing organization` or `Clerk token has no usable email claim`, in the failing request's `401` response: the session token lacks the claims in the [Clerk setup guide](https://github.com/solana-foundation/solana-developer-platform/blob/main/apps/sdp-api/docs/self-hosting/clerk-setup.md).
- `Clerk session refused: not issued to the dashboard`, in the API logs: `FRONTEND_URL` isn't the origin the dashboard loads from. With `ENVIRONMENT=production`, the API accepts only sessions issued to that origin.
## The local signer isn't offered
If the local provider is missing from the dashboard, `CUSTODY_PRIVATE_KEY` isn't set. If choosing it returns `Local requires manual activation for this organization.`, add the organization override from [First devnet deployment](/docs/self-hosting/first-devnet-deployment), and check that the Clerk webhook reaches the API.
## Still stuck?
Collect the failing service's logs and your `.env` settings with the secrets removed, then open an issue on the [project repository](https://github.com/solana-foundation/solana-developer-platform).
---
### External Co-Signers
Source: https://platform.solana.com/docs/self-hosting/external-co-signers
> Host provider-specific signing services outside the default self-hosted SDP stack.
Some signing providers need a co-signer or automation process that you run beside the SDP API. Run it outside `compose.yml`, which starts only SDP's own services: a co-signer has its own secrets, identity, ingress, and availability needs.
## Production pattern
Run each co-signer on a managed container service, such as Google Cloud Run or your cloud's equivalent.
- Use the image and runtime the provider or your operator team supplies.
- Store co-signer secrets in a cloud secret manager, never in the SDP `.env`.
- Give the co-signer a dedicated service account that can read only the secrets it needs.
- Set ingress and invoker IAM for the provider's flow. Keep ingress private or authenticated unless the provider needs a public callback.
- Keep at least one instance running (`minScale: 1` on Cloud Run) when callback latency or availability matters. Scale to zero only when cold starts are acceptable.
- Separate development, staging, and production co-signers by provider account, cloud project, service account, and secrets.
- Alert on failed signing attempts, container restarts, authentication failures, and stale or expired key material.
## Utila
SDP signs with Utila directly: select `utila` in the configurator and fill in its `UTILA_*` credentials. SDP submits each transaction to Utila and polls until it's signed, up to `UTILA_MAX_POLL_ATTEMPTS` times every `UTILA_POLL_INTERVAL_MS` (60 tries, a second apart, by default). Like the local signer, Utila needs a per-organization activation.
`UTILA_DESIGNATED_SIGNERS` lists the Utila users asked to sign each transaction. Left empty, it's the service account itself.
If your Utila setup runs an automated co-signer, host it outside the stack. `infra/utila/cloud-run/` in the repository has Cloud Run scaffolding for one: a service template and a deploy script for an image you supply.
---
## Reference
### Overview
Source: https://platform.solana.com/docs/reference
> The API reference and its conventions, AI resources, the Postman collection, and provider onboarding.
Reference material for building on SDP:
- [API reference](/docs/reference/api): every public endpoint, by family.
- [API conventions](/docs/reference/conventions): errors, pagination, rate limits, and versioning.
- [AI resources](/docs/reference/docs-for-ai): machine-readable docs for coding agents.
- [Postman collection](/docs/reference/postman-collection): the public API, ready to import.
- [Provider onboarding](/docs/reference/provider-onboarding): add your infrastructure integration to SDP.
---
### Infrastructure Provider Onboarding
Source: https://platform.solana.com/docs/reference/provider-onboarding
> Add an infrastructure provider integration to SDP through the self-service contribution process.
SDP accepts infrastructure provider integrations in the categories below. An onboarded provider is listed as an available integration, not an endorsed or recommended partner. Institutions using SDP are responsible for their own provider due diligence.
## Start here
Pick your category, complete its intake form, and review its criteria before you open a pull request.
| Category | Intake form | Criteria |
| --- | --- | --- |
| Custodial enterprise wallet | [Complete custody intake](https://solanafoundation.typeform.com/to/wShiq9SN) | [Download custody criteria PDF](/provider-onboarding/custodial-wallet-vendor.pdf) |
| RPC infrastructure | [Complete RPC intake](https://solanafoundation.typeform.com/to/cq5m65jI) | [Download RPC criteria PDF](/provider-onboarding/rpc-providers.pdf) |
A module's own categories publish their intake on that module's pages. If your category has no published intake, contact the SDP team before you open a pull request.
## Onboarding steps
1. Review the criteria PDF for your category.
2. Complete the matching intake form.
3. Provide sandbox or playground API access.
4. Share documentation, test credentials, supported networks, rate limits, and sandbox limitations.
5. Build the SDP integration, with tests and docs.
6. Open a pull request that links to the intake submission or tracking issue.
The SDP team reviews intake completeness within five business days. Gate evaluation usually takes one to three weeks, depending on the category, how complete the documentation is, and the technical validation it needs.
## Evaluation
Each category uses the criteria linked above. A review starts with hard gates, then a scored evaluation where the category has one. SDP evaluates:
- Solana compatibility for the category.
- Stable sandbox or playground access.
- Clear API and integration documentation.
- Predictable error handling and testability.
- Operational readiness and support.
- The regulatory, compliance, or security posture the category requires.
A provider that doesn't clear the required gates may be tracked for future support, but isn't presented as available until the gaps close. A provider that clears evaluation must still be configured by the deployment. It's then either generally available to every organization, or needs a per-organization Manual Provider Activation while commercial or operational onboarding finishes.
## Access requirements
Provide access that supports repeatable validation:
- Sandbox API keys or equivalent credentials.
- Test accounts, wallets, endpoints, or settlement rails, when the category needs them.
- The Solana networks you support.
- Known rate limits, feature flags, and sandbox limitations.
- A technical support contact for the review.
## Pull request expectations
An SDP infrastructure provider contribution usually includes:
- Provider metadata and capability registration.
- API support for setup, configuration, and the category's runtime operations.
- Dashboard support, where it applies.
- Tests for setup, success paths, failure states, disabled configuration, and missing credentials.
- User-facing docs for setup, sandbox behavior, and provider limits.
Open the pull request once intake is complete, or once the SDP team has opened a tracking issue while intake is finalized.
---
### Postman Collection
Source: https://platform.solana.com/docs/reference/postman-collection
> Download the public SDP API as a Postman collection, generated from the OpenAPI contract.
The collection is generated from the same public OpenAPI contract as the [API reference](/docs/reference/api), and holds every family published there. Internal families, such as `admin`, `onboarding`, `organizations`, `members`, and `api-keys`, are left out.
Download Postman collection
Open raw JSON
After you import it, set two collection variables:
- `baseUrl`: the API host to call. It defaults to `https://api.solana.com`.
- `sdpApiKey`: your API key (`sf_...`), sent as a bearer token.
## Idempotency keys
Each route that requires an `Idempotency-Key` sends the one saved in its `idempotencyKey.` collection variable. Postman creates the key when the variable is empty. It clears the key only after a final `2xx`, a replayed refusal, or a `422` other than `IDEMPOTENCY_KEY_REUSED`.
Every other answer keeps the key for the next send. That covers a transfer, batch, or schedule still `pending` or `processing` (poll it with `GET` until it's final), a timeout, a `5xx`, a `409` or `429`, a `400`, `401`, or `403` refusal, and `422 IDEMPOTENCY_KEY_REUSED`. See [Idempotency](/docs/developing-with-sdp/idempotency#the-dashboard-and-the-postman-collection).
---
## Conventions
### API conventions
Source: https://platform.solana.com/docs/reference/conventions
> What every SDP API call has in common, from how it authenticates to how it fails.
Every route follows these rules:
- [Authentication](/docs/developing-with-sdp/authentication): send an API key in the `Authorization` header. Its grants decide what it can do.
- [Environments](/docs/developing-with-sdp#environments): a project runs in sandbox (devnet) or production (mainnet-beta), and its keys work in that environment only.
- [Idempotency](/docs/developing-with-sdp/idempotency): send an `Idempotency-Key` to retry a mutation safely. Routes that move money require one.
- [Errors](/docs/reference/conventions/errors): one error envelope, with a code to branch on.
- [Pagination](/docs/reference/conventions/pagination): lists return one page at a time.
- [Rate limits](/docs/reference/conventions/rate-limits): each API key gets a set number of requests per minute.
- [Versioning](/docs/reference/conventions/versioning): the version is in the path, and a breaking change gets a new one.
A successful JSON response wraps its payload in `data`, with `meta.requestId` beside it. A `204` has no body, and the health checks answer with their own. Every response also carries the request ID in `X-Request-ID`: quote it when you report a problem.
---
### Errors
Source: https://platform.solana.com/docs/reference/conventions/errors
> A failed request returns an HTTP error status and one error envelope, with a code to branch on.
A failed request returns an error status and this body. The health checks are the exception: each answers with its own body, as its page shows.
```json
{
"error": {
"code": "FORBIDDEN",
"message": "Access denied"
},
"meta": { "requestId": "req_..." }
}
```
- `code` is stable: branch on it, not on `message`, which is for people and can change.
- `details`, when present, says more, such as which field failed validation.
- `meta.requestId` identifies the request. Quote it when you report a problem.
## Statuses
| Status | Means | Common codes |
| --- | --- | --- |
| `400` | The request is malformed or fails validation. | `BAD_REQUEST`, `IDEMPOTENCY_KEY_REQUIRED` |
| `401` | The API key is missing, invalid, expired, or revoked. | `UNAUTHORIZED`, `INVALID_API_KEY`, `EXPIRED_API_KEY`, `REVOKED_API_KEY` |
| `403` | The key lacks the grant or wallet access the request needs, or the call comes from an IP address the key or its organization doesn't allow (`details.reason` is `ip_not_allowed`). | `FORBIDDEN`, `INSUFFICIENT_PERMISSIONS` |
| `404` | Nothing with that ID exists in the project. | `NOT_FOUND` |
| `409` | The request conflicts with the resource's state, or an earlier request with the same idempotency key is still running or never recorded its outcome. | `CONFLICT`, `IDEMPOTENCY_KEY_IN_FLIGHT`, `IDEMPOTENCY_KEY_OUTCOME_UNKNOWN` |
| `413` | The request body is larger than 1 MB. | `PAYLOAD_TOO_LARGE` |
| `422` | The request is well formed but can't go ahead: its idempotency key was used for a different request, the transaction fee sponsor rejected the transaction, or the wallet's policy at its custody provider refused to sign it. | `IDEMPOTENCY_KEY_REUSED`, `SIGNING_REJECTED`, `CUSTODY_POLICY_REFUSED` |
| `429` | Too many requests. See [Rate limits](/docs/reference/conventions/rate-limits). | `RATE_LIMITED` |
| `500` | Something failed inside SDP. | `INTERNAL_ERROR` |
| `502` | Solana RPC or a provider answered, but with something SDP couldn't use, or the custody provider failed or timed out while signing. | `SOLANA_RPC_ERROR`, `PROVIDER_RESPONSE_INVALID`, `CUSTODY_ERROR` |
| `503` | SDP, or a provider it depends on, is unavailable for now, or a provider isn't configured for the project's environment. | `SERVICE_UNAVAILABLE`, `PROVIDER_UNAVAILABLE`, `PROVIDER_NOT_CONFIGURED`, `RPC_UNAVAILABLE` |
Each operation's page lists its statuses and shows a success example. The OpenAPI spec carries every response schema.
## Preconditions
A write that should apply only to the version you last read takes an optional `expectedUpdatedAt` field in its request body: the `updatedAt` from your last read of the resource. When the resource has changed since, the write changes nothing and returns `409`, carrying the current `updatedAt` in `error.details`. The operation's page says whether it takes the field, and names the code it returns. Omit it to write whatever the current version is.
The field is part of the body, so it is part of what an `Idempotency-Key` identifies: a retry under the same key must send the same value.
## Retrying
- `400`, `401`, `403`, `404`, `413`, and `422`: fix the request before you send it again.
- `422 CUSTODY_POLICY_REFUSED`: the wallet's policy at its custody provider refused to sign. The message names the provider. Review the policy there, or change the request, before you send it again.
- `409 CONFLICT`: read the resource's current state, then decide.
- `409` answering a stale `expectedUpdatedAt` (the code its operation's page names): the resource changed since the version you sent. Read it again, review what changed, then send the write with the new `updatedAt` and a new key.
- `409 IDEMPOTENCY_KEY_IN_FLIGHT` and `429`: wait for `Retry-After`, then retry with the same `Idempotency-Key`.
- `409 IDEMPOTENCY_KEY_OUTCOME_UNKNOWN`: final for that key. Read the resource with `GET` to see whether the request ran, then send it again with a new key.
- `503 RPC_UNAVAILABLE`: `details.reason` says why Solana RPC was unavailable. `upstream_unavailable` is temporary: wait for `Retry-After`, then retry as the `5xx` rule below says. `not_configured` and `cluster_mismatch` are SDP configuration problems that a retry won't fix. Quote `meta.requestId` when you report one.
- `503 PROVIDER_NOT_CONFIGURED`: a provider the request needs, such as the custody provider that signs for the wallet, isn't configured for the project's environment. A retry won't fix it until that changes. Check the provider's configuration, or quote `meta.requestId` when you report it.
- `5xx`: most routes store the error and return it again to a same-key retry. Read the resource with `GET` to see whether the request took effect, then retry with a new key. On the routes that move money, a same-key retry runs again and never acts twice.
[Idempotency](/docs/developing-with-sdp/idempotency) covers what each retry returns.
---
### Pagination
Source: https://platform.solana.com/docs/reference/conventions/pagination
> Lists return one page at a time, by page number on most routes and by cursor on a few.
Each list operation's page says which style it uses, with its default and largest page size.
## Page numbers
Most lists take `page` (from 1) and `pageSize`. The page's items are in `data`, and `meta` says where you are:
| Field | Means |
| --- | --- |
| `total` | Items across every page |
| `page` | This page's number |
| `pageSize` | Items per page |
| `hasMore` | Whether a later page exists |
Ask for the next `page` while `hasMore` is `true`.
## Cursors
A few lists take a `limit` and a cursor parameter instead. Each page returns a `nextCursor`: send it back in the cursor parameter the operation names, such as `cursor`, to get the next page. It's `null` on the last page.
---
### Rate limits
Source: https://platform.solana.com/docs/reference/conventions/rate-limits
> Each API key gets a set number of requests per minute, by its tier, and responses usually say how many are left.
Requests count against a limit per minute. Under `/v1`, a request with an API key counts against that key's tier (keys start on `standard`), and one without a key counts against its IP address. Outside `/v1`, every request counts against its IP address, even one that sends a key. The health checks and a few other public routes aren't limited.
| Caller | Requests per minute |
| --- | --- |
| An API key on the `standard` tier | 100 |
| An API key on the `elevated` tier | 500 |
| An API key on the `unlimited` tier | 10,000 |
| No API key, per IP address | 20 |
Some operations also have a tighter quota of their own, counted per key (or signed-in member) and per organization. They can return `429` before the key reaches its tier's limit. While SDP's counter store is unavailable, those operations refuse with `503` rather than run uncounted.
Responses usually say where you stand. The headers are left off while the counter store is unavailable, so don't depend on them:
| Header | Means |
| --- | --- |
| `X-RateLimit-Limit` | Requests allowed per window |
| `X-RateLimit-Remaining` | Requests left in this window |
| `X-RateLimit-Reset` | When this window ends, in Unix seconds |
Past a limit, a request gets `429` with the code `RATE_LIMITED` and a `Retry-After` header. Wait that many seconds before you send it again.
---
### Versioning
Source: https://platform.solana.com/docs/reference/conventions/versioning
> The API version is in every product route's path. /v1 is current, and a breaking change ships under a new major version.
Every product route's path starts with its version: `/v1` is the current contract. A breaking change ships under a new major version, such as `/v2`, never inside `/v1`. The health checks, `/health` and `/health/ready`, have no version.
The OpenAPI document's `info.version` counts revisions of that document for the current path version. It isn't the API's version.
---
## Docs
### Connect Fireblocks
Source: https://platform.solana.com/docs/wallet-operations/fireblocks
> Connect your Fireblocks workspace and import Solana wallets into SDP.
Use your Fireblocks wallets in SDP. Your workspace keeps the signing keys and
controls approvals.
## Connection setup
Sign in as an SDP **organization admin**. Have a Fireblocks **workspace owner**
available for API user approval. If Fireblocks isn't available, ask the SDP team.
Choose an SDP **Sandbox project** for devnet (`SOL_TEST`) or **Production
project** for mainnet (`SOL`, [mainnet access required](/docs/guides/go-to-mainnet)).
Each selected vault must already contain a Solana wallet on that network.
SDP project environments are separate from Fireblocks workspace types; check
workspace compatibility with the SDP team.
1. **Generate a CSR in SDP.** Open **Integrations → Fireblocks → Add connection**,
enter a **Connection name**, and click **Generate and download CSR**.
SDP creates the API authentication key. Share only the
[CSR](https://developers.fireblocks.com/docs/integrate-fireblocks-with-third-party-service-providers),
never a private key. Register within **seven days**.
2. **Register an API user in Fireblocks.** Open **Developer center → API users →
Add API user**. Name it, choose **Editor**, upload the CSR, and click **Add user**.
Have the workspace owner approve it; wait until active.
For [IP-restricted keys](https://developers.fireblocks.com/docs/whitelist-ips-for-api-keys),
get SDP's outbound IPs from the SDP team. Have an authorized operator add them
under **⋯ → Allowlist IP address** before continuing.
3. **Import vaults in SDP.** Copy **API User (ID)** into SDP. Click **Load vaults**,
select up to **20 vaults**, then **Connect and verify**. SDP checks API access
and each wallet's address, then lists imported wallets on the connection page
and under **Wallets**. This does not test signing or transfer policies.
## Transfers
These steps enable direct **SOL and canonical USDC transfers**. Fireblocks signs
and broadcasts them. RAW signing and SDP fee sponsorship are not used.
### Configure approvals
Have an authorized Fireblocks operator open **Security → Policies → Transfer →
Edit policy → Add rule**:
- **Scope:** your SDP API user as **Initiator**, allowed vaults and destinations,
and an address type permitting one-time addresses.
- **Funds:** allowed assets and amount limits.
- **Action:** **Request approval**, your **Approver set** and **Threshold**, then
**Designated signer → Users** and your organization's signer.
Publish the rule. SDP uses
[one-time address transfers](https://developers.fireblocks.com/reference/transaction-sources-destinations#one_time_address),
so set **Settings → General → One-time address transactions** to **Allow**.
Complete the required approvals for both changes.
**Check the policy yourself:** SDP's Editor user cannot read policies, and the
**Pay** reminder does not detect a missing policy.
### Fund and test
Open **Accounts → Vault**, select the source vault's Solana wallet, and use its
deposit address. Add **SOL for network fees**, including for USDC transfers.
Send a [small payment](/docs/payments/send-basic-payment), then open
**Payments → Transactions** in SDP. While awaiting approval, it shows
**Processing**; open it for the Fireblocks transaction ID and status.
Approve the matching transaction in Fireblocks. In **Transaction history**,
confirm **COMPLETED**; SDP should show **Confirmed**.
## Manage your connection
RAW signing and ownership checks
Batch payments and the optional ownership check require workspace RAW signing
and a RAW policy permitting the SDP API user with your designated signer.
If disabled, ask your Fireblocks Customer Success Manager to enable it.
An API Co-signer isn't required.
Open the wallet's **Settings → Ownership → Prove ownership** and approve promptly
in Fireblocks. SDP simulates the signed transaction without broadcasting.
The check requires a configured, funded SDP fee payer.
Rotate credentials
In **Integrations → Fireblocks**, select the connection and **Rotate secret**.
Generate a new CSR, register a replacement Editor user, and get owner approval.
Before switching, add it to the old **Initiator** policies and set its IP
allowlist.
Select the original verification vault. SDP checks every wallet before switching.
Keep the old user active and permitted for the **24-hour rollback window**.
Disconnect
In Fireblocks, choose **Developer center → API users → ⋯ → Delete** for the SDP
API user. If unavailable, ask an authorized operator.
SDP blocks connection deactivation while wallets are active and cannot deactivate
Fireblocks wallets. Contact the SDP team for cleanup.
Deleting the user does not cancel accepted payments. Reject or cancel them in
Fireblocks too; cancellation can race execution.
Credential storage
API credentials default to GCP Secret Manager on hosted SDP and encrypted
database storage when self-hosted. Pending CSR keys are encrypted and deleted
after registration or expiry.
## Transfer troubleshooting
Blocked by policy
In **Transaction history**, expand the transaction and check **Status** and
**Sub-status**. **Inspect Policy** uses the current policy, which may differ
from the original. Pending approval is not a refusal.
Refused submissions and replays return **HTTP 422** with
`error.code: "CUSTODY_POLICY_REFUSED"`. Later refusals mark the transfer failed;
the dashboard shows a wallet-policy error.
Recipient whitelisting
Manage Fireblocks whitelists under **Whitelisted addresses**. Check the Solana
network and address. Creating an SDP contact does not whitelist an address.
Submission timed out
Keep the original [Idempotency-Key](/docs/developing-with-sdp/idempotency) and
check the existing payment before intentionally starting another.
## Notes
- Generated from docs source. Hidden or internal-only APIs are intentionally excluded.