# 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 - Interactive API docs: https://api.solana.com/docs - 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 tokenization, custody, payments, and compliance on Solana. SDP provides tokenization, custody, payments, and compliance through one stable API in front of interchangeable providers. Operations use project-scoped authentication. | Module | What it does | | --- | --- | | **Issuance** | Deploy, mint, burn, freeze, pause, and seize tokens built on Token-2022 | | **Custody** | Provision and operate Solana wallets through interchangeable custody providers | | **Payments** | Move funds onchain — transfers, batches, recurring payments, payment requests, and ramps | | **Compliance** | Screen wallets and transactions against your regulatory program | ## Background Institutional capital is moving onchain: [$35 billion in tokenized real-world assets](https://app.rwa.xyz/) across public blockchains, [$3.2 billion of it on Solana](https://app.rwa.xyz/networks/solana), and [tokenized stocks](https://app.rwa.xyz/stocks) — where Solana settles most trading volume — as the fastest-growing slice. Assets at this scale need regulated custody, enforceable compliance, and auditable controls. SDP provides them as one API, so bringing an asset onchain does not require a team of Solana developers. ## Core Features ### Unified Programming Interface SDP puts one stable API in front of the Solana ecosystem's providers, so you integrate once and swap or add providers without changing your code. The same onramp call works for every ramp provider. The `provider` field is the only thing that changes. ```javascript title="onramp-quote.js" rotate-providers const response = await fetch( "https://api.solana.com/v1/payments/ramps/onramp/quote", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify({ provider: "moonpay", counterpartyId: "cp_...", destinationCustodyWalletId: "cwlt_...", assetRail: "usdc.solana", fiatCurrency: "USD", fiatAmount: "100.00", }), }, ); const quote = await response.json(); ``` ```python title="onramp_quote.py" rotate-providers response = requests.post( "https://api.solana.com/v1/payments/ramps/onramp/quote", headers={"Authorization": "Bearer sk_test_..."}, json={ "provider": "moonpay", "counterpartyId": "cp_...", "destinationCustodyWalletId": "cwlt_...", "assetRail": "usdc.solana", "fiatCurrency": "USD", "fiatAmount": "100.00", }, ) quote = response.json() ``` ```bash title="Terminal" rotate-providers curl -X POST https://api.solana.com/v1/payments/ramps/onramp/quote \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "provider": "moonpay", "counterpartyId": "cp_...", "destinationCustodyWalletId": "cwlt_...", "assetRail": "usdc.solana", "fiatCurrency": "USD", "fiatAmount": "100.00" }' ``` ### Wallet Custody Provision signing wallets through integrated custody providers, so your team never handles private keys. Hosted keys never leave the provider's MPC or TEE infrastructure and SDP only handles the signed transaction. Local development keys are encrypted at rest with `AES-256-GCM` using per-organization derived keys, and every signing request passes policy and permission checks before it executes. - **Built on `@solana/keychain`** The unified Solana signing interface. Every custody backend is a keychain adapter behind the same signing API, which is what makes providers interchangeable. - **[Wallet policies](/docs/wallet-operations/policies)** Constrain how a wallet can move funds. Destination controls, amount limits, operation permissions, approval requirements, and audit visibility, enforced before any operation executes. - **Scoped custody configuration** Custody settings resolve per project, then organization, then environment, so different teams can run different providers side by side. ### Issuance and Compliance with Token-2022 Create tokens from pre-built templates (stablecoin, tokenized security) or configure your own using Token-2022 extensions. Deploy, mint, burn, and manage supply through the dashboard or API. Those same extensions are what make compliance enforceable onchain. Freeze individual accounts, pause token activity, seize tokens, and manage each token's **Token Access Control** list for regulated issuance flows. Selected compliance actions accept reason or memo fields for audit trails. Every enforcement action is one POST on the same token surface. ```javascript title="compliance-action.js" rotate-token-ops const response = await fetch( "https://api.solana.com/v1/issuance/tokens/tok_abc123/freeze", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify(operationParams), }, ); const result = await response.json(); ``` ```python title="compliance_action.py" rotate-token-ops response = requests.post( "https://api.solana.com/v1/issuance/tokens/tok_abc123/freeze", headers={"Authorization": "Bearer sk_test_..."}, json=operation_params, ) result = response.json() ``` ```bash title="Terminal" rotate-token-ops curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/freeze \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d "$OPERATION_PARAMS" ``` ### Ramps and Onchain Transfers SDP exposes unified endpoints for onchain transfers and ramps across providers, built on official Solana program implementations like [Subscriptions](https://github.com/solana-program/subscriptions) and [Solana Pay](https://github.com/solana-foundation/pay). These power Recurring Payments, Payment Requests, and Batch Transfers within SDP. ```bash title="Terminal" curl -X POST https://api.solana.com/v1/payments/recurring-payments \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "sourceCustodyWalletId": "cwlt_123", "counterpartyId": "cp_...", "counterpartyAccountId": "cpa_...", "token": "USDC", "amount": "25.00", "periodHours": 720 }' ``` ```bash title="Terminal" curl -X POST https://api.solana.com/v1/payments/requests \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "walletId": "privy_wallet_123", "counterpartyId": "cp_...", "token": "USDC", "amount": "25.00", "expiresAt": "2026-08-31T00:00:00.000Z" }' ``` ```bash title="Terminal" curl -X POST https://api.solana.com/v1/payments/transfer-batches \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "externalId": "payroll_2026_06_30", "sourceCustodyWalletId": "cwlt_example", "token": "USDC", "recipients": [ { "externalId": "payroll_row_001", "counterpartyId": "cp_...", "counterpartyAccountId": "cpa_...", "amount": "25.00" } ] }' ``` The same API extends to on/offramps: rate estimates, quotes, and settlement work identically across every provider and corridor, so money can enter as fiat, live onchain as a token you issued, and exit back to fiat: a complete lifecycle from issuance through circulation to redemption, without stitching together provider-specific integrations. See [Payments](/docs/payments) for transfers and [Ramps](/docs/payments/ramps) for supported providers, currencies, and corridors. ### Audit Trails Every sensitive operation is recorded automatically. Each entry captures the acting user or API key, the resource touched, success or failure, client IP, user agent, request id, and secret-redacted metadata, and is queryable per organization. | Area | Recorded events | | ----------------------- | ---------------------------------------------------------------------------------- | | API keys | Create, revoke, and rotate, for both organization and project-scoped keys | | Token issuance | Deploy, mint, burn, force-burn, supply changes, and authority updates | | Compliance actions | Freeze, unfreeze, pause, unpause, seize, and Token Access Control changes | | Custody | Provider configuration, wallet provisioning, and credential lifecycle changes | | Transactions & signing | Signing requests, submissions, and failed submissions | | Organization & access | Members, invitations, projects, and project membership | | Counterparties & assets | Counterparty and Counterparty Account changes, asset profile changes | --- ## Developing with SDP ### Overview Source: https://platform.solana.com/docs/developing-with-sdp > The interfaces, environments, and conventions you build against. SDP exposes two interfaces that operate over the same backend and enforce the same permissions, policies, and audit trails: - **Dashboard** — browser-based UI for setup, token management, and compliance operations. Sign in with email, Google, or GitHub. - **REST API:** programmatic access for backend integrations. Routes authenticate with an API key in the `Authorization` header. Everything the dashboard does is available through the API. Build against the API for automation and integrations; use the dashboard to set up your organization, inspect state, and perform one-off operations. ## Environments Authenticated environments are a property of the **project**, not of individual requests. Every tenant resource and API key belongs to a project and inherits that project's environment. | Environment | Network | API key prefix | | ------------ | ------------ | -------------- | | `sandbox` | devnet | `sk_test_` | | `production` | mainnet-beta | `sk_live_` | Both environments expose the same route shapes. Develop and test authenticated integrations against a sandbox project, then switch to production by swapping your 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" } } ``` Include the `requestId` when reporting an issue — it links your request to our internal traces and audit records. ## Execution model SDP executes custody operations by building, signing, and submitting with the configured custody wallet. --- ### Idempotency Source: https://platform.solana.com/docs/developing-with-sdp/idempotency > Replay-safe mutations through the Idempotency-Key header, claimed by the record itself. Retries are a fact of distributed systems: clients time out, connections drop, workers die mid-run. SDP lets you replay a mutation safely by sending an `Idempotency-Key` header — a unique value (a UUID works well) that you mint once per logical operation and reuse on every retry of that operation. ```bash title="Terminal" curl -X POST https://api.solana.com/v1/payments/transfers \ -H "Authorization: Bearer sk_test_..." \ -H "Idempotency-Key: 8a2f6c1e-9d4b-4f3a-b7e5-2c8d9f0a1b3c" \ -H "Content-Type: application/json" \ -d '{ ... }' ``` ## How replay resolution works SDP's model differs from response-cache designs: the idempotency key is claimed by the **business record itself**, not by a snapshot of the original response. 1. The first request creates the record — a transfer, a batch, an issuance transaction — carrying your key and a fingerprint of the normalized request payload. 2. A retry with the same key and the same payload returns **HTTP 200 with the stored record**, in the normal success envelope. Because it is the live record and not a cached response, a replay reflects current state — a transfer that has since finalized comes back `finalized`, not the `processing` you would have seen on the first attempt. 3. A request with the same key but a **different payload** is rejected with **409 `CONFLICT`** ("Idempotency key already used with different request payload"). The fingerprint comparison is field-normalized, so formatting differences don't cause false conflicts. Keys are retained for the lifetime of the record they claimed — there is no TTL, and a replay works for as long as the record exists. The header itself is validated on every `/v1` route (1–255 printable ASCII characters), but only the endpoints below consume it. Elsewhere it is accepted and ignored. ## Where it applies Each domain claims keys in its own namespace, and the namespaces are scoped differently: | Domain | Operations | Key namespace | | --- | --- | --- | | Payments — transfers | `POST /v1/payments/transfers` | Per project | | Payments — batches | `POST /v1/payments/transfer-batches` | Per project | | Issuance | Execute-mode deploy, mint, burn, force-burn, freeze, thaw, pause, unpause, seize, and authority transfer | Per organization, shared across **all** issuance operations and tokens | | Custody | Provider credential submission (dashboard flow) | Required on every submission | Namespaces are independent across domains — the same key value used on a transfer and on a mint are two separate claims that never collide. Within issuance, however, the namespace is organization-wide: reusing a key for a different issuance operation, even on a different token, returns 409. ## This model is open to change The per-domain differences above reflect the order in which these surfaces shipped, not a finished design, and we expect them to converge. Treat the core contract as stable — one key per logical operation, replay returns the record, payload mismatch returns 409 — and treat the scoping details (per-project vs per-organization namespaces, which endpoints consume the header) as subject to change. We are considering extending consumption to more mutation endpoints and unifying namespace scoping across domains. --- ### Authentication Source: https://platform.solana.com/docs/developing-with-sdp/authentication > Authenticate API requests with project-scoped API keys, layered with roles, wallet bindings, and network controls. SDP layers independent controls onto a single API key — role, per-key permissions, wallet bindings, policy profiles, and IP allowlist — so a key grants exactly the access its integration needs, and no more. Authenticated API requests carry an API key in the `Authorization` header: ```bash title="Terminal" curl https://api.solana.com/v1/wallets \ -H "Authorization: Bearer sk_test_..." ``` The `Bearer` prefix is optional — a raw key in the `Authorization` header is also accepted. ## Key scope Keys are issued per **project** and inherit that project's environment: `sk_test_` keys belong to sandbox projects (devnet), `sk_live_` keys to production projects (mainnet-beta). A key can only reach the wallets, tokens, and payments of the project it was issued for; 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 | | --- | --- | | **Role** | Baseline permission set — see the table below | | **Per-key permissions** | Optional fine-grained overrides of the role's defaults. A key can only grant permissions its creator holds | | **Wallet bindings** | Restrict which custody wallets the key can operate, each binding with its own permission list | | **Policy profiles** | Rule sets (destination controls, amount limits, approval requirements) evaluated before a bound wallet moves funds | | **IP allowlist** | Optional IPv4/IPv6/CIDR ranges the key may be used from | | Role | Description | | --- | --- | | `api_admin` | Full access including custody and platform operations | | `api_developer` | Read/write access to tokens, payments, counterparties, wallets, and webhooks; excludes custody administration | | `api_readonly` | Read-only access to all resources, including audit records | ## Errors Authentication and authorization failures are always machine-distinguishable: | Condition | Status | Code | | --- | --- | --- | | No API key on a keyed-only request | 401 | `UNAUTHORIZED` | | Malformed or unknown key | 401 | `INVALID_API_KEY` | | Key has been revoked | 401 | `REVOKED_API_KEY` | | Key past its expiry or rotation deadline | 401 | `EXPIRED_API_KEY` | | Request origin not in the key's IP allowlist | 403 | `FORBIDDEN` | | Key lacks a required permission | 403 | `INSUFFICIENT_PERMISSIONS` | | Per-key rate limit exceeded | 429 | `RATE_LIMITED` | Requests are rate limited per key; the standard tier allows 100 requests per minute. 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 role-based access, wallet bindings, and zero-downtime rotation. Create, rotate, and revoke the API keys that authenticate SDP API requests. Each key belongs to a project, inherits its environment, and carries a role plus optional fine-grained controls — see [Authentication](/docs/developing-with-sdp/authentication) for how those layers are enforced. The full secret is returned exactly once — at creation and at rotation. SDP stores only a salted hash and a display prefix; a lost key cannot be recovered, only rotated or replaced. Every key tracks `lastUsedAt` so you can identify stale credentials before revoking them. ## Key format | Environment | Prefix | Solana network | | --- | --- | --- | | Sandbox | `sk_test_` | devnet | | Production | `sk_live_` | mainnet-beta | ## Roles | Role | Description | | --- | --- | | `api_admin` | Full access including custody and platform operations | | `api_developer` | Read/write access, excludes custody actions | | `api_readonly` | Read-only access to all resources | A key can also carry explicit `permissions` that override the role's defaults. A key can only be granted permissions its creator holds. ## Create a key 1. Navigate to **API keys** in the sidebar. You start with an empty list. ![API keys page with no keys yet](/images/getting-started/api-keys-empty.png) 2. Click **New API key** in the top right, then fill in the key details: - **Name** — a descriptive label (e.g., "CI deploy key") - **Role** — Admin, Developer, or Read only - **Wallet access** — All wallets or Selected wallets - **Expiration (optional)** — date/time picker ![Create API key modal](/images/getting-started/api-key-create.png) 3. Click **Continue**, review the summary, and click **Create key**. ![Review API key modal](/images/getting-started/api-key-review.png) 4. Click **Copy** on the **API key generated** modal and save the key — it will not be shown again. ![API key generated modal showing the full key](/images/getting-started/api-key-generated.png) 5. Dismiss the modal. The key now appears in the table with its prefix, role, environment, and status. ![API keys list with one active key](/images/getting-started/api-keys-list.png) ```bash title="Terminal" curl -X POST https://api.solana.com/v1/api-keys \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "name": "CI deploy key", "role": "api_developer", "walletScope": "all", "expiresAt": "2026-12-31T23:59:59Z" }' ``` ```typescript title="create-api-key.ts" const response = await fetch("https://api.solana.com/v1/api-keys", { method: "POST", headers: { "Authorization": "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify({ name: "CI deploy key", role: "api_developer", walletScope: "all", expiresAt: "2026-12-31T23:59:59Z", }), }); const { data } = await response.json(); // data.apiKey.key — save this, only shown once ``` ```java title="CreateApiKey.java" HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.solana.com/v1/api-keys")) .header("Authorization", "Bearer sk_test_...") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(""" { "name": "CI deploy key", "role": "api_developer", "walletScope": "all", "expiresAt": "2026-12-31T23:59:59Z" }""")) .build(); ``` The new key belongs to the project of the key that created it and inherits that project's environment — there is no environment field on the request. The response includes the full `key` value — **save it, it is only returned once**. Optional fields: `description`, `permissions` (fine-grained overrides), `allowedIps` (IPv4/IPv6/CIDR ranges), `signingWalletId` / `signingWalletIds`, `walletBindings` (per-wallet permission lists). ## Rotate a key Rotation replaces the secret without an availability gap: the new key is a fresh record that clones the old key's role, permissions, IP allowlist, wallet bindings, and policy profiles, while the old key keeps working until its grace deadline. During the grace period both keys are valid; after it, the old key fails with `EXPIRED_API_KEY`. 1. In the API keys table, open the **Actions** dropdown next to the key and click **Rotate key (24h grace)**. The dashboard always uses a 24-hour grace period — use the API for a custom value (0–168h). ![Actions dropdown showing Rotate key and Delete key options](/images/getting-started/api-key-rotate.png) 2. Copy the new key value from the generated key modal — it is shown once. ```bash title="Terminal" curl -X POST https://api.solana.com/v1/api-keys/key_abc123/rotate \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "gracePeriodHours": 24 }' ``` ```typescript title="rotate-api-key.ts" const response = await fetch( "https://api.solana.com/v1/api-keys/key_abc123/rotate", { method: "POST", headers: { "Authorization": "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify({ gracePeriodHours: 24 }), } ); const { data } = await response.json(); // data.apiKey — the new key, only shown once // data.previousKey.rotationDeadline — when the old key stops working ``` ```java title="RotateApiKey.java" HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.solana.com/v1/api-keys/key_abc123/rotate")) .header("Authorization", "Bearer sk_test_...") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(""" { "gracePeriodHours": 24 }""")) .build(); ``` `gracePeriodHours` accepts 0–168 (default 24). A key cannot rotate itself — authenticate the rotation with a different key or a Clerk JWT from the dashboard. ## Revoke a key Open the **Actions** dropdown next to the key and click **Delete key**. The key stops working immediately and cannot be restored. ```bash title="Terminal" curl -X DELETE https://api.solana.com/v1/api-keys/key_abc123 \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "confirmation": "CI deploy key" }' ``` ```typescript title="revoke-api-key.ts" await fetch("https://api.solana.com/v1/api-keys/key_abc123", { method: "DELETE", headers: { "Authorization": "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify({ confirmation: "CI deploy key" }), }); ``` ```java title="RevokeApiKey.java" HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.solana.com/v1/api-keys/key_abc123")) .header("Authorization", "Bearer sk_test_...") .header("Content-Type", "application/json") .method("DELETE", HttpRequest.BodyPublishers.ofString(""" { "confirmation": "CI deploy key" }""")) .build(); ``` The `confirmation` field must match the key's **name**. Revocation takes effect immediately, is idempotent (revoking an already-revoked key succeeds), and cannot be undone. As with rotation, a key cannot revoke itself. --- ## Getting Started ### Set Up Your Organization & Projects Source: https://platform.solana.com/docs/guides/setup-organization > Set up your SDP organization and see how projects scope work within it. Every SDP user works through an **organization** — a team workspace containing one or more **projects**, each a provisioned environment with its own API keys, wallets, and members. This guide creates your organization and covers how projects scope your resources. ### 1. Sign up Go to [platform.solana.com](https://platform.solana.com) and click **Dashboard**. On the sign-in screen, click **Sign up** to create a new account. ![SDP sign-in screen with Sign up link](/images/getting-started/sign-in.png) Enter your email address and a password to create your account. ![Create your account screen](/images/getting-started/create-account.png) ### 2. Create or select an organization After signing in you'll be prompted to set up your organization. Upload a logo and enter your organization name, then click **Continue**. ![Setup your organization screen](/images/getting-started/setup-organization.png) If you already have an organization, the **Organization Switcher** in the top-left of the sidebar lets you switch between them or create a new one. Organization Switcher dropdown ### 3. Organize with projects A **project** is a provisioned environment within your organization — sandbox or production-style — with its own API keys, wallets, tokens, and members, so teams don't share the same wallet and token surface. See the [Projects API reference](/docs/reference/api/projects) for managing them programmatically. ### 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 - [Create your first token](/docs/tokens/create-a-token) --- ### Set Up Wallets Source: https://platform.solana.com/docs/guides/setup-wallets > Initialize a custody provider and create wallets for signing transactions. A **custody wallet** is a Solana keypair whose key is held and signed for by a custody provider — SDP routes signing requests to whichever provider holds the wallet's key. Initialize a provider, then create wallets for token deployment, minting, transfers, and other onchain operations. ## Supported providers | Provider | Description | | --- | --- | | **Privy** | Embedded wallet infrastructure platform | | **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 | | **Anchorage** | Regulated institutional crypto custody | ### 1. Navigate to Wallets Open the sidebar and click **Wallets**. If you haven't linked your organization yet, complete the [organization setup](/docs/guides/setup-organization) first. ### 2. Choose a custody provider The Wallets page shows all supported providers, each with a **New wallet** button. Pick the provider that fits your setup. ![Wallets page showing custody provider cards](/images/getting-started/wallet-providers.png) ### 3. Create the wallet Click **New wallet** on your chosen provider. In the modal, enter a label for the wallet and click **Create wallet**. New wallet modal ### 4. View your wallet After provisioning, the wallet card appears on the Wallets page with its address, wallet ID, and balance. ![Wallets page after wallet creation](/images/getting-started/wallet-created.png) To create additional wallets, click **Create Wallet** in the top right and repeat the process. ### 5. Provider capabilities Each provider supports a different set of operations shown as capability chips: - **Issuance** — can deploy and mint tokens - **Transfers** — can sign payment transactions - **Compliance** — can sign freeze/unfreeze instructions #### 1. Initialize the signing provider ```bash title="Terminal" curl -X POST https://api.solana.com/v1/wallets/initialize \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "provider": "privy", "walletLabel": "Master wallet" }' ``` ```typescript title="initialize-wallet-provider.ts" const response = await fetch("https://api.solana.com/v1/wallets/initialize", { method: "POST", headers: { "Authorization": "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify({ provider: "privy", walletLabel: "Master wallet", }), }); const { data } = await response.json(); // data: { configId, publicKey, walletId } ``` ```java title="InitializeWalletProvider.java" HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.solana.com/v1/wallets/initialize")) .header("Authorization", "Bearer sk_test_...") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(""" { "provider": "privy", "walletLabel": "Master wallet" }""")) .build(); ``` #### 2. Create additional wallets ```bash title="Terminal" curl -X POST https://api.solana.com/v1/wallets \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "label": "Mint authority wallet", "purpose": "mint_authority", "setDefault": false }' ``` ```typescript title="create-wallet.ts" const response = await fetch("https://api.solana.com/v1/wallets", { method: "POST", headers: { "Authorization": "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify({ label: "Mint authority wallet", purpose: "mint_authority", setDefault: false, }), }); ``` ```java title="CreateWallet.java" HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.solana.com/v1/wallets")) .header("Authorization", "Bearer sk_test_...") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(""" { "label": "Mint authority wallet", "purpose": "mint_authority", "setDefault": false }""")) .build(); ``` The `purpose` field is optional metadata in the API. The current dashboard create-wallet flow does not expose wallet-purpose selection. #### 3. List wallets ```bash title="Terminal" curl "https://api.solana.com/v1/wallets?view=summary" \ -H "Authorization: Bearer sk_test_..." ``` ```typescript title="list-wallets.ts" const response = await fetch("https://api.solana.com/v1/wallets?view=summary", { headers: { "Authorization": "Bearer sk_test_..." }, }); const { data } = await response.json(); // data.wallets includes walletId, publicKey, provider, label, purpose, and status ``` ```java title="ListWallets.java" HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.solana.com/v1/wallets?view=summary")) .header("Authorization", "Bearer sk_test_...") .GET() .build(); ``` Use the returned `walletId` when setting a default wallet or binding wallet-scoped API keys. #### 4. Set a default wallet ```bash title="Terminal" curl -X POST https://api.solana.com/v1/wallets/default-wallet \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "walletId": "wal_xyz789" }' ``` ```typescript title="set-default-wallet.ts" await fetch("https://api.solana.com/v1/wallets/default-wallet", { method: "POST", headers: { "Authorization": "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify({ walletId: "wal_xyz789" }), }); ``` ```java title="SetDefaultWallet.java" HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.solana.com/v1/wallets/default-wallet")) .header("Authorization", "Bearer sk_test_...") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(""" { "walletId": "wal_xyz789" }""")) .build(); ``` --- ## Tokens ### Tokenize an Asset Source: https://platform.solana.com/docs/tokens/tokenize-an-asset > Map a real-world asset to the right SDP token model, authorities, and implementation flow. Use this page when you want to issue a stablecoin, tokenized security, loyalty token, or any other asset through Solana Developer Platform. It walks you from business decision to the first API call. ## Choose a token model Pick the **Tokenization Template** that matches your asset's compliance and operational requirements: | Asset model | Template | Typical controls | | --- | --- | --- | | Fiat-backed stablecoin | `stablecoin` | Mint authority, freeze authority, pausable, permanent delegate | | Tokenized security or RWA | `tokenized-security` | Allowlist required, freeze authority, pausable, permanent delegate | | Loyalty, rewards, or utility token | `custom` | Manually selected extensions | | Non-standard asset | `custom` | Manually selected extensions | **Custody wallets** hold your token's authorities by default, so SDP can execute deploy, mint, and compliance actions without external signing. ## Implementation sequence Each step links to its own guide with UI and API instructions: 1. [Set Up Your Organization & Projects](/docs/guides/setup-organization) 2. [Set Up Wallets](/docs/guides/setup-wallets) 3. [Manage API Keys](/docs/developing-with-sdp/manage-api-keys) 4. [Create a Token](/docs/tokens/create-a-token) 5. [Deploy a Token](/docs/tokens/deploy-a-token) 6. [Mint and Burn](/docs/tokens/mint-and-burn) 7. [Basic payment](/docs/payments/send-basic-payment) — transfer tokens between wallets 8. [Allowlists](/docs/tokens/allowlists) — Token Access Control, required for `tokenized-security` 9. [Manage Token Settings](/docs/tokens/manage-token-settings) — freeze, pause, seize for compliance workflows ## Core API endpoints | Method | Endpoint | Purpose | | --- | --- | --- | | `GET` | `/v1/issuance/templates` | List available token templates | | `POST` | `/v1/issuance/tokens` | Create a token definition | | `POST` | `/v1/issuance/tokens/{tokenId}/deploy` | Deploy to Solana | | `POST` | `/v1/issuance/tokens/{tokenId}/mint` | Mint new supply | | `POST` | `/v1/issuance/tokens/{tokenId}/burn` | Reduce supply | | `POST` | `/v1/issuance/tokens/{tokenId}/allowlist` | Add a Token Access Control entry | | `POST` | `/v1/issuance/tokens/{tokenId}/freeze` | Freeze an account | | `POST` | `/v1/issuance/tokens/{tokenId}/pause` | Pause all transfers | | `POST` | `/v1/payments/transfers` | Transfer tokens | See the full schema in the [Issuance API reference](/docs/reference/api/issuance) and [Payments API reference](/docs/reference/api/payments). ## Operational constraints - A token must be **created** before deployment, and **deployed** before minting or transfer. - If the token is paused, minting and transfer operations are unavailable until it is unpaused. - Freeze and unfreeze flows accept a holder wallet address; SDP derives the associated token account automatically. - SDP operations require the relevant authority on a custody wallet. If an authority has been rotated outside SDP, rotate it back to a controlled wallet before deploying, minting, or running compliance actions. ## For AI agents If you are driving SDP through an agent workflow, start from [Documentation for AI](/docs/reference/docs-for-ai). That page links the machine-readable SDP discovery files and keeps AI-facing guidance grounded in the supported public API surface. --- ### Create a Token Source: https://platform.solana.com/docs/tokens/create-a-token > Define a new token using a template or custom configuration. Token creation defines your token's metadata, extensions, and operational rules. The token is saved in SDP but **not yet deployed** onchain — see [Deploy a Token](/docs/tokens/deploy-a-token) for the next step. ![Empty Issuance page with a create card](/images/tokens/token-list-empty.png) ## Templates | Template | Decimals | Key extensions | Use case | | --- | --- | --- | --- | | `stablecoin` | 6 | Permanent delegate, pausable | USD-backed tokens | | `tokenized-security` | 8 | Permanent delegate, pausable, scaled UI amount, allowlist required | Regulated assets | | `custom` | 9 | None — you choose | Full control | ## How it works Select the **Tokenization Template** that matches your asset model. Templates pre-fill extensions and constraints you can review before confirming. Navigate to **Issuance** in the sidebar and click **Create token** in the top right corner. The first screen shows three template cards: - **Stablecoin** — USD-backed tokens with compliance controls - **Tokenized Security** — regulated assets with allowlist requirement - **Custom** — fully configurable Token-2022 setup Select a template and click **Continue**. ![Template selection modal showing Stablecoin, Tokenized Security, and Custom cards](/images/tokens/token-create-templates.png) List available templates to see what fields each one pre-configures: ```bash title="Terminal" curl https://api.solana.com/v1/issuance/templates \ -H "Authorization: Bearer sk_test_..." ``` ```typescript title="list-templates.ts" const res = await fetch("https://api.solana.com/v1/issuance/templates", { headers: { Authorization: "Bearer sk_test_..." }, }); const { data } = await res.json(); // data[].id — use as the "template" value when creating ``` Set the core metadata: name, symbol, decimals, and a URI pointing to your token metadata JSON. In step 2 of 3, fill in the identity fields: - **Metadata URI** — HTTPS URL for the token metadata JSON - **Token name** — e.g., "Acme Dollar" - **Symbol** — 1–10 uppercase alphanumeric characters, e.g., `ACME` - **Decimals** — options depend on the selected template Click **Continue** when ready. ![Identity configuration form with Metadata URI, Token Name, Symbol, and Decimals fields](/images/tokens/token-create-identity.png) Pass identity fields in the `POST /v1/issuance/tokens` body: ```bash title="Terminal" curl -X POST https://api.solana.com/v1/issuance/tokens \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Dollar", "symbol": "ACME", "decimals": 6, "description": "USD-backed stablecoin", "template": "stablecoin" }' ``` Choose the exact SDP Wallet that will deploy the token, and configure Token Access Control and supply controls. In step 3 of 3, configure: - **Main signer** — the exact SDP-controlled wallet selected for deployment - **Allowlist** — Token Access Control's allowlist mode; toggle on or off, required and locked for `tokenized-security` Click **Create token** to save the draft. ![Operational settings form showing Main Signer and Transfer Controls with Allowlist and Denylist toggles](/images/tokens/token-create-settings.png) Add operational fields to the same body. `signingCustodyWalletId` is the exact SDP Wallet `id` returned by `GET /v1/wallets` (for example, `cwlt_*`), not the provider-specific `walletId` or the wallet's public key. You can omit it while drafting and provide it when you deploy. ```typescript title="create-token.ts" const res = await fetch("https://api.solana.com/v1/issuance/tokens", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Acme Dollar", symbol: "ACME", decimals: 6, description: "USD-backed stablecoin", template: "stablecoin", signingCustodyWalletId: "cwlt_abc123", requiresAllowlist: false, isMintable: true, isFreezable: true, }), }); ``` For a custom token, omit `template` or set it to `"custom"` and pass `overrides.extensions`: ```typescript title="create-custom-token.ts" body: JSON.stringify({ name: "Game Coin", symbol: "GAME", decimals: 0, template: "custom", signingCustodyWalletId: "cwlt_abc123", overrides: { extensions: { pausable: {}, transferFee: { basisPoints: 50, maxFee: "1000" }, }, }, isMintable: true, isFreezable: false, }), ``` The token is saved in SDP with status `pending`. Nothing has been written to Solana yet. A success toast confirms creation. The token appears in the **Issuance** list with a `Draft` badge. ![Issuance list showing the newly created token with a Draft badge](/images/tokens/token-list-draft.png) Open the token to see its detail page. The status shows **Not deployed** and a **Deploy** button is available to move to the next step. ![Token detail page showing Not deployed status and Deploy button](/images/tokens/token-detail-pending.png) A successful response returns the new token object: ```typescript title="create-token.ts" const { data } = await res.json(); // data.token.id — save this; used in all subsequent calls // data.token.status — "pending" // data.token.signingCustodyWalletId — exact wallet selected for deployment ``` The token is not yet onchain. Pass `data.token.id` to [Deploy a Token](/docs/tokens/deploy-a-token). --- ### Deploy a Token Source: https://platform.solana.com/docs/tokens/deploy-a-token > Deploy a created token to the Solana blockchain. After [creating a token](/docs/tokens/create-a-token), deploy it onchain. Deployment creates the mint account on Solana and signs the deployment with the exact SDP Wallet selected for execution. ## Prerequisites - A token in `pending` status - The exact SDP Wallet `id` returned by `GET /v1/wallets` (for example, `cwlt_*`) ## How it works Locate the token you want to deploy. Deployment is irreversible — the mint account and its configured authorities are created onchain. In the **Issuance** list, click the token with a `Pending` badge to open its detail page. The **Deploy** button appears in the token header and in the fund management panel. ![Token detail page showing Not deployed status and Deploy button](/images/tokens/token-detail-pending.png) You need the `tokenId` returned when you created the token. If you don't have it, list your tokens: ```bash title="Terminal" curl https://api.solana.com/v1/issuance/tokens \ -H "Authorization: Bearer sk_test_..." ``` Note the token `id` (e.g., `tok_abc123`) — required for all deploy and post-deploy calls. Also list your wallets and use the wallet record's `id`, not its provider-specific `walletId` or public key: ```bash title="Terminal" curl https://api.solana.com/v1/wallets \ -H "Authorization: Bearer sk_test_..." ``` SDP signs and submits the deploy transaction using `signingCustodyWalletId`. If you supply it here, it overrides the pending draft's execution wallet; if you omit it, SDP uses the value stored on the draft. One of those values is required. An override changes the deployment signer and historical attribution, but it does not rewrite immutable authority values already captured in the token's configuration. Click **Deploy**. A modal shows the signer wallet that will authorize the transaction. Review the wallet ID and public key, then click **Deploy now**. ![Deploy token modal showing the signer wallet details](/images/tokens/token-deploy-modal.png) A confirmation prompt asks you to confirm the on-chain submission. Click **Deploy now** to proceed. ![Deploy token confirmation dialog](/images/tokens/token-deploy-confirm.png) SDP submits the transaction to Solana and polls for confirmation. The page updates automatically when deployment completes. ```bash title="Terminal" curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/deploy \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: deploy-acme-001" \ -d '{"signingCustodyWalletId":"cwlt_abc123"}' ``` ```typescript title="deploy-token.ts" const res = await fetch( "https://api.solana.com/v1/issuance/tokens/tok_abc123/deploy", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Content-Type": "application/json", "Idempotency-Key": "deploy-acme-001", }, body: JSON.stringify({ signingCustodyWalletId: "cwlt_abc123" }), } ); const { data } = await res.json(); // data.token.mintAddress — the onchain mint address ``` Always pass an `Idempotency-Key` — retrying with the same key after a network error prevents double-deployment. Once the transaction is confirmed, the token is active and ready for minting and transfers. The token status badge changes from `Not deployed` to the onchain mint address. The detail page shows Mint and Burn actions in the Operations tab, and a confirmed deploy transaction in the Transactions table. ![Deployed token detail page with mint address, Operations tab, and confirmed deploy transaction](/images/tokens/token-deployed.png) The deploy response includes the confirmed state: ```typescript title="deploy-token.ts" // data.token.status — "active" // data.token.mintAddress — e.g., "AcMeXYZ..." // data.token.signingCustodyWalletId — exact wallet used for deployment ``` After deployment, `signingCustodyWalletId` is historical deployment attribution. Mint and admin actions resolve their current on-chain authority, while a standard burn uses its exact source wallet; none use the deployment wallet as a fallback. Fetch the token at any time to verify: ```bash title="Terminal" curl https://api.solana.com/v1/issuance/tokens/tok_abc123 \ -H "Authorization: Bearer sk_test_..." ``` --- ### Mint and Burn Source: https://platform.solana.com/docs/tokens/mint-and-burn > Increase or decrease token supply by minting to an address or burning from an account you control. After [deploying a token](/docs/tokens/deploy-a-token), you can mint new tokens to any address and burn tokens from accounts you control. Force-burn lets admins burn from any account without the holder's signature. ## Prerequisites - A deployed token with status `active` - `tokens:write` permission — or `tokens:admin` for force-burn - For standard burn, the exact SDP Wallet `id` returned by `GET /v1/wallets` (for example, `cwlt_*`) ## How it works Open the token you want to manage. Mint and burn are both available from the token detail page. In the **Issuance** list, click an active token to open its detail page. The **Fund management** panel shows the current supply with two primary actions: **Mint** and **Burn**. ![Token Operations tab showing Mint and Burn actions](/images/tokens/token-deployed.png) All supply operations share this URL pattern: ```text title="Endpoints" POST /v1/issuance/tokens/{tokenId}/mint POST /v1/issuance/tokens/{tokenId}/burn POST /v1/issuance/tokens/{tokenId}/force-burn ``` The `amount` field is a decimal string in **UI units** (human-readable token amounts). `"1"` means one token; `"1.5"` means one and a half. SDP converts to on-chain base units using the token's `decimals`. `signingCustodyWalletId` always means the exact SDP Wallet `id`, not a provider-specific `walletId` or public key. It is required for a standard burn because that wallet owns and signs for the source. For mint and force-burn it is optional, but when supplied it must control the current mint or permanent-delegate authority. When omitted, SDP requires exactly one accessible wallet to match that live authority. Later actions never fall back to the token's deployment wallet. Issue new tokens to a destination address. SDP derives the associated token account automatically. In the **Mint** form: - **Destination** — wallet address to receive the new tokens - **Amount** — in human-readable units (e.g., `1.0`) - **Memo** — optional, recorded onchain Click **Mint**. A modal shows the signer wallet and a destination picker — select or paste the destination address. Fill in the amount and optional memo, then click **Mint tokens**. ![Mint Tokens modal with amount and memo filled in](/images/tokens/mint-modal-filled.png) Confirm the on-chain submission in the confirmation prompt. ![Mint tokens confirmation dialog — click Mint now to submit the transaction on-chain](/images/tokens/mint-confirm.png) Once confirmed, the mint appears in the Transactions table with a `confirmed` status and a "Mint transaction finalized." toast appears. ![Operations tab showing confirmed mint entry and Mint transaction finalized toast](/images/tokens/mint-confirmed.png) ```bash title="Terminal" curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/mint \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: mint-001" \ -d '{ "signingCustodyWalletId": "cwlt_abc123", "mint": { "destination": "7xKXz...9fGh", "amount": "1000", "memo": "Initial distribution" } }' ``` ```typescript title="mint-tokens.ts" const res = await fetch( "https://api.solana.com/v1/issuance/tokens/tok_abc123/mint", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Content-Type": "application/json", "Idempotency-Key": "mint-001", }, body: JSON.stringify({ signingCustodyWalletId: "cwlt_abc123", mint: { destination: "7xKXz...9fGh", amount: "1000", memo: "Initial distribution", }, }), } ); ``` Control priority with `options.priorityFee`: `"none"`, `"low"`, `"medium"`, `"high"`, or a micro-lamport value. Reduce supply by burning from an account your wallet controls. Use force-burn to burn from any holder account without their signature. Click **Burn**. A modal shows the signer and a source field — select the wallet holding the tokens you want to remove. ![Burn Tokens modal with empty source field](/images/tokens/burn-modal-empty.png) Select the source wallet from the dropdown. ![Burn Tokens modal with source wallet dropdown open](/images/tokens/burn-modal-source.png) Fill in the amount and optional memo, then click **Burn tokens**. ![Burn Tokens modal filled with source, amount, and memo](/images/tokens/burn-modal-filled.png) Confirm the on-chain submission in the confirmation prompt. ![Burn tokens confirmation dialog](/images/tokens/burn-confirm.png) The "Burn transaction finalized." toast appears and the burn shows as confirmed in the Transactions table. ![Operations tab showing confirmed burn transaction and finalized toast](/images/tokens/burn-confirmed.png) For **Force burn** (requires `tokens:admin`), use the Compliance tab instead of the Operations tab — force-burn lets admins remove tokens from any holder account without their signature. **Burn from a controlled account:** ```typescript title="burn-tokens.ts" await fetch("https://api.solana.com/v1/issuance/tokens/tok_abc123/burn", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Content-Type": "application/json", "Idempotency-Key": "burn-001", }, body: JSON.stringify({ signingCustodyWalletId: "cwlt_abc123", burn: { source: "3xYZa...2aBc", amount: "250", memo: "Redemption" }, }), }); ``` **Force-burn from any holder** (requires `tokens:admin`): ```typescript title="force-burn-tokens.ts" await fetch("https://api.solana.com/v1/issuance/tokens/tok_abc123/force-burn", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Content-Type": "application/json", "Idempotency-Key": "force-burn-001", }, body: JSON.stringify({ signingCustodyWalletId: "cwlt_abc123", forceBurn: { source: "3xYZa...2aBc", amount: "250", memo: "Compliance action" }, }), }); ``` After minting or burning, refresh the cached supply total. The dashboard refreshes supply automatically after a confirmed operation. The Transactions table shows all completed operations — deploy, mint, and burn — each with a `confirmed` status. ![Operations tab showing confirmed burn, mint, and deploy transactions](/images/tokens/operations-history.png) Reload the token detail page if the displayed supply value appears stale. ```bash title="Terminal" curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/supply/refresh \ -H "Authorization: Bearer sk_test_..." ``` Safe to include in automated post-mint workflows. --- ### Allowlists Source: https://platform.solana.com/docs/tokens/allowlists > Control which addresses can receive your token by maintaining an approved list. **Token Access Control** gates who may hold or receive your token. This page covers its `allowlist` mode: tokens created with `requiresAllowlist: true` enforce a list of approved destination addresses on allowlist-aware issuance flows such as minting and administrative transfers, and it's required for `tokenized-security` tokens. This is distinct from a wallet's transfer allowlist (a Wallet Policy concern) and the Platform Allowlist (deployment admission). ## Prerequisites - A token created with `requiresAllowlist` enabled - `tokens:write` permission on the API key ## How it works The allowlist is configured at token creation time. Decide before creating whether your token requires one — it cannot be changed after deployment. In the five-step **Create token** flow, open **Token settings** and toggle **Allowlist** on. For `tokenized-security` tokens, the allowlist toggle is pre-enabled and locked. The **Operations** tab on the token detail page shows **Approved recipients**, with Address and Label fields and a Control Lists sidebar tracking entry and frozen account counts. ![Operational settings form showing the Allowlist toggle during token creation](/images/tokens/token-create-settings.png) Set `requiresAllowlist: true` when creating the token: ```typescript title="enable-allowlist.ts" body: JSON.stringify({ name: "Acme Security", symbol: "ACMES", decimals: 8, template: "tokenized-security", requiresAllowlist: true, isMintable: true, isFreezable: true, }), ``` Once deployed, the allowlist is enforced automatically on issuance flows. Add a wallet address to the allowlist before minting to it or using it as an administrative transfer destination. Open the token detail page, go to **Operations**, then select **Approved recipients**. Enter the wallet address and an optional label, then click **Add allowlist entry**. The address is enforced on the next allowlist-checked operation. For an on-chain list, SDP uses its authority wallet automatically when there is one matching wallet. If several custody records match, select the signer in the confirmation dialog. Database-only lists need no signer. ![Approved recipients section with empty address and label fields, ready to add a new entry](/images/tokens/allowlist-empty.png) After adding, the entry appears below the form with a **Remove entry** button. ![Allowlist entry added and visible in the list](/images/tokens/allowlist-entry-added.png) ```bash title="Terminal" curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/allowlist \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "address": "7xKXz...9fGh", "label": "Treasury wallet" }' ``` ```typescript title="add-allowlist-entry.ts" const res = await fetch( "https://api.solana.com/v1/issuance/tokens/tok_abc123/allowlist", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify({ address: "7xKXz...9fGh", label: "Treasury wallet" }), } ); // res.data.entry.id — save this to remove the entry later ``` For on-chain additions, the JSON body accepts optional `signingCustodyWalletId` (an exact `cwlt_…` record ID). It must control the list's current authority, which may differ from the token's freeze authority. Without the selector, multiple matching custody records return `409`. Selecting a wallet does not bypass permissions or runtime availability checks. Check which addresses are currently approved. Useful before minting to a new destination or during a compliance audit. The **Approved recipients** section shows all approved addresses, their labels, and when they were added. Use the search field to find a specific address by label or public key. ```bash title="Terminal" curl "https://api.solana.com/v1/issuance/tokens/tok_abc123/allowlist?search=So1&label=Treasury" \ -H "Authorization: Bearer sk_test_..." ``` ```typescript title="list-allowlist.ts" const { data, meta } = await res.json(); // meta: { total, page, pageSize, hasMore } ``` Query parameters: - `page`, `pageSize` — pagination (`pageSize` max 500, default 50). - `search` — contains-style match over the entry address **and** label. A blank value is treated as no filter. - `label` — restrict to entries with this exact label. Search and the label filter run server-side over the whole list, so results aren't limited to the current page. To populate a label filter, fetch the distinct labels in use (the response also includes `total`, the unfiltered entry count, for a summary display): ```bash curl https://api.solana.com/v1/issuance/tokens/tok_abc123/allowlist/labels \ -H "Authorization: Bearer sk_test_..." ``` ```typescript const { data } = await res.json(); // data: { labels: ["Market maker", "Treasury"], total: 42 } ``` Remove an address when a holder's compliance status changes. Existing token holdings are unaffected — only future allowlist-enforced flows are blocked. In **Operations → Approved recipients**, click **Remove entry** next to the address. For an on-chain list with multiple matching wallets, choose the signer in the confirmation dialog. After successful removal, the list and its count refresh. ```bash title="Terminal" curl -X DELETE \ https://api.solana.com/v1/issuance/tokens/tok_abc123/allowlist/alw_abc123 \ -H "Authorization: Bearer sk_test_..." ``` Use the `entry.id` returned when the address was added. For an on-chain removal, pass the optional exact signer as a query parameter: `?signingCustodyWalletId=cwlt_example`. To discover the current list authority, request `GET /v1/issuance/tokens/{tokenId}?includeAllowlistAuthority=true` and read `data.allowlistAuthority` (`null` for a database-only list). Match that address against your wallets; do not infer it from the token's freeze authority. ## Typical workflow for a tokenized security 1. Create the token with `requiresAllowlist: true` and template `tokenized-security` 2. [Deploy the token](/docs/tokens/deploy-a-token) 3. Collect wallet addresses from KYC-verified investors 4. Add each verified address to the allowlist 5. [Mint tokens](/docs/tokens/mint-and-burn) to allowlisted addresses 6. Remove addresses when compliance status changes --- ### Freeze and Compliance Source: https://platform.solana.com/docs/tokens/freeze-and-compliance > Screen addresses, freeze accounts, pause transfers, and seize tokens for compliance workflows. SDP provides two categories of compliance tooling: - **Token controls** — freeze individual accounts, pause all transfers, or seize tokens. Available for tokens with `isFreezable` enabled or the `pausable` extension. - **Address screening** — check a wallet against configured compliance providers before approving a destination or adding it to a token's Token Access Control allowlist. Token-control actions require `tokens:admin` permission on the API key. ## How it works Check a Solana address against configured compliance providers before onboarding a wallet or approving it as a transfer destination. Use the **Screen address** action in the Compliance section of the dashboard. Enter the wallet address and select the intent (e.g., transfer destination or Token Access Control allowlist candidate). Results appear per provider — each showing risk level, score, and flags. ```bash title="Terminal" curl -X POST https://api.solana.com/v1/compliance/address-screenings \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "address": "8dHEsGLp...hQWRGZ", "network": "solana", "intent": "transfer_destination" }' ``` ```typescript title="screen-address.ts" const { data } = await res.json(); // data.screening.providers[] // → { provider, status, riskScore, riskLevel, message, evaluatedAt } ``` See the [Compliance API reference](/docs/reference/api/compliance) for the full schema. Prevent a specific holder from sending or receiving this token. The freeze targets one associated token account — other accounts are unaffected. On the token detail page, go to the **Compliance** tab and select **Freeze Controls**. ![Freeze Controls tab with empty wallet address and reason fields](/images/tokens/freeze-controls-empty.png) Enter the holder's wallet address and an optional reason, then click **Freeze account**. ![Freeze Controls tab with wallet address and reason filled in](/images/tokens/freeze-controls-filled.png) Confirm the on-chain submission in the confirmation prompt. ![Freeze account confirmation dialog](/images/tokens/freeze-confirm.png) The "Freeze transaction finalized." toast appears and the Frozen Accounts count updates to 1. ![Compliance tab showing Freeze transaction finalized toast and Frozen Accounts count updated to 1](/images/tokens/freeze-confirmed.png) To lift the freeze, find the account in the frozen list and click **Unfreeze**. Confirm the on-chain submission. ![Unfreeze account confirmation dialog](/images/tokens/unfreeze-confirm.png) The "Unfreeze transaction finalized." toast appears and the Frozen Accounts count returns to 0. ![Compliance tab showing Unfreeze transaction finalized toast and Frozen Accounts count back to 0](/images/tokens/unfreeze-confirmed.png) **Freeze:** ```bash title="Terminal" curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/freeze \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "accountAddress": "3xYZa...2aBc", "reason": "Suspicious activity investigation" }' ``` `accountAddress` accepts a holder wallet or its token account — SDP derives the associated token account when needed. **Unfreeze:** ```typescript title="unfreeze-account.ts" await fetch("https://api.solana.com/v1/issuance/tokens/tok_abc123/unfreeze", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Content-Type": "application/json" }, body: JSON.stringify({ accountAddress: "3xYZa...2aBc" }), }); ``` **List frozen accounts:** ```bash title="Terminal" curl https://api.solana.com/v1/issuance/tokens/tok_abc123/frozen \ -H "Authorization: Bearer sk_test_..." ``` Halt all minting and transfer activity globally. Requires the `pausable` extension on the token. On the token detail page, go to the **Compliance** tab and select **Pause Controls**. Click **Pause token**. ![Pause Controls tab showing Pause token and Unpause token buttons](/images/tokens/pause-controls.png) Confirm the on-chain submission in the confirmation prompt. ![Pause token confirmation dialog](/images/tokens/pause-confirm.png) The prompt uses the only matching authority wallet automatically. If several custody records control the same authority, select the signer before confirming. The Unpause button uses the same selection rules. The "Pause transaction finalized." toast appears and an orange **Token is paused** banner displays across the page. All supply and transfer operations are now blocked. ![Compliance tab showing Token is paused banner and Pause transaction finalized toast](/images/tokens/pause-confirmed.png) To resume, click **Unpause token** and confirm. ![Unpause token confirmation dialog](/images/tokens/unpause-confirm.png) ```bash title="Terminal" # Pause curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/pause \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{"signingCustodyWalletId":"cwlt_example"}' # Unpause curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/unpause \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{"signingCustodyWalletId":"cwlt_example"}' ``` `signingCustodyWalletId` is optional when exactly one in-scope custody record matches the current pause authority; send `{}` to use that resolution. Multiple matches without a selector return `409`. An explicit choice must control the live authority and pass authorization and runtime checks. Reusing an idempotency key with a different wallet returns `409`. While paused, all transfer-related and supply-management operations are rejected until unpause. Force-transfer tokens from a holder to a recovery address without the holder's signature. For court orders and regulatory enforcement. Requires `tokens:admin`. In the **Compliance** tab, click **Seize tokens**. Provide the source, destination, amount, and a memo (strongly recommended for audit records). Confirm to submit. ```bash title="Terminal" curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/seize \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: seize-001" \ -d '{ "seize": { "source": "3xYZa...2aBc", "destination": "9aBCd...4eEf", "amount": "250", "memo": "Court order #12345" } }' ``` ## Compliance response workflow 1. **Screen** the address via the Compliance API 2. **Freeze** the affected account while investigating 3. Based on findings — **Unfreeze** if cleared, **Seize** tokens to a recovery address, or **Force-burn** (see [Mint and Burn](/docs/tokens/mint-and-burn)) 4. Record all actions with `memo` fields for the audit trail --- ### Manage Token Settings Source: https://platform.solana.com/docs/tokens/manage-token-settings > Update token authorities and operational settings after deployment. After a token is deployed, you can rotate its authorities and adjust operational settings. Authority management is separate from compliance actions — for freeze, pause, and seize flows see [Freeze and Compliance](/docs/tokens/freeze-and-compliance). For on-chain metadata edits, `PATCH /v1/issuance/tokens/{tokenId}` accepts optional `signingCustodyWalletId`. It selects the exact custody record controlling the current metadata authority for this request, not a new deployment wallet or authority. Omission requires exactly one matching in-scope record; multiple matches return `409`. Database-only edits need no signer. In the dashboard, one matching wallet is automatic; when there are several, choose one in the metadata confirmation or beside the Asset Profile Save button. Historical `data.token.signingCustodyWalletId` does not change. To select that signer after an external authority rotation, request `GET /v1/issuance/tokens/{tokenId}?includeMetadataAuthority=true` and use `data.metadataAuthority`. For tokens with a mint this is read live from the chain, unlike the stored snapshot in `data.token.metadataAuthority`; tokens without a mint return their stored metadata authority or mint authority without RPC. RPC failure returns `502`, never the stored value as a fallback. The dashboard keeps metadata and allowlist lookup errors separate, and the update rechecks authority before signing. This read changes neither the stored token nor its deployment wallet. ## Prerequisites - A deployed token with status `active` - `tokens:admin` permission on the API key ## How it works Review the token's current authorities and configuration before making changes. Open the token detail page. The **Overview** tab shows the token address, mint authority, current supply, and core metadata. ![Token detail Overview tab showing token address, mint authority, supply, template, and decimals](/images/tokens/token-settings-overview.png) The **Permissions** tab lists every authority and which wallet currently holds it. ![Permissions tab showing Mint Authority, Freeze Authority, Metadata Authority, and Permanent Delegate Authority with Edit buttons](/images/tokens/token-settings-permissions.png) The **Extensions** tab shows the template and each operational flag — Token Access Control (allowlist) status, mintable, freezable, and default account state. ![Extensions tab showing template, allowlist, mintable, freezable, and default account state settings](/images/tokens/token-settings-extensions.png) Fetch the current token state: ```bash title="Terminal" curl https://api.solana.com/v1/issuance/tokens/tok_abc123 \ -H "Authorization: Bearer sk_test_..." ``` ```typescript title="get-token-settings.ts" const { data } = await res.json(); // data.token.authorities — { mintAuthority, freezeAuthority, permanentDelegate } // data.token.extensions — active extensions on the token // data.token.status — "active" | "paused" ``` Transfer a token authority to a different wallet. Use this to move signing responsibilities or to hand off control to an external key holder. On the token detail page, go to **Permissions**. Click **Edit** next to the authority you want to change. Select another controlled wallet or choose **Use custom address**, then click **Save authority**. ![Mint Authority modal showing current authority wallet, new authority dropdown with controlled wallets, and Save authority button](/images/tokens/token-authority-edit.png) > Once an authority is rotated to an external wallet, SDP can no longer sign operations that require it. Rotate the authority back to a controlled wallet to resume using SDP's deploy, mint, and compliance actions. SDP signs the authority rotation: ```bash title="Terminal" curl -X POST https://api.solana.com/v1/issuance/tokens/tok_abc123/authority \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: rotate-001" \ -d '{ "authority": { "type": "mintAuthority", "newAuthority": "9aBCd...4eEf" } }' ``` `type` can be `mintAuthority`, `freezeAuthority`, or `permanentDelegate`. Permanently remove an authority from the token. This action is irreversible — the token will no longer be mintable, freezable, or delegatable depending on which authority is revoked. In **Settings → Authorities**, click **Revoke** next to the authority and confirm. A warning dialog explains the consequences before you proceed. Set `newAuthority` to `null` to revoke: ```typescript title="revoke-authority.ts" body: JSON.stringify({ authority: { type: "mintAuthority", newAuthority: null, }, }), ``` After revoking the mint authority, no new tokens can ever be minted. Revocation is permanent and cannot be undone. --- ## Wallet operations ### Overview Source: https://platform.solana.com/docs/wallet-operations > Configure custody-wallet controls, operation permissions, and balances. This section covers the controls and audit trail attached to a custody wallet: **wallet policies** that gate every wallet operation, and balance visibility. A wallet policy applies uniformly across payment, ramp, issuance, signing, and on-chain program activity, whichever product flow initiates the request. ## What's in this section - [Wallet policies](/docs/wallet-operations/policies) — configure default decisions, destination controls, amount limits, operation permissions, approval requirements, and audit visibility. - [Wallet balances](/docs/wallet-operations/balances) — retrieve token balances and optional USD valuations for a custody wallet. ## API surface The current wallet-policy and balance endpoints remain under `/v1/payments/wallets` because they were introduced with the Payments API. Their behavior is wallet-wide: policy operation families include transfer, payment, ramp, issuance, and program. ## Related - [Set up wallets](/docs/guides/setup-wallets) — provision the wallet before configuring controls. - [Payments](/docs/payments) — payment and ramp flows that can use a policy-controlled wallet. - [Manage API keys](/docs/developing-with-sdp/manage-api-keys) — an API key policy narrows or routes to approval; it never expands what the wallet policy allows. --- ### Wallet policies Source: https://platform.solana.com/docs/wallet-operations/policies > Wallet controls, operation permissions, and policy audit visibility. A **wallet policy** constrains how a custody wallet can move funds and execute supported operations. SDP starts from default allow, then applies the active wallet control profile and any policy fields you configure. Policies are managed via `GET` and `PUT` on `/v1/payments/wallets/{walletId}/policies` and enforced before each wallet operation executes. Use policies for treasury wallets, automated payout wallets, or any custody wallet where unbounded outbound flow would be a liability. ## Reading a wallet's policies ```bash title="Terminal" curl https://api.solana.com/v1/payments/wallets/wal_abc123/policies \ -H "Authorization: Bearer sk_test_..." ``` ```typescript title="get-wallet-policies.ts" const response = await fetch( "https://api.solana.com/v1/payments/wallets/wal_abc123/policies", { headers: { Authorization: "Bearer sk_test_..." } } ); const { data } = await response.json(); // data.policy.destinationAllowlist: string[] // data.policy.maxTransferAmount?: string // data.policy.maxDailyAmount?: string // data.policy.controlProfile?.providerMappingStatus // data.policy.audit?.recentEvaluations ``` The payload is wrapped in the standard `data` / `meta` envelope, with the policy under a `policy` key: ```json title="response.json" { "data": { "policy": { "walletId": "wal_abc123", "destinationAllowlist": ["7xKXz...9fGh", "5aBCd...2eFg"], "maxTransferAmount": "100.00", "maxDailyAmount": "1000.00", "defaultAction": "allow", "controlProfile": { "id": "wcp_abc123", "status": "active", "activeRevisionId": "wcpr_abc123", "revisionId": "wcpr_abc123", "revisionNumber": 3, "defaultAction": "allow", "rules": [ { "id": "approval-required", "kind": "approval", "families": ["payment", "ramp"], "action": "approval_required", "name": "Approval checks" } ], "providerMappingStatus": "not_applicable", "createdAt": "2026-05-14T08:00:00Z", "updatedAt": "2026-05-14T10:15:00Z", "activatedAt": "2026-05-14T10:15:00Z" }, "audit": { "recentEvaluations": [ { "walletOperationId": "wop_abc123", "policyEvaluationId": "peval_abc123", "operationFamily": "payment", "operationType": "payment_transfer_execute", "asset": "USDC", "amount": "100.00", "destination": "7xKXz...9fGh", "status": "pending_approval", "decision": "approval_required", "reasonCode": "wallet_policy_match", "reason": "Payment matched approval policy.", "requiresApproval": true, "approvalRequestId": "appr_abc123", "operationCreatedAt": "2026-05-14T10:20:00Z", "operationUpdatedAt": "2026-05-14T10:20:00Z", "evaluatedAt": "2026-05-14T10:20:00Z" } ] }, "createdAt": "2026-05-14T08:00:00Z", "updatedAt": "2026-05-14T10:15:00Z" } }, "meta": { "requestId": "req_...", "timestamp": "2026-05-18T00:00:00.000Z" } } ``` Amount fields are **UI-unit decimal strings** (`"100.00"` means 100 tokens, regardless of the mint's decimals — matching the transfer-request convention). The configured value is a **single threshold**, but enforcement is **per token**: `maxDailyAmount` sums each token's outbound transfers separately within the day window, so a wallet with `maxDailyAmount: "1000"` can send up to 1000 USDC *and* 1000 SOL in the same UTC day — there is no aggregate cap across mints. If you need different per-token thresholds (e.g., 10,000 USDC daily but 50 SOL daily), partition into separate wallets. ## Default allow and active controls Wallet policy evaluation starts from default allow. If no wallet control profile exists, supported wallet operations continue to run unless another product-specific check rejects them. When you activate a control profile, SDP evaluates its rules first and returns the resulting `decision` as one of `allow`, `deny`, `approval_required`, `provider_approval_required`, `review`, or `not_evaluated`. The `controlProfile` object shows the active immutable revision. Treat `revisionId` and `revisionNumber` as audit references: a policy evaluation records which revision was active when the wallet operation was checked. ### Dashboard actions The dashboard authoring flow exposes three actions: **Allow**, **Deny**, and **Require approval**. Choose **Require approval** when an operator must decide whether an operation can continue; it holds the operation and creates an **Approval Request**. `review` remains a possible API evaluation result for legacy or safety paths, such as an unrecognized policy rule. It is visible in audit records, but it is not a dashboard authoring option or a separate human-review workflow. Do not use it to model operator approval; use `approval_required` instead. ## Rule kinds A control profile revision is an ordered list of rules. Each rule either abstains or contributes a decision; the strictest matched decision wins and the revision's `defaultAction` applies when nothing matches. For every kind except `velocity`, `action` is the decision a match produces. | Kind | Matches | Fields | | --- | --- | --- | | `operation_family` | Operations in the named families. | `family`, `families` | | `operation_type` | Operations of the named types. | `operationType`, `operationTypes` | | `asset` | Operations moving one of the named mints. | `asset`, `assets` | | `destination` | Destinations on the allowlist, or off the blocklist. | `allowlist`, `blocklist`, `destination`, `destinations` | | `amount` | A single operation's amount inside `[min, max]` for the named mints. Outside the bounds the rule denies. | `min`, `max`, `asset`, `assets` | | `velocity` | The rolling sum of prior operations plus this one exceeds `max` for the named mints. | `scope`, `window`, `max`, `asset`, `assets`, `operationTypes` | | `approval` | Selectors match; holds the operation for an approval group. | `families`, `operationTypes`, `assets`, `approvalGroupId` | | `always` | Every operation. | none | ### Velocity limits A `velocity` rule caps volume over a rolling window instead of per operation: ```json title="velocity-rule.json" { "id": "daily-usdc-transfers", "kind": "velocity", "scope": "organization", "window": "P1D", "max": "100000", "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "operationTypes": ["payment_transfer_execute"], "action": "approval_required" } ``` - `scope` chooses whose history counts: `wallet` (default), `organization`, or `api_key`. - `window` is an ISO 8601 duration using days, hours and minutes (`PT1H`, `P1D`, `P1DT12H`). Weeks, months, years and seconds are rejected. - `max` is a UI-unit decimal string in the asset's units. Like `amount`, the rule must name at least one mint. - The window sums every decided, non-failed, non-canceled wallet operation for the scope and mint, optionally narrowed by `operationTypes`. Operations still awaiting their own policy decision do not count, so concurrent requests never block each other. - `action` is the decision **on breach** (default `deny`). Within the limit the rule abstains rather than allowing, so a velocity rule never overrides a stricter rule beside it. Set `action: "approval_required"` to route outliers into the approval flow instead of refusing them. - A `Dry-Run: true` request reports the velocity verdict against current history without recording anything. ## Operation permissions Wallet policies can match operations at two levels: - **Operation family** rules apply to every evaluated operation in a broad product area. - **Operation type** rules match one exact operation identifier and take precedence when you need a narrower decision. These are wallet-policy selectors, not API-key permissions. API-key permissions such as `payments:write` and `tokens:write` determine whether a caller may invoke an endpoint; operation permissions determine what the selected wallet may do after the request is authorized. The wallet policy is the baseline: an API key policy can narrow what a key may do, or route an operation into approval, but it can never expand past what the wallet policy allows. See [Manage API keys](/docs/developing-with-sdp/manage-api-keys) for caller permissions. ### Operation families | Family | Description | Current evaluated operations | | --- | --- | --- | | `payment` | Outbound single and batch payments. | Single and batch transfer execution | | `ramp` | Fiat-to-crypto and crypto-to-fiat ramp activity. | On-ramp and off-ramp quote creation | | `issuance` | Token lifecycle actions signed by the wallet. | Mint, burn, seize, and authority-update execution | | `program` | On-chain program interactions signed by the wallet. | No current public operation type | | `transfer` | Generic direct-transfer family reserved for operations outside the payment workflow. | No current public operation type | Family values with no current public operation type are valid policy values, but they do not match a public SDP flow until an operation is emitted in that family. ### Exact operation types | Operation type | Family | What it controls | | --- | --- | --- | | `payment_transfer_execute` | `payment` | Executes one outbound payment transfer from the wallet. | | `payment_transfer_batch_execute` | `payment` | Executes an outbound batch and evaluates the aggregate amount before submission. | | `ramp_onramp_quote` | `ramp` | Creates an on-ramp quote that delivers crypto to the destination wallet. | | `ramp_offramp_quote` | `ramp` | Creates an off-ramp quote that draws crypto from the source wallet. | | `issuance_mint_execute` | `issuance` | Executes a token mint using the configured signing wallet. | | `issuance_burn_execute` | `issuance` | Executes a token burn using the configured signing wallet. | | `issuance_force_burn_execute` | `issuance` | Force-burns tokens from a holder account. | | `issuance_seize_execute` | `issuance` | Seizes tokens into the issuer-controlled destination. | | `issuance_update_authority_execute` | `issuance` | Changes a token authority using the current authority wallet. | | `issuance_freeze_execute` | `issuance` | Freezes a holder token account using the freeze authority wallet. | | `issuance_unfreeze_execute` | `issuance` | Thaws a frozen holder token account using the freeze authority wallet. | | `issuance_pause_execute` | `issuance` | Pauses all token transfers using the pause authority wallet. | | `issuance_unpause_execute` | `issuance` | Resumes token transfers using the pause authority wallet. | | `issuance_metadata_update_execute` | `issuance` | Rewrites on-chain token metadata using the metadata authority wallet. | | `recurring_payment_create` | `payment` | Creates a recurring payment schedule. | | `recurring_payment_update` | `payment` | Updates a recurring payment schedule. | | `recurring_payment_collection` | `payment` | Collects a due recurring payment. | Operation-type matching is exact and case-sensitive. The API accepts only the supported identifiers above, so a rule cannot be activated for an operation SDP never emits. ## Policy evaluation and audit records Each supported action against the wallet is recorded as a **Wallet Operation** — the attempt, whatever the outcome — before evaluation runs, and its **Policy Evaluation** — the verdict, plus a point-in-time snapshot of the operation and the policy that decided it — is recorded once SDP decides what should happen. `audit.recentEvaluations` exposes the most recent decisions for the wallet so support and customer operators can review: - what operation was evaluated (`walletOperationId`, `operationFamily`, `operationType`) - what decision SDP made (`decision`, `reasonCode`, `reason`, `requiresApproval`) - whether an approval request was created (`approvalRequestId`) - when the operation was created, updated, and evaluated Use these records to explain allow, deny, approval-required, and review outcomes. They are an operational audit summary, not the full raw provider payload. ## Provider mapping status `providerMappingStatus` describes whether an SDP policy revision has also been mapped into a custody or ramp provider's native policy system. Current payment wallet controls are SDP-enforced first. A value of `not_applicable` means there is no provider-native mapping for that revision. `pending`, `partial`, or `failed` mean SDP still evaluates the operation, but you should not assume the same rule has been fully synced to the provider. ## Destination allowlist This is a wallet-policy control on outbound destinations — distinct from a token's **Token Access Control** allowlist, which gates who may hold the token itself. It is a list of on-chain destination addresses (32-44 character base-58 Solana pubkeys). When set, `POST /v1/payments/transfers` rejects any destination not in the list. - Maximum entries: **500 addresses per wallet**. - Each entry is a single on-chain address — no patterns, ranges, or address books. - Empty allowlist (`[]`) means no destination restriction. To enable enforcement, populate at least one entry. The allowlist exists to bound risk on automated wallets (a hot wallet that should only ever pay a known set of counterparties) and to enforce treasury controls (only the corporate cold-storage address can drain the operating wallet). ## Transfer limits A **transfer limit** always targets one **Policy Asset** — identified by mint, with native SOL under its well-known mint — never a blanket cap across assets. Two limits, both optional: - **`maxTransferAmount`** — per-transfer cap. Any single `POST /v1/payments/transfers` exceeding this amount is rejected. Compared against the request's `amount` directly, so it is naturally per-token. - **`maxDailyAmount`** — UTC-calendar-day cap, **enforced per token**. The day window resets at 00:00 UTC; the projected total is the sum of that wallet's outbound transfers for the **same token** in the current day (`pending`/`processing`/`confirmed`/`finalized`) plus the new request's `amount`. Different tokens are summed independently — see the per-token note under [Reading a wallet's policies](#reading-a-wallets-policies). Set both for defense in depth: a per-transaction cap that catches obvious mistakes, plus a daily cap that bounds blast radius if many small transfers are submitted in a coordinated attack. ## Updating policies `PUT /v1/payments/wallets/{walletId}/policies` has **full-replace** semantics — the request body becomes the new policy state in its entirety. To remove the allowlist, send `destinationAllowlist: []`. To drop a transfer limit, omit the field on the next PUT. ```bash title="Terminal" curl -X PUT https://api.solana.com/v1/payments/wallets/wal_abc123/policies \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "destinationAllowlist": [ "7xKXz...9fGh", "5aBCd...2eFg" ], "maxTransferAmount": "100.00", "maxDailyAmount": "1000.00" }' ``` ```typescript title="update-wallet-policies.ts" await fetch( "https://api.solana.com/v1/payments/wallets/wal_abc123/policies", { method: "PUT", headers: { "Authorization": "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify({ destinationAllowlist: ["7xKXz...9fGh", "5aBCd...2eFg"], maxTransferAmount: "100.00", maxDailyAmount: "1000.00", }), } ); ``` Because PUT is full-replace, treat policy edits like ordinary form submissions: read the current state, present it for editing, and PUT the complete updated object. Don't construct partial patches. ## Interaction with compliance Wallet policies enforce **structural** constraints (where funds can go, how much, how fast). They do not replace compliance screening. If your organization has compliance screening enabled, screening happens on the destination address as a separate check, in addition to the allowlist match. A transfer can be rejected by either layer; both must pass to reach the network. See the [Compliance API reference](/docs/reference/api/compliance) for address-screening flows that complement policies. ## Related - [Payouts and disbursements](/docs/payments/send-payouts) — running an outbound batch from a policy-constrained wallet. - [Basic payment](/docs/payments/send-basic-payment) — the underlying transfer endpoint subject to policy enforcement. - [Wallet balances](/docs/wallet-operations/balances) — current balance, useful to size transfer limits. --- ### 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 the wallet's native **SOL** balance plus every **SPL token** balance held by a custody wallet — raw amount, human-readable amount, decimals, and optional USD valuation when a price source is configured. SOL is always prepended as the first entry and is represented with `token: "SOL"` and `mint: "So11111111111111111111111111111111111111112"`. ## Request ```bash title="Terminal" curl https://api.solana.com/v1/payments/wallets/wal_abc123/balances \ -H "Authorization: Bearer sk_test_..." ``` ```typescript title="get-wallet-balances.ts" const response = await fetch( "https://api.solana.com/v1/payments/wallets/wal_abc123/balances", { headers: { Authorization: "Bearer sk_test_..." } } ); const { data } = await response.json(); // data.walletBalances.balances: TokenBalance[] ``` ```java title="GetWalletBalances.java" HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.solana.com/v1/payments/wallets/wal_abc123/balances")) .header("Authorization", "Bearer sk_test_...") .GET() .build(); ``` ## Response shape The payload is wrapped in the standard `data` / `meta` envelope, with the balances under a `walletBalances` key: ```json title="response.json" { "data": { "walletBalances": { "walletId": "wal_abc123", "address": "3xYZa...2aBc", "balances": [ { "token": "SOL", "mint": "So11111111111111111111111111111111111111112", "amount": "2500000000", "uiAmount": "2.5", "decimals": 9 }, { "token": "USDC", "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "amount": "1500000000", "uiAmount": "1500.00", "decimals": 6, "usdPrice": 1.0, "usdValue": 1500.0 } ] } }, "meta": { "requestId": "req_...", "timestamp": "2026-05-18T00:00:00.000Z" } } ``` Per-balance fields: | Field | Notes | | --- | --- | | `token` | Token symbol if known, otherwise the on-chain mint. | | `mint` | On-chain mint address. | | `amount` | Raw amount in the smallest unit (string to preserve precision). | | `uiAmount` | Human-readable decimal string — `amount / 10^decimals`. | | `decimals` | Token's decimal count. | | `usdPrice` | Per-token USD price; omitted when no pricing data is available. | | `usdValue` | Total USD value (= `usdPrice * uiAmount`); omitted when pricing is unavailable. | | `confidential` | Reserved for confidential transfer balances; absent for standard balances. | ## Use cases - **Account dashboards** — display the contents of a custody wallet to its operator. - **Pre-transfer balance checks** — validate that a wallet has enough of a specific token before kicking off a payout batch. - **Treasury accounting** — periodic snapshots into an internal ledger. ## Pricing freshness When USD valuation is populated, the price comes from SDP's configured price source and reflects the most recent quote available. The cadence and source depend on deployment configuration — do not rely on `usdValue` for downstream balance-sheet decisions without confirming the price source's freshness with your operations team. When in doubt, treat `amount` / `uiAmount` as authoritative and convert with your own price feed. If no price source is configured, the `usdPrice` and `usdValue` fields are absent rather than zero — the omission signals "unknown," not "$0.00," so downstream consumers can decide how to handle the gap. ## Comparison to on-chain RPC You can also enumerate SPL token balances by calling `getTokenAccountsByOwner` against a Solana RPC and decoding the returned token-account data. Reasons to use SDP's `balances` endpoint instead: - **Token resolution** — SDP resolves the mint to a symbol when it knows one; raw RPC returns mints only. - **USD pricing** — RPC has no concept of price. - **No RPC dependency in your stack** — one fewer external integration when you are already calling SDP. Reasons to skip SDP and go to RPC directly: - **Cluster-of-truth** — you want the wallet's state at a specific slot for an audit. - **Confidential transfer balances** — full extension support is RPC-side. - **Latency-sensitive paths** — RPC is one hop closer to the cluster. ## Related - [Wallet policies](/docs/wallet-operations/policies) — set transfer caps based on balance bounds. - [Payouts and disbursements](/docs/payments/send-payouts) — balance-check before running a batch. - [Set Up Wallets](/docs/guides/setup-wallets) — provision the wallets whose balances this endpoint reports. --- ## Payments ### Overview Source: https://platform.solana.com/docs/payments > Build payments on Solana with SDP — concepts, send, accept, and ramps. SDP provides a Solana-native payments stack: stablecoin transfers, fiat on-ramps and off-ramps, and reconciliation primitives. This section mirrors the structure of the [public Solana payments documentation](https://solana.com/docs/payments) and maps each workflow to the SDP APIs you call to ship it. If you have not yet provisioned a custody wallet or token, start with [Set Up Wallets](/docs/guides/setup-wallets) and [Tokenize an Asset](/docs/tokens/tokenize-an-asset). ## What's in this section - **[How payments work on SDP](/docs/payments/concepts)** — wallets, tokens, signing modes, fees, and the transfer data model. - **Send payments** — outbound flows. - [Basic payment](/docs/payments/send-basic-payment) — a single transfer in either signing mode. - [Payment with memo](/docs/payments/send-payment-with-memo) — attach order or customer references to a transfer. - [Payouts and disbursements](/docs/payments/send-payouts) — batched outbound transfers and reconciliation. - **Accept payments** — inbound flows. - [Accept overview](/docs/payments/accept-overview) — receiving-address convention and integration options. - [Verifying a payment](/docs/payments/accept-verification) — transfer-status semantics and finality. - [Indexing and reconciliation](/docs/payments/accept-indexing) — listing inbound transfers for your ledger. - **Ramps** — fiat on-ramps and off-ramps. - [Ramps overview](/docs/payments/ramps) — corridor discovery, estimates, and the Ramp Quote request shape. - [Ramp providers](/docs/payments/ramps-providers) — capability matrix and configuration for all seven ramp providers. Wallet-wide controls and balances are documented in [Wallet operations](/docs/wallet-operations). ## Underlying API Every page in this section links to specific endpoints in the auto-generated [Payments API reference](/docs/reference/api/payments). The Payments API surface is namespaced under `/v1/payments` and groups into four areas: `transfers`, `ramps`, `wallets/.../balances`, and `wallets/.../policies`. ## Related - [End-to-end payment flow](/docs/tutorials/end-to-end-payment-flow) — high-level tutorial covering quote through settlement. --- ### How payments work on SDP Source: https://platform.solana.com/docs/payments/concepts > Wallets, tokens, server-side signing, fees, and the data model that backs SDP payments. SDP payments move value between Solana accounts: a custody wallet sends tokens to a destination address and SDP records the resulting transfer. This page introduces the building blocks — wallets, tokens, execution, fees, and the data model — that the rest of the section assumes. ## Custody wallets Every SDP payment is anchored to a **custody wallet** — an SDP-managed Solana wallet, scoped to an organization and (optionally) a project. The wallet's key is held and signed for by a custody provider; SDP presents a stable Solana address and routes signing to whichever provider holds the key. Wallets sign outbound transfers on your behalf and contribute to balances and policies for the addresses you control. You provision custody wallets up front (see [Set Up Wallets](/docs/guides/setup-wallets)). Transfer requests use the exact SDP Wallet `id` (`cwlt_…`) as `sourceCustodyWalletId`; responses expose it as `custodyWalletId` and retain the provider's identifier separately as `providerWalletId` evidence. The **destination** remains the recipient's on-chain Solana address. Ramp quote endpoints use the same public shape for every provider: `destinationCustodyWalletId` (onramp) or `sourceCustodyWalletId` (offramp) identifies a custody wallet you control by its internal custody wallet `id` (the `id` returned by `GET /v1/wallets`). Provider-specific identifiers such as customer and payout-account IDs are resolved server-side from the **Counterparty**'s Provider Requirements — see [Ramps](/docs/payments/ramps). ## Tokens and token accounts Payments transfer **SPL tokens** — stablecoins, tokenized assets, or any mint your project supports. The `token` field on `POST /v1/payments/transfers` requires the on-chain **mint address** for SPL tokens; the literal string `SOL` is accepted as the native-SOL shorthand. Symbols like `USDC` are not resolved at request time. The `token` filter on `GET /v1/payments/transfers` does an **exact match** against the `Transfer.token` value as stored — API-created transfers store the mint you supplied (or `SOL`), while indexed/observed transfers run mint-to-symbol resolution and may store a resolved symbol (e.g. `USDC`). Filter using the same label you see on the response, or query without `token` and filter client-side. **Balance responses always attempt symbol resolution** and return the symbol whenever the mint is known — so a wallet holding USDC reports `USDC` rather than the mint string. **Amounts on the wire are UI-unit decimal strings** — `"100.00"` means 100 tokens, regardless of the mint's decimal count. This applies to issuance operations (mint, burn, seize, force-burn) as well as payments transfers. SDP handles the smallest-unit conversion when it builds the on-chain transaction. The one exception is wallet-balance responses, which return both a raw `amount` (smallest unit) **and** a `uiAmount` (decimal string) so balance consumers can pick whichever fits. ## Transfer execution Outbound wallet transfers are server-executed through `POST /v1/payments/transfers`. SDP builds the transaction, signs with the source wallet's custody key, submits it on the configured Solana cluster, and records the resulting transfer lifecycle. The rest of this section assumes transfers are submitted by SDP from a custody wallet you control. ## Fees and sponsorship Solana fees are paid in SOL by the transaction's fee payer. On wallet transfers, the source custody wallet is the fee payer by default; SDP can be configured to sponsor fees so end-user wallets do not need a SOL balance. ## The transfer data model Every successful or attempted transfer creates a `Transfer` record. The fields that matter for most flows: | Field | Meaning | | --- | --- | | `id` | SDP-internal transfer identifier (use this in API URLs). | | `status` | Wallet transfers: `processing`, `confirmed`, `finalized`, `failed`. Ramp transfers: `pending`, `awaiting_payment`, `settling`, `completed`, `canceled`, `expired`, `failed`. See [Verifying a payment](/docs/payments/accept-verification). | | `direction` | `inbound` (someone paid you) or `outbound` (you paid someone). | | `source`, `destination` | On-chain addresses. | | `token`, `amount` | What moved and how much. `amount` is a UI-unit decimal string (e.g. `"100.00"`). | | `memo` | Optional UTF-8 string, up to 256 chars. See [Payment with memo](/docs/payments/send-payment-with-memo). | | `signature` | Solana transaction signature once confirmed. **Unique across SDP** — the natural dedup key. | | `slot`, `blockTime`, `fee` | On-chain settlement details, populated as the transaction lands. | | `error` | Set if the transfer failed; the rest of the record explains what reached the network. | | `risk` | Optional risk-score metadata, when a risk provider is configured. | Outbound wallet transfers start at `processing` and move to `confirmed` → `finalized`. Inbound transfers discovered on-chain surface directly at `confirmed`. Ramp transfers can start at `pending` while provider-side steps are still underway. See [Verifying a payment](/docs/payments/accept-verification#status-lifecycle) for the full state table. ## What SDP does and does not give you SDP exposes everything you need to **send**, **track**, and **reconcile** payments at the transfer level: status polling, filtered lists, wallet-level balances, exact-wallet `Idempotency-Key` replay, and a unique on-chain `signature` once populated (see [Basic payment → Deduplication](/docs/payments/send-basic-payment#deduplication)). It does **not** ship higher-level commerce primitives such as a checkout session or invoice object. The [Accept payments](/docs/payments/accept-overview) section shows how to build those abstractions on top of the transfer model. ## Related - [Payments API reference](/docs/reference/api/payments) — auto-generated endpoint reference. - [End-to-end payment flow](/docs/tutorials/end-to-end-payment-flow) — a step-by-step tutorial through the same lifecycle from a different angle. --- ## Payments — Send Payments ### Basic payment Source: https://platform.solana.com/docs/payments/send-basic-payment > Send a single token transfer with SDP. SDP handles token transfers through its payments API. A basic payment moves tokens **from a custody wallet you control to any Solana wallet address**. The `sourceCustodyWalletId` field is the exact SDP Wallet `id` (`cwlt_…`, from `GET /v1/wallets`), resolved server-side from the caller's custody-wallet list; `destination` is the recipient's on-chain **wallet (owner) address** — for SPL transfers, SDP derives the associated token account (ATA) from this address and creates it if needed, so pass the wallet pubkey, **not** a token-account address. Transfers are available through the Payments dashboard and the API. ## Prerequisites - A deployed token with minted supply - A custody wallet to use as `sourceCustodyWalletId`, with a custody signer configured so SDP can sign and submit. - An API key with both `payments:write` and `wallets:read` permissions, where the key's `payments:write` scope includes the source wallet — `POST /v1/payments/transfers` is gated by that pair at the route layer, and the source wallet is then re-checked against the key's `payments:write` wallet scope. ## How it works Specify the source, destination, token, and amount. Go to **Payments** in the sidebar. The overview shows your total SDP balance, recent transactions, and **Send** / **Receive** actions. ![Payments overview showing balance, Send and Receive buttons, and recent transactions](/images/tokens/transfer-confirmed.png) Click **Send** and select **Wallet transfer** to send from an SDP wallet to a Solana address. ![Send page showing Wallet transfer and Off-ramp options](/images/tokens/transfer-send-options.png) On the **Enter transfer details** form, select a source wallet from the dropdown. ![Transfer details form with source wallet dropdown open](/images/tokens/transfer-form-source.png) Fill in: - **Source** — a wallet you control in SDP - **Amount** — human-readable units (e.g., `0.5`) - **Asset** — select from your deployed tokens - **Destination address** — any valid Solana wallet address - **Memo** — optional note recorded onchain ![Transfer details form with all fields filled in](/images/tokens/transfer-form-filled.png) 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", // onchain mint address amount: "100.00", // human-readable units memo: "Payment for invoice #1234", }; ``` `token` is the mint address returned by the deploy step, not the SDP token ID. `destination` is the wallet pubkey — SDP derives the associated token account from it. Submit the transfer. SDP signs with the source custody wallet and submits the transaction. Click **Review** to see the transfer summary, then **Confirm** to submit. The transfer status updates to `Confirmed` once the transaction is finalized. ```bash title="Terminal" curl -X POST https://api.solana.com/v1/payments/transfers \ -H "Authorization: Bearer sk_test_..." \ -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 title="execute-transfer.ts" const response = await fetch( "https://api.solana.com/v1/payments/transfers", { method: "POST", headers: { "Authorization": "Bearer sk_test_...", "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.id, data.transfer.status — transfer record under the `transfer` envelope key ``` ```java title="ExecuteTransfer.java" HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.solana.com/v1/payments/transfers")) .header("Authorization", "Bearer sk_test_...") .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(); ``` Confirm the transfer completed and balances updated. The transfer appears in **Payments** history with a `confirmed` status and an onchain transaction signature. ![Payments overview showing confirmed inbound transaction](/images/tokens/transfer-confirmed.png) Click the record to see full details including the Solana Explorer link. ```typescript title="verify-transfer.ts" // data.transfer.status — "confirmed" // data.transfer.signature — onchain tx signature ``` Poll status for async confirmations: ```bash title="Terminal" curl https://api.solana.com/v1/payments/transfers/txn_abc123 \ -H "Authorization: Bearer sk_test_..." ``` ## Deduplication Send one stable `Idempotency-Key` per logical payment and reuse it for network retries. An identical request for the same exact `sourceCustodyWalletId` returns the original transfer; reusing the key with a different payload or wallet returns `409`. A request without a key creates a new transfer intent. 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 the same pattern applied to batched outbound flows. ## Related - [Payment with memo](/docs/payments/send-payment-with-memo) — using the `memo` field to carry order references - [Payouts and disbursements](/docs/payments/send-payouts) — batched outbound flows - [Mint and Burn](/docs/tokens/mint-and-burn) — create or destroy token supply - [Allowlists](/docs/tokens/allowlists) — define approved destinations for allowlist-enabled issuance flows - [Freeze and Compliance](/docs/tokens/freeze-and-compliance) — halt transfers for compliance --- ### 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. The `memo` field on `POST /v1/payments/transfers` carries an arbitrary UTF-8 string (up to 256 characters) alongside the on-chain transfer. Memos are persisted on the SDP `Transfer` record and surface in list and read responses, so your indexer can map each transfer back to a row in your product database. ## When to use a memo Use a memo when you need a free-form correlation token that your backend already knows at request time: - **Order or invoice IDs** — `order_2026-05-14_4837` so the inbound transfer ties to a checkout row. - **Customer references** — internal customer or account identifiers for support and reconciliation. - **Run identifiers** — a payout-batch ID shared across many outbound transfers in the same disbursement. Memos are stored by SDP. They are **not** automatically attached as a Solana memo-program instruction; if you need an on-chain memo that block explorers display, that has to be handled at the transaction level (see the trade-offs section below). ## API usage ```bash title="Terminal" curl -X POST https://api.solana.com/v1/payments/transfers \ -H "Authorization: Bearer sk_test_..." \ -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 title="send-payment-with-memo.ts" const response = await fetch( "https://api.solana.com/v1/payments/transfers", { method: "POST", headers: { "Authorization": "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify({ sourceCustodyWalletId: "cwlt_123", destination: "7xKXz...9fGh", token: "9aBCd...4eEf", amount: "100.00", memo: "order_2026-05-14_4837", }), } ); const { data } = await response.json(); ``` ```java title="SendPaymentWithMemo.java" HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.solana.com/v1/payments/transfers")) .header("Authorization", "Bearer sk_test_...") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(""" { "sourceCustodyWalletId": "cwlt_123", "destination": "7xKXz...9fGh", "token": "9aBCd...4eEf", "amount": "100.00", "memo": "order_2026-05-14_4837" }""")) .build(); ``` The memo is preserved on the transfer record: ```json title="response.json" { "id": "xfr_...", "memo": "order_2026-05-14_4837", "status": "finalized", "signature": "5xR1...", ... } ``` Memo limits: `z.string().max(256)`. UTF-8 is accepted; trim or sanitize on the way in if your downstream systems are stricter. ## Trade-offs vs. Solana Pay reference accounts For accept flows, **Solana Pay reference accounts** are an alternative correlation mechanism: you generate a unique pubkey per order and include it in the on-chain transfer as a reference. The advantage is on-chain provenance — anyone can verify the link without trusting SDP's database — and the ability for the payer to construct the transfer themselves from a Solana Pay URL. SDP does not attach a Solana Pay reference on-chain or expose one on inbound transfer records today — use `memo` for SDP-side correlation, or read the reference directly from the transaction via Solana RPC. Use [Accept overview](/docs/payments/accept-overview) and [Indexing and reconciliation](/docs/payments/accept-indexing) for the inbound side. Rule of thumb: memos are simpler when **you** initiate the transfer (server-side payouts, internal moves), references are stronger when **a third party** initiates the transfer (customer checkout, donations). ## Indexing implications Memos are returned by `GET /v1/payments/transfers` and `GET /v1/payments/transfers/{id}`, so your reconciliation worker can filter by date / direction / token and then match each row to your order table by memo. See [Indexing and reconciliation](/docs/payments/accept-indexing) for query patterns. ## Related - [Basic payment](/docs/payments/send-basic-payment) — the underlying transfer endpoint. - [Payouts and disbursements](/docs/payments/send-payouts) — using memos to tag a batch. - [Accept overview](/docs/payments/accept-overview) — when to reach for reference accounts instead. --- ### Payouts and disbursements Source: https://platform.solana.com/docs/payments/send-payouts > Run batched outbound transfers, deduplicate retries, and reconcile a payout batch with SDP. A payout run is a batch of outbound transfers — marketplace seller payouts, payroll, affiliate disbursements, royalty distributions. Use `POST /v1/payments/transfer-batches` when recipients are registered counterparty accounts. For arbitrary Solana addresses, call `POST /v1/payments/transfers` once per recipient and reconcile the returned transfer IDs. ## 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 sk_test_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: payroll-2026-05-14" \ -d '{ "externalId": "payroll_2026-05-14", "sourceCustodyWalletId": "cwlt_123", "token": "9aBCd...4eEf", "recipients": [ { "externalId": "row_4837", "counterpartyId": "cp_4837", "counterpartyAccountId": "cpa_4837", "amount": "150.00" } ] }' ``` Reuse the same `Idempotency-Key` for retries of that exact request. Use `GET /v1/payments/transfer-batches/{batchId}` to inspect recipient and chunk outcomes. `externalId` is a correlation field, not an idempotency key. The rest of this page covers the per-recipient alternative for destinations that are not registered counterparty accounts. ## Designing a payout run A typical pattern: 1. **Resolve recipients** in your application — list (or stream) the rows to be paid, with destination address and amount per row. 2. **Pick a batch identifier** you control (`payout_2026-05-14_marketplace_sellers`). 3. **Iterate** — for each recipient, call `POST /v1/payments/transfers` with the destination, amount, token, and a memo that encodes the batch ID and a per-row identifier (`payout_2026-05-14_marketplace_sellers/row_4837`). 4. **Capture** the returned `Transfer.id` and `Transfer.status`. Persist the SDP transfer ID alongside the recipient row so you can re-poll status later. 5. **Reconcile** — once the batch is fully submitted, run `GET /v1/payments/transfers?direction=outbound&from=…&to=…` and confirm every row in your application 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. ## Dedup and retries Two layers protect you against double-spends if a worker dies mid-run: 1. **Idempotency keys** — generate one stable `Idempotency-Key` per recipient row and reuse it for retries. An identical request for the same exact source wallet returns the original transfer; reusing the key for a different request returns `409`. 2. **Application mapping** — persist the SDP transfer ID for each recipient row before moving to the next row. On restart, replay with the same idempotency key or skip rows that already have an associated `Transfer.id`. The on-chain `signature` is unique across SDP transfers once populated. Use it as a cross-indexer integrity check, not as the retry mechanism. Using a fresh idempotency key creates a new transfer intent, even if the payload is otherwise identical. ```bash title="Terminal" curl -X POST https://api.solana.com/v1/payments/transfers \ -H "Authorization: Bearer sk_test_..." \ -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 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 sk_test_...", "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 title="PayoutRecipient.java" HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.solana.com/v1/payments/transfers")) .header("Authorization", "Bearer sk_test_...") .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: ```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 sk_test_..." ``` ```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 sk_test_..." }, }); const { data, meta } = await res.json(); yield* data; if (!meta.hasMore) return; page += 1; } } ``` Filter the returned transfers on `memo.startsWith(batchId)` (or whatever convention you chose) to scope to the batch. Cross-reference each recipient row by SDP `Transfer.id`. ## Failure modes Per-transfer outcomes you should expect to handle: - **`failed`** — the transaction was built and submitted but the network rejected it (insufficient funds, frozen account, expired blockhash). The `error` field on the transfer record explains what reached the chain. The recipient was not paid; you can retry by submitting a new transfer. - **`pending` past the expected confirmation window** — usually a backend or RPC hiccup. Re-poll with `GET /v1/payments/transfers/{id}` rather than re-submitting; you only want a new transfer if `status` settles to `failed`. - **Mid-run worker crash** — resume with each row's original `Idempotency-Key`; persist the returned `Transfer.id` before advancing. - **Source wallet runs out of SOL for fees** — submissions start failing with fee-related errors. If SDP is configured to sponsor fees this won't happen; otherwise top up the source wallet's SOL balance before continuing. ## Related - [Basic payment](/docs/payments/send-basic-payment) — the underlying transfer endpoint. - [Payment with memo](/docs/payments/send-payment-with-memo) — the memo field used above to tag a batch. - [Indexing and reconciliation](/docs/payments/accept-indexing) — the same list endpoint, used inbound. - [Wallet policies](/docs/wallet-operations/policies) — destination allowlist and daily-limit guardrails for payout wallets. --- ## Payments — Accept Payments ### Accept payments Source: https://platform.solana.com/docs/payments/accept-overview > Receiving-address convention, integration options, and how to track inbound payments with SDP. SDP does not ship a hosted checkout primitive. To accept payments, you publish a Solana address you control and detect inbound transfers via the SDP transfer list — this page covers the pattern; [verification](/docs/payments/accept-verification) and [indexing](/docs/payments/accept-indexing) cover the details. ## Receiving-address convention The receiving address is a custody wallet you control. Two scopings to choose between: - **One wallet per order** *(recommended for accept flows)* — provision a fresh custody wallet (or sub-account) per order or per customer and use the destination address itself as the correlation key. The inbound transfer's `destination` field maps 1:1 to your order row, so you do not depend on memo or reference fields that SDP does not surface for inbound payments. - **One wallet per merchant scope** — for marketplaces, one custody wallet per seller; for treasury operations, one per ledger account. Reasonable when correlation isn't required (treasury, settlement) or when you accept a single fixed-amount payment per merchant. The address you publish is the on-chain `address` of the custody wallet. Fetch it with [`GET /v1/wallets/{walletId}`](/docs/reference/api/wallets); when you only need wallet metadata, add `?includeBalance=false` to avoid a balance RPC and pricing lookup. You publish that address; senders use it as the destination of a normal SPL token transfer. SDP indexes the inbound transfer and exposes it in [`GET /v1/payments/transfers`](/docs/payments/accept-indexing) with `direction=inbound`. **Correlation caveats.** For *customer-initiated* inbound transfers, SDP today does **not** populate `Transfer.memo` (Solana memo-program instructions aren't extracted into the record) and does **not** expose a reference field on the inbound record. The reliable on-record correlation key is the inbound transfer's `destination` — hence the per-order receiving-address recommendation. If you need memo- or reference-based correlation (e.g., for a Solana Pay URL that carries an order pubkey), read the on-chain transaction directly via Solana RPC using the transfer's `signature`. ## Integration options There are two ways customers can land a payment on the address you control: 1. **Direct address share** — you display the address (and the expected token + amount) in your UI. The customer's wallet sends a transfer. This works with any Solana wallet and any client; SDP picks the transfer up purely from the on-chain side. Best for B2B settlement, treasury operations, and crypto-native flows. 2. **Solana Pay request** — for consumer checkout, you give the customer a [Solana Pay](https://solana.com/docs/payments/accept-payments/solana-pay) URL or QR code with the destination, amount, and a per-order `reference` pubkey. The customer's wallet builds the transfer and attaches the `reference` account on-chain, so you can match the transfer back to the order by reading the transaction directly via Solana RPC (the `reference` won't be on the SDP `Transfer` record — neither will any memo the wallet sets, since SDP doesn't extract memo-program instructions for inbound transfers). SDP does not ship a hosted Solana Pay endpoint or attach a Solana Pay reference on-chain today (see [Indexing and reconciliation → Solana Pay reference accounts](/docs/payments/accept-indexing#solana-pay-reference-accounts-roadmap)). For SDP-side correlation today, use **per-order destination addresses** (covered above); use the on-chain transaction (via RPC) when you need to read memo or `reference`. ## What SDP gives you today - **Indexed inbound transfers** — `GET /v1/payments/transfers?direction=inbound` returns persisted transfers across your custody wallets. Filter by exact `custodyWalletId`; add `includeObserved=true` to include recent chain observations for that wallet. See [Indexing and reconciliation](/docs/payments/accept-indexing). - **Per-transfer status reads** — `GET /v1/payments/transfers/{id}` for a single payment's lifecycle. See [Verifying a payment](/docs/payments/accept-verification). - **Wallet balances** — `GET /v1/payments/wallets/{walletId}/balances` for the current per-token balance on a receiving wallet. See [Wallet balances](/docs/wallet-operations/balances). - **Address-based correlation** — the inbound transfer's `destination` is the on-record correlation key for accept flows. Pair with per-order receiving addresses (see above) to map inbound transfers to orders without depending on memo. `Transfer.memo` is populated only for SDP-initiated transfers (outbound or internal); for customer-initiated inbound transfers the field is **omitted** from the response (it's optional in the schema, not nullable). Read the on-chain transaction via Solana RPC if you need memo or Solana Pay reference correlation. ## What SDP does not give you yet - **No checkout session / payment intent object** — you do not pre-register an expected payment with the API. Inbound transfers are recognized after the fact by matching the on-record `destination` (token and amount are available for additional sanity checks). Memo and Solana Pay reference aren't on the transfer record for customer-initiated inbound transfers — see the [correlation caveats](#receiving-address-convention) above; read those from the on-chain transaction via Solana RPC if you need them. - **No webhooks on settlement** — status changes are observed by polling `GET /v1/payments/transfers/{id}` or by re-listing with date filters. See the [polling cadence guidance](/docs/payments/accept-verification#polling-cadence). - **No hosted Solana Pay URL endpoint** — if you want to ship a Solana Pay URL or QR, build it client-side from the destination address, token mint, amount, and reference pubkey. ## Worked example: a marketplace checkout A minimum-viable accept flow built on the per-order-address pattern: 1. At checkout, provision (or claim from a warm pool) a **fresh custody wallet** for the order and stage an `orders` row with that wallet's on-chain address, the expected amount, and the token. 2. Display the per-order custody-wallet address, the token, and the amount to the customer. Build a Solana Pay URL from those fields if you want a QR code; no SDP-specific endpoint required. 3. The customer's wallet submits the transfer. 4. Your reconciliation worker periodically lists recent persisted inbound transfers. If it must discover chain activity not yet persisted, poll each receiving wallet with its exact `custodyWalletId` and `includeObserved=true`. Match each transfer's `destination` to an open order in your `orders` table. 5. When a match lands, mark the order paid and ship the goods. Use the transfer's `signature` (UNIQUE on the transfer record once present) to dedup if the worker is re-run. This is intentionally low-level — there is no SDP-provided session object. Treat the `orders` table as your checkout state machine and SDP as the settlement and status backend. ## Next steps - [Verifying a payment](/docs/payments/accept-verification) — status semantics and how to know when a payment is safe to act on. - [Indexing and reconciliation](/docs/payments/accept-indexing) — listing inbound transfers, deduping, and building a delta-poll worker. --- ### Verifying a payment Source: https://platform.solana.com/docs/payments/accept-verification > Transfer-status semantics, finality on Solana, and recommended polling cadence for SDP transfers. When a payment lands on a wallet you control, the inbound transfer moves through a handful of status states. This page describes those states, what each one means about on-chain finality, and how to decide when it is safe to ship goods or release funds. ## Status lifecycle The `status` field on the `Transfer` record takes one of several values, depending on whether the transfer is a wallet transfer or a ramp transfer. **Wallet transfers** (`POST /v1/payments/transfers`, and inbound transfers discovered on-chain): | Status | Meaning | | --- | --- | | `processing` | The transfer is in flight. For outbound transfers this is the **initial state** at record creation: the transaction is being built and submitted; `signature` is not yet populated. | | `confirmed` | The cluster has confirmed the transaction. `signature` and `slot` are populated. `blockTime` may also be set, but is not guaranteed at this stage — treat it as optional until `finalized`. | | `finalized` | The slot containing the transaction is locked in — finality at the Solana cluster's strongest guarantee. `signature` and `slot` are populated; `blockTime` is best-effort and may remain `null` for transfers finalized by the `track-pending-transfers` job (which updates status and slot but does not backfill block time). Resolve a definitive timestamp from `signature` via Solana RPC if you need one. Safe to treat as irreversible. | | `failed` | The transaction was built and submitted but rejected by the network (insufficient funds, frozen account, expired blockhash, etc.). The `error` field describes what reached the chain. | Outbound wallet transfers move `processing → confirmed → finalized`. Inbound transfers discovered via on-chain signature history surface directly at `confirmed` (or `failed`) — they never appear as `processing`. **Ramp transfers** (created by a [Ramp Quote](/docs/payments/ramps)): | Status | Meaning | | --- | --- | | `pending` | The quote was created; no funding has been detected yet. No `signature`. | | `awaiting_payment` | The quote's delivery mode is `manual_instructions` — SDP is waiting for the payer to fund the account described in the quote. | | `settling` | The provider has detected or started processing the payment. | | `completed` | The provider reports the ramp settled. | | `canceled` | Canceled via `POST /v1/payments/ramps/transfers/cancel` before funding. | | `expired` | The funding window elapsed without payment. | | `failed` | The provider rejected or reversed the ramp. The `error` field describes why, when set. | Any state can transition to `failed`; `failed`, `completed`, `canceled`, and `expired` are terminal. ## Finality on Solana For real-money flows, treat `finalized` as the safe-to-act-on signal. `confirmed` is durable enough for most non-adversarial workflows (think: showing a "paid" badge in your dashboard) but a sufficiently motivated cluster reorganization can theoretically roll back a `confirmed` transaction; this does not happen for `finalized` transactions. Use `confirmed` to release low-stakes side effects (status emails, UI updates) and `finalized` for irreversible side effects (releasing custody, shipping high-value goods, posting to your general ledger). ## Polling cadence SDP does not currently emit settlement webhooks. Status is observed by polling: - `GET /v1/payments/transfers/{id}` for a single transfer's current state. - `GET /v1/payments/transfers?direction=inbound&from=…` for a windowed list — see [Indexing and reconciliation](/docs/payments/accept-indexing). Cadence depends on how quickly you need to see state transitions and how aggressive your retry budget is. A reasonable starting range: - **Active checkout** (customer is staring at a 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 whenever a worker tick fires. - **Long-tail catch-up** (status reads after the fact): single one-shot reads when your application needs to act, no polling loop at all. The exact recommended cadence will be tightened in a follow-up once the backend confirms a target; treat the ranges above as defensible defaults today. ## Independent verification Every transfer that reaches `processing` or beyond carries enough information to verify against the network directly: - **`signature`** — the Solana transaction signature; paste into [Solscan](https://solscan.io/) or [Solana Explorer](https://explorer.solana.com/) to see the on-chain record. - **`slot`** — the slot the transaction landed in. - **`blockTime`** — ISO 8601 timestamp of slot finality. - **`fee`** — fee paid, in lamports. If you maintain your own indexer (`getSignatureStatuses` against a Solana RPC), cross-reference SDP's `signature` to your indexer's view to confirm SDP's status reflects what your nodes see. ## Risk metadata When a risk-screening provider is configured on the organization, transfers may carry a `risk` field with `provider`, `score`, `level` (`low | medium | high | unknown`), and `evaluatedAt`. Use this to gate downstream side effects on a risk threshold; the field is absent when no risk provider is wired up. Risk evaluation is not blocking by default — a transfer can complete with a high-risk score. If you want a transfer to halt on risk threshold, enforce that in your application logic on top of the `risk.level` value. ## Failure handling When `status` settles to `failed`: - Read the `error` field. SDP returns the chain-level error message; common causes are insufficient source funds, account frozen by token authority, or stale blockhash. - Decide whether to retry. The original transfer record is terminal; a retry creates a new transfer with a new ID and (eventually) a new signature. - Surface the failure to whoever needs to know — the merchant, the customer, your operations team. ## Related - [Accept overview](/docs/payments/accept-overview) — the broader inbound-payment flow. - [Indexing and reconciliation](/docs/payments/accept-indexing) — list-based status checking across many transfers. - [Concepts: the transfer data model](/docs/payments/concepts#the-transfer-data-model) — full field reference. --- ### Indexing and reconciliation Source: https://platform.solana.com/docs/payments/accept-indexing > List inbound transfers, paginate, dedupe, and reconcile against your product ledger. `GET /v1/payments/transfers` is the workhorse for reconciliation. By default, it returns paginated persisted transfer records across your organization's custody wallets, with filters for direction, status, token, exact `custodyWalletId`, and date range. Use it to mirror SDP transfers locally, match inbound payments to orders, and drive a delta-polling worker. To include recent on-chain observations for one exact wallet, set `custodyWalletId` and `includeObserved=true`. ## Listing inbound transfers The minimal inbound-only request: ```bash title="Terminal" curl "https://api.solana.com/v1/payments/transfers?direction=inbound&pageSize=50" \ -H "Authorization: Bearer sk_test_..." ``` ```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 sk_test_..." }, }); const { data, meta } = await res.json(); // data: Transfer[] // meta: { total, page, pageSize, hasMore, requestId } ``` ### Query parameters | Parameter | Type | Notes | | --- | --- | --- | | `direction` | `inbound` \| `outbound` | Common filter. Inbound = payments to wallets you control. | | `status` | `pending` \| `processing` \| `confirmed` \| `finalized` \| `failed` | Scope to a single state. | | `custodyWalletId` | string | Exact SDP Wallet ID (`id` from `GET /v1/wallets`). | | `includeObserved` | boolean | Default `false`. When `true`, adds recent on-chain activity for the exact `custodyWalletId`; it cannot be used without that filter. Synthetic observed rows have `custodyWalletId: null`. | | `token` | string | Filter by token symbol or on-chain mint (exact match against the stored transfer's `token` value). | | `from`, `to` | ISO 8601 datetime | Time-window filter on `createdAt`. Use with offset (`2026-05-14T00:00:00Z`). | | `page` | integer | Default `1`. | | `pageSize` | integer | Default `20`, max `100`. | ## Deduplication Two natural dedup keys: 1. **`Transfer.id`** — SDP-internal, immutable, present on every transfer (including `pending` ones with no signature yet). Use this as your local primary key. 2. **`Transfer.signature`** — the on-chain signature, populated once the transaction has been built and observed on-chain (typically at `confirmed`/`finalized`, but treat its presence rather than the status enum as the trigger — `processing` and even `pending` records can still have a null signature). **`UNIQUE` across SDP's transfers table** wherever it is set; two records cannot share a signature. Use this if you need to dedupe across multiple data sources (SDP + a parallel chain indexer, say) — only once `signature` is present. In practice: store transfers in your reconciliation table keyed by SDP `Transfer.id`, and assert `signature` uniqueness once it is populated. ## Reconciling an order Customer-initiated inbound transfers do **not** populate `Transfer.memo` (memo-program instructions aren't extracted into the field), and the inbound record doesn't expose a reference. The reliable on-record correlation key is the inbound transfer's `destination` — match it against an order whose pre-issued receiving address is that same `destination`. The typical inbound match cycle: 1. **Read** recent persisted inbound transfers for the relevant token: ```text title="Request" GET /v1/payments/transfers ?direction=inbound &token= &from= &to= &pageSize=100 ``` 2. **For each transfer**, look up an open order keyed by the transfer's `destination` (`findOrderByDestination(tr.destination)`). 3. **If matched**, assert the amount and token match the expected values on the order row, then mark the order paid, record the SDP `Transfer.id`, and dispatch downstream side effects (email, shipment, ledger entry). 4. **If unmatched**, leave the transfer for the next pass (it may be an early-arrival for an order you haven't staged yet) or flag it for manual review after a grace window. If you need memo- or Solana-Pay-`reference`-based correlation (e.g., a single-wallet flow where the destination address is shared across orders), read the on-chain transaction directly via Solana RPC using the transfer's `signature`. SDP does not surface either field on inbound transfer records today. ## Solana Pay reference accounts (roadmap) SDP does not attach a Solana Pay reference pubkey on-chain, and inbound transfer records do not expose a reference field today. The realistic correlation options are: (a) **per-order destination addresses** — pre-issue a fresh receiving wallet per order and match by `Transfer.destination` (the recommended pattern; see [Reconciling an order](#reconciling-an-order) above); (b) **on-chain reads via Solana RPC** keyed off the transfer's `signature`, used to recover the memo-program instruction or Solana Pay `reference` that SDP itself doesn't surface for inbound transfers. ## A delta-poll worker A reconciliation worker that wakes on a tick and asks "what is new since last time": ```typescript title="reconcile-tick.ts" const OVERLAP_MS = 5_000; // re-query the last 5s on each tick to catch boundary arrivals async function reconcileTick(state: WorkerState) { // Nudge `from` backwards by the overlap window; `alreadySeen` dedupes the // resulting duplicates. Always advance the high-water mark to `to` regardless, // otherwise the window grows without bound. const fromMs = state.lastSeenIso ? Date.parse(state.lastSeenIso) - OVERLAP_MS : Date.now() - 60_000; const from = new Date(fromMs).toISOString(); const to = new Date().toISOString(); // This lists persisted records. To discover additional recent chain activity, // run the same window per exact wallet with custodyWalletId + includeObserved. for await (const tr of listInbound({ from, to })) { if (await alreadySeen(tr.id)) continue; // Match by destination — the on-record correlation key for inbound transfers. // `tr.memo` is omitted (undefined) for customer-initiated inbound payments. const order = await findOrderByDestination(tr.destination); if (!order) { await persistUnmatched(tr); continue; } if (tr.status === "finalized") { await markOrderPaid(order, tr); } await persistSeen(tr.id, tr.status); } state.lastSeenIso = to; } ``` Notes: - The overlap window (5s above) re-queries the trailing edge of the last tick to catch transfers that arrive at the boundary; `alreadySeen` collapses the resulting duplicates. - The high-water mark advances to `to` on every pass even though `from` is rolled back — otherwise the query window grows unbounded. - Always re-fetch a transfer to upgrade its status: a transfer seen as `confirmed` in one tick will appear as `finalized` in a later tick. - Use `pageSize=100` and the async iterator pattern (see [Payouts and disbursements](/docs/payments/send-payouts)) to handle large windows. ## Related - [Verifying a payment](/docs/payments/accept-verification) — single-transfer status reads. - [Accept overview](/docs/payments/accept-overview) — the broader inbound flow. - [Payouts and disbursements](/docs/payments/send-payouts) — the same list endpoint used outbound. - [Payments API reference](/docs/reference/api/payments) — full endpoint reference. --- ## Payments — Ramps ### Ramps Source: https://platform.solana.com/docs/payments/ramps > Fiat on-ramps and off-ramps — discover corridors, price a Ramp Quote just-in-time, and hand off to the provider's own execution flow. SDP wraps ramp providers behind a single pricing step: a **Ramp Quote**, priced just-in-time against the provider. There is no execute endpoint — SDP quotes the ramp and hands the end-user to the provider's own flow (hosted redirect or funding instructions) to complete it. Every quote is tied to a **Counterparty** — the KYC subject for the ramp — and a custody wallet you control. For per-provider configuration, see [Ramp providers](/docs/payments/ramps-providers). ## Prerequisites 1. **A Counterparty.** Create one with `POST /v1/counterparties` before requesting a quote. 2. **Provider Requirements satisfied**, if the provider needs them. Some providers require KYC or bank details collected against the counterparty before they will price or accept a quote. Check `GET /v1/counterparties/{counterpartyId}/requirements?provider=…&direction=…` and submit collected fields with `POST` to the same path until `status` reads `ready`. 3. **A custody wallet you control**, used as the crypto leg — the onramp destination or the offramp source. ## Discovering corridors `GET /v1/payments/ramps/onramp/currency` and `GET /v1/payments/ramps/offramp/currency` list the fiat/crypto pairs SDP supports and which providers cover each one — the **Corridor** dimensions (currency, country coverage, entity type). ```bash title="Terminal" curl "https://api.solana.com/v1/payments/ramps/onramp/currency?source=USD&dest=usdc.solana" \ -H "Authorization: Bearer sk_test_..." ``` | Query param | Notes | | --- | --- | | `source` | Onramp: fiat currency (ISO code). Offramp: crypto rail (`usdc.solana`, `sol.solana`, …). | | `dest` | Onramp: crypto rail. Offramp: fiat currency. | | `provider` | Narrow to one provider. | The response's `pairs` list which providers cover each source/dest combination; `providerDetails` gives each provider's country coverage and supported entity types for the direction. ## Estimating a rate `POST /v1/payments/ramps/onramp/estimate` and `.../offramp/estimate` return a rate preview without creating anything — no counterparty or wallet required. SDP calls every provider on the corridor concurrently and returns one result per provider: `ok` (with the estimate), `unsupported`, or `error`. ```bash title="Terminal" curl -X POST https://api.solana.com/v1/payments/ramps/onramp/estimate \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "assetRail": "usdc.solana", "fiatCurrency": "USD", "fiatAmount": "100.00" }' ``` ```typescript title="estimate-onramp.ts" const response = await fetch( "https://api.solana.com/v1/payments/ramps/onramp/estimate", { method: "POST", headers: { "Authorization": "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify({ assetRail: "usdc.solana", fiatCurrency: "USD", fiatAmount: "100.00", }), } ); const { data } = await response.json(); // data.estimates — one entry per provider on the corridor ``` Each entry under `estimates` carries `fiatAmount`, `cryptoAmount`, `exchangeRate`, and `fees` when `status` is `ok`. Use this to show the user a rate before they commit to a quote. ## Requesting a quote `POST /v1/payments/ramps/onramp/quote` (fiat → crypto) and `POST /v1/payments/ramps/offramp/quote` (crypto → fiat) create the actual **Ramp Quote** against the named provider. This is the step that talks to the provider and, for most providers, produces a provider-side record — unlike estimate, it requires a resolved counterparty and wallet. ```bash title="Terminal" curl -X POST https://api.solana.com/v1/payments/ramps/onramp/quote \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "provider": "moonpay", "counterpartyId": "cp_...", "destinationCustodyWalletId": "cwlt_...", "assetRail": "usdc.solana", "fiatCurrency": "USD", "fiatAmount": "100.00" }' ``` ```typescript title="onramp-quote.ts" const response = await fetch( "https://api.solana.com/v1/payments/ramps/onramp/quote", { method: "POST", headers: { "Authorization": "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify({ provider: "moonpay", counterpartyId: "cp_...", destinationCustodyWalletId: "cwlt_...", assetRail: "usdc.solana", fiatCurrency: "USD", fiatAmount: "100.00", }), } ); const { data } = await response.json(); // data.quote.deliveryMode tells you what to do next — see below ``` ```java title="OnrampQuote.java" HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.solana.com/v1/payments/ramps/onramp/quote")) .header("Authorization", "Bearer sk_test_...") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(""" { "provider": "moonpay", "counterpartyId": "cp_...", "destinationCustodyWalletId": "cwlt_...", "assetRail": "usdc.solana", "fiatCurrency": "USD", "fiatAmount": "100.00" }""")) .build(); ``` ### Request fields | Field | Required | Notes | | --- | --- | --- | | `provider` | yes | One of `moonpay`, `lightspark`, `bvnk`, `moneygram`, `coinbase`, `mural`, `stripe`. | | `counterpartyId` | yes | An existing Counterparty. Provider-side customer and payout-account state is resolved server-side from it, not passed on the request. Counterparties do not store identity or email fields. | | `destinationCustodyWalletId` (onramp) / `sourceCustodyWalletId` (offramp) | yes | A custody wallet you control — its `id` (the `id` returned by `GET /v1/wallets`), resolved against your API key's wallet scope. Same shape for every provider. | | `assetRail` | yes | Canonical SDP crypto asset rail (`usdc.solana`, `usdt.solana`, `sol.solana`, …). | | `fiatCurrency` | onramp: yes; offramp: conditional | Required for onramp and for Lightspark/BVNK offramp; providers that resolve fiat currency during their own flow (MoneyGram) can omit it on offramp. | | `fiatAmount` (onramp) / `cryptoAmount` (offramp) | yes | Decimal string greater than zero. | | `rampsMemo` | no | Up to 20 key/value string pairs stored on the resulting transfer for your own correlation. | The quote also creates the underlying **Payment Transfer** record for the ramp; log the returned `quote.id` (the transfer's `providerReference`) for your own records. ### Delivery mode `quote.deliveryMode` tells you how to complete the flow with the named provider: | Provider | `deliveryMode` | What to do | | --- | --- | --- | | MoonPay | `hosted` | Redirect the user to `quote.hostedUrl`. | | Lightspark | `manual_instructions` | Display the funding instructions in `quote.paymentInstructions` (bank or wallet details to send fiat to). | | BVNK | `manual_instructions` | Same — display `quote.paymentInstructions`. | | MoneyGram | `session_widget` | Mount `quote.widgetUrl` using `quote.sessionToken`. Onramp and offramp both. | | Coinbase | `hosted` | Redirect the user to `quote.hostedUrl`. Onramp only; quote creation is sandbox-only today. | | Mural | `manual_instructions` | Display `quote.paymentInstructions`. Onramp only. | | Stripe | `session_widget` | Mount the session with `quote.clientSecret` and `quote.publishableKey`. Onramp only. | There is no further SDP endpoint to call after this — the provider owns settlement from here. Watch for the resulting on-chain transfer on the destination wallet via [`GET /v1/payments/transfers?direction=inbound`](/docs/payments/accept-indexing), or watch the provider's own side (dashboard or their webhook) for offramp payout confirmation. ## Canceling a quote `POST /v1/payments/ramps/transfers/cancel` cancels the transfer behind a quote while it is still `pending` or `awaiting_payment` — before the provider has moved money. ```bash title="Terminal" curl -X POST https://api.solana.com/v1/payments/ramps/transfers/cancel \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "provider": "bvnk", "providerReference": "..." }' ``` A transfer past that window (already funded or settling) returns an error instead of canceling. ## Status field `quote.status` is one of `pending`, `processing`, `completed`, `failed`, reflecting the provider's reported state **at the moment the quote is created** — SDP does not subsequently poll the provider to refresh it, and there is no dedicated read endpoint for the quote itself. For the on-chain side of a successful onramp, observe the resulting **inbound transfer** on the destination wallet, surfaced via [Accept payments](/docs/payments/accept-overview). ## Rate limits Estimate and quote calls carry fail-closed quotas: estimate fans out one live call per provider on the corridor, quote creates a provider-side record. | Endpoint | Per-key limit | Per-org limit | | --- | --- | --- | | `.../estimate` | 30 / minute | 120 / minute | | `.../quote` | 20 / minute | 60 / minute | ## Provider selection You always pass `provider` explicitly; SDP does not auto-select. Choose based on: - **Coverage** — which providers your org has [Provider Availability](/docs/payments/ramps-providers#provider-gating) for. - **Corridor** — currency, country, and entity type; check with the currency-discovery endpoints above. - **Sandbox needs** — sandbox vs production is chosen per request from the calling API key's environment (see [Sandbox vs production](/docs/payments/ramps-providers#sandbox-vs-production-selection)). ## Fiat currency support Fiat currency support is corridor-specific, not a fixed allowlist — use `GET /v1/payments/ramps/onramp/currency` and `.../offramp/currency` to see which fiat currencies a given provider covers for a given crypto rail. ## Related - [Ramp providers](/docs/payments/ramps-providers) — per-provider configuration and capability matrix. - [Accept overview](/docs/payments/accept-overview) — how onramp deliveries show up as inbound transfers. - [Provider onboarding](/docs/reference/provider-onboarding) — how providers are activated for an organization. --- ### Ramp providers Source: https://platform.solana.com/docs/payments/ramps-providers > MoonPay, Lightspark, BVNK, MoneyGram, Coinbase, Mural, and Stripe — capability matrix, configuration, and per-provider notes. SDP currently integrates seven ramp providers. They are configured at the organization level via environment variables on the SDP API deployment; the same `provider` string on `POST /v1/payments/ramps/onramp/quote` and `POST /v1/payments/ramps/offramp/quote` selects between them. ## Sandbox vs production selection The sandbox/production mode is chosen **per request**, not at deployment time: - Non-production SDP deployments (`ENVIRONMENT !== "production"`) always use sandbox credentials. - In production deployments, the calling API key's environment picks the mode — `sk_test_` keys use sandbox, `sk_live_` use production. Configure both credential pairs for any provider you want to expose to test and live traffic on the same deployment — MoonPay, Lightspark, BVNK, and Mural read separate sandbox and production credentials this way. Coinbase and Stripe use a single credential pair regardless of the resolved mode (see their sections below); MoneyGram only has sandbox credentials today. ## Capability matrix | Provider | Onramp | Offramp | Sandbox | Delivery mode | Notes | | --- | --- | --- | --- | --- | --- | | MoonPay | ✓ | ✓ | ✓ | `hosted` | Sandbox vs production is selected per request (see [Sandbox vs production](#sandbox-vs-production-selection)). | | Lightspark | ✓ | ✓ | ✓ | `manual_instructions` | Grid API. Same per-request sandbox/production selection as MoonPay. | | BVNK | ✓ | ✓ | ✓ | `manual_instructions` | Hawk auth. Compliance party details are built from the Counterparty record, not passed on the request. | | MoneyGram | ✓ | ✓ | ✓ | `session_widget` | Sandbox-only pilot integration — there is no production credential path yet. | | Coinbase | ✓ (sandbox only) | ✗ | ✓ | `hosted` | Rate estimates work in production; order creation is sandbox-only. Onramp only. | | Mural | ✓ | ✗ (estimate only) | ✓ | `manual_instructions` | Off-ramp rate estimate works; off-ramp quote creation is not implemented yet. | | Stripe | ✓ | ✗ | ✓ (shared credentials) | `session_widget` | No separate sandbox credentials — supply a Stripe test-mode secret key. Onramp only. | ## MoonPay | Variable | Required | Notes | | --- | --- | --- | | `MOONPAY_API_KEY` | conditional | Production API key. Required when the request resolves to production. | | `MOONPAY_SECRET_KEY` | conditional | Production secret used to sign onramp / offramp URLs. Paired with `MOONPAY_API_KEY`. | | `MOONPAY_SANDBOX_API_KEY` | conditional | Sandbox API key. Required when the request resolves to sandbox. | | `MOONPAY_SANDBOX_SECRET_KEY` | conditional | Sandbox secret. Paired with `MOONPAY_SANDBOX_API_KEY`. | | `MOONPAY_ONRAMP_URL` | no | Override the onramp host. Defaults to MoonPay's standard URLs; the sandbox/production choice follows the [per-request rule](#sandbox-vs-production-selection). | | `MOONPAY_OFFRAMP_URL` | no | Same idea for offramp. | MoonPay normalizes provider currency codes internally — SDP accepts the canonical asset rails such as `usdc.solana` and maps them to MoonPay's `usdc_sol` code. ## Lightspark | Variable | Required | Notes | | --- | --- | --- | | `LIGHTSPARK_GRID_CLIENT_ID` | conditional | Production Grid API client identifier. Required when the request resolves to production. | | `LIGHTSPARK_GRID_CLIENT_SECRET` | conditional | Production Grid API client secret. | | `LIGHTSPARK_GRID_SANDBOX_CLIENT_ID` | conditional | Sandbox Grid API client identifier. Required when the request resolves to sandbox. | | `LIGHTSPARK_GRID_SANDBOX_CLIENT_SECRET` | conditional | Sandbox Grid API client secret. | Lightspark is integrated against the Grid API (`https://api.lightspark.com/grid/2025-10-13`); the sandbox/production choice follows the [per-request rule](#sandbox-vs-production-selection). `destinationCustodyWalletId` / `sourceCustodyWalletId` are your own custody wallet, same as every other provider — Lightspark's customer id and payout account are resolved server-side from the counterparty's Provider Requirements, not passed on the quote request. Counterparties no longer store identity or email fields. Existing Lightspark customers continue to resolve from provider data, but first-time customer creation currently fails with a clear error until just-in-time identity collection is connected. Lightspark quotes always return `deliveryMode: "manual_instructions"` — the response carries funding instructions to display to the payer, not a redirect URL. ## BVNK | Variable | Required | Notes | | --- | --- | --- | | `BVNK_WALLET_ID` | conditional | Production BVNK-side wallet used for settlement. | | `BVNK_HAWK_AUTH_ID` | conditional | Production Hawk auth id. Paired with `BVNK_HAWK_SECRET_KEY`. | | `BVNK_HAWK_SECRET_KEY` | conditional | Production Hawk secret key. | | `BVNK_SANDBOX_WALLET_ID` | conditional | Sandbox BVNK wallet. Required when the request resolves to sandbox. | | `BVNK_SANDBOX_HAWK_AUTH_ID` | conditional | Sandbox Hawk auth id. | | `BVNK_SANDBOX_HAWK_SECRET_KEY` | conditional | Sandbox Hawk secret key. | BVNK uses Hawk authentication. Bearer-token auth is not currently supported. BVNK is a travel-rule / compliance-sharing provider and requires party details such as name and address. Counterparties no longer store those fields, so customer creation, payment-rule creation, and off-ramp compliance payloads currently fail with a clear error at the pending just-in-time collection seam. BVNK quotes always return `deliveryMode: "manual_instructions"` — the response carries funding-account details to display to the payer. ## MoneyGram | Variable | Required | Notes | | --- | --- | --- | | `MONEYGRAM_SANDBOX_PUBLIC_KEY` | conditional | Sandbox public key, sent as `x-api-key` on the rail-discovery (currency list) call. | | `MONEYGRAM_SANDBOX_SECRET_KEY` | conditional | Sandbox secret key, sent as `x-api-key` on estimate, quote, and session calls. | MoneyGram is a sandbox-only pilot integration today — the client always calls MoneyGram's sandbox host and throws if a request resolves to production; there is no production credential pair to configure yet. MoneyGram quotes always return `deliveryMode: "session_widget"`: both onramp and offramp create a session against the same endpoint and return a `widgetUrl` (with `mode=on-ramp` or `mode=off-ramp`) plus a short-lived `sessionToken` to mount. It requires no Provider Requirements collection — every counterparty is treated as ready. ## Coinbase | Variable | Required | Notes | | --- | --- | --- | | `COINBASE_CDP_API_KEY_ID` | conditional | CDP Secret API Key id. Same key is used for sandbox and production — sandbox is flagged per-request, not by a separate credential. | | `COINBASE_CDP_API_KEY_SECRET` | conditional | CDP Secret API Key secret. Paired with `COINBASE_CDP_API_KEY_ID`; used to sign a per-request JWT. | Coinbase is onramp-only — both `estimateOfframp` and `createOfframpQuote` reject with "Coinbase Onramp does not support off-ramp." Onramp quotes always return `deliveryMode: "hosted"`: SDP creates a guest-checkout Apple Pay order and returns its payment link as `hostedUrl`. Order creation (the quote step) is sandbox-only today — it requires buyer agreement and phone-OTP attestation timestamps that SDP's production flow does not yet collect, so a non-sandbox request fails loudly instead of sending a false attestation. Rate estimates have no such restriction and work in both modes. Coinbase also requires email and phone values; quote creation currently fails at the pending just-in-time collection seam. ## Mural | Variable | Required | Notes | | --- | --- | --- | | `MURAL_PAY_API_KEY` | conditional | Production API key for organization, account, and payin calls. | | `MURAL_PAY_SANDBOX_API_KEY` | conditional | Sandbox equivalent. Required when the request resolves to sandbox. | | `MURAL_PAY_TRANSFER_API_KEY` | conditional | Production transfer-scoped key, sent as a separate header on payout-execute calls. | | `MURAL_PAY_SANDBOX_TRANSFER_API_KEY` | conditional | Sandbox equivalent of the transfer key. | Mural onramp is business-counterparty only (no individual entity type). A quote requires the counterparty's Mural organization and payin account already provisioned — through Provider Requirements onboarding (organization creation, terms-of-service acceptance, verification) — and returns `deliveryMode: "manual_instructions"` built from that account's active payin method; the quote step itself makes no live Mural call. Existing Mural organizations continue to resolve from provider data. First-time organization creation currently fails with a clear error until just-in-time identity collection is connected. Off-ramp rate estimates work today (a real call to Mural's fees endpoint), but off-ramp quote creation always throws — it is not implemented yet. ## Stripe | Variable | Required | Notes | | --- | --- | --- | | `STRIPE_SECRET_KEY` | conditional | Secret key, sent as the username on HTTP Basic auth. A single key serves both modes — use a `sk_test_…` key to sandbox it, `sk_live_…` for production. | | `STRIPE_PUBLISHABLE_KEY` | conditional | Publishable key, echoed back on the quote for the client to mount the onramp session. | Stripe is onramp-only — both `estimateOfframp` and `createOfframpQuote` reject as unsupported. Onramp is also limited to `usdc.solana` and `sol.solana` as destination assets. Quotes always return `deliveryMode: "session_widget"`: a `clientSecret` and `publishableKey` to mount Stripe's crypto onramp session, plus an optional `redirectUrl`. ## Provider gating Even when all seven providers' env vars are populated, an org can be restricted to a subset. This is [**Provider Availability**](/docs/reference/provider-onboarding): a provider must be configured for the deployment and either generally available or manually activated for the organization. SDP checks this on every ramp call — if the org lacks availability for the requested provider, the call returns an error. To activate or change provider availability for a deployment, see [Provider onboarding](/docs/reference/provider-onboarding). ## Choosing a provider | Need | Lean towards | | --- | --- | | Fastest sandbox iteration | MoonPay or BVNK. | | Travel-rule / compliance party metadata | BVNK. | | Lightning-aware fiat settlement | Lightspark. | | Broadest payment-method coverage | MoonPay (corridor-dependent). | | Business-entity onramp counterparties | Mural (its only supported entity type). | | Widget/session-embedded onramp UI | MoneyGram or Stripe. | | Onramp only, no off-ramp needed | Coinbase or Stripe. | For self-hosted deployments where only a subset of providers will ever be configured, see [Providers optional for self-hosted SDP](/docs/reference/provider-onboarding) — SDP supports running with any single provider active. ## Related - [Ramps](/docs/payments/ramps) — onramp and offramp endpoints and request shape. - [Provider onboarding](/docs/reference/provider-onboarding) — activation flow. --- ## Tutorials ### Issue a Regulated Stablecoin Source: https://platform.solana.com/docs/tutorials/issue-a-regulated-stablecoin > Build a GENIUS-compliant digital dollar on Solana with institutional custody, integrated compliance, and operational reversibility from day one. This tutorial walks you through issuing a regulated stablecoin on Solana. The product you ship at the end is one that a Federal Reserve examiner could read and a bank's compliance committee could sign against. Issuing one is not the same as deploying a token. It requires institutional custody, integrated compliance, and operational reversibility from day one. The [GENIUS Act](https://www.gibsondunn.com/the-genius-act-a-new-era-of-stablecoin-regulation/) treats those capabilities as preconditions, not features. ## 1. What this work demands **1. Custody is not your laptop.** A regulated stablecoin is held by a federally chartered custodian. For a US issuer operating under the GENIUS Act framework, that usually means Anchorage Digital Bank or an equivalent federally chartered crypto bank. The custodian holds the signing keys for the mint, freeze, and permanent-delegate authorities. Not the engineer at the issuer. **2. Compliance is integrated from day one.** Every transfer destination gets screened before the transfer is built. Every frozen account ties back to a court order, sanctions notice, or internal compliance ticket. The integration is not bolted on later; it sits on the same API surface as the mint and burn endpoints. **3. Operational reversibility is the product, not a failure mode.** Freeze, seize, and force-burn are the technological capabilities the GENIUS Act requires you to have ready before launch. They exist in the SDP API because the regulation says they must, and your compliance officer will check for them. Section 5 covers each in detail. **4. The reader is institutional.** This tutorial assumes you are a developer at a financial institution or fintech, building under a compliance officer's oversight. The decisions you make are decisions she will see in product review. Section 2 walks through the prerequisites in that light. Every code sample in this tutorial was exercised against SDP's sandbox during writing; the request and response shapes shown here are what the live API actually returned. ## 2. Prerequisites You need an SDP account, a custody provider, two project-scoped API keys (one for routine operations, one for compliance operations), and a funded devnet wallet. About thirty minutes end-to-end if you have nothing today. ### 1. Account and organization Sign up at [platform.solana.com](https://platform.solana.com) using email, Google, or GitHub. Create your organization when the dashboard prompts you. SDP provisions a matching organization on its side via a [Clerk](https://clerk.com) webhook; the dashboard's onboarding cards clear once the sync lands. ### 2. Custody provider An institutional issuer in the US pipeline configures [Anchorage Digital Bank](https://www.anchorage.com/platform/stablecoin-issuance) as the custodian. The Anchorage onboarding team provisions the signing wallets and binds them to your SDP organization. If you're working on the self-serve sandbox without an Anchorage relationship yet, SDP auto-provisions Privy wallets for development access instead. The API surface is identical; only the wallet record's `provider` field differs. ### 3. API keys: the two-key institutional pattern The hardest cliff in setup is that the dashboard's "Create API key" dialog only mints **org-scoped** keys. The issuance API requires **project-scoped** keys. Project management in the dashboard is on the SDP roadmap; until it lands, the workaround is a short sequence of API calls. The bootstrap path: From the dashboard, mint an org-scoped API key with **Role: Admin**, **Environment: Sandbox**. This is your bootstrap credential. ![Create API key dialog in the SDP dashboard, with Role set to Admin and Environment set to Sandbox.](/issue-a-regulated-stablecoin/01-create-api-key-dialog.png) With the Admin key, call `GET /v1/projects` and pick the project with slug `default-sandbox`. Every organization is provisioned with exactly one sandbox and one production project automatically — isolation between workloads comes from separate organizations, not additional projects. The project ID is what you'll need in the next step: ```json title="response.json" { "data": { "projects": [ { "id": "prj_3bc13bcb-...", "slug": "default-sandbox", "environment": "sandbox", "status": "active" }, { "id": "prj_8a201d47-...", "slug": "default-production", "environment": "production", "status": "active" } ] } } ``` Call `POST /v1/projects/{projectId}/api-keys` twice to mint two project-scoped keys: one with `role: "api_developer"` for routine create, mint, and burn calls, and one with `role: "api_admin"` for compliance operations (screening, freeze, seize, force-burn). Each response includes the full key value exactly once: ```json title="response.json" { "data": { "apiKey": { "id": "key_375fe1e1-...", "keyPrefix": "sk_test__b3", "key": "sk_test_", "role": "api_developer", "environment": "sandbox" } } } ``` The `key` field appears only on this response. Capture it into your secrets manager immediately; SDP does not surface it again. Store the two project-scoped keys with your other production secrets. Keep the org-scoped Admin key cold; you only touch it when you provision additional keys or rotate. ### 4. Sandbox readers: fund your custody wallet On devnet, the custody wallet starts at zero lamports, which means SDP cannot pay transaction fees from it. Fund it via the browser faucet: Go to [faucet.solana.com](https://faucet.solana.com). Set the network to **Devnet**. Paste the wallet's public key. Request one SOL. The browser faucet is more reliable than the RPC `requestAirdrop` method, which is heavily rate-limited. Production readers operating under Anchorage do not handle this step manually; the bank funds the custody address as part of its standard operational support. ### 5. API base URL The canonical SDP API URL is `https://api.solana.com`. Use this hostname for every production request. ### 6. Wire up your client and verify Wire two HTTP clients (one per project-scoped key) using a thin wrapper around `fetch`: ```javascript title="sdp-client.js" const BASE = "https://api.solana.com"; class SdpClient { constructor(apiKey, baseUrl = BASE) { this.apiKey = apiKey; this.baseUrl = baseUrl; } async request(method, path, body) { const res = await fetch(`${this.baseUrl}${path}`, { method, headers: { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json", }, body: body ? JSON.stringify(body) : undefined, }); const payload = await res.json(); if (!res.ok) throw new Error(`${payload.error.code}: ${payload.error.message}`); return payload; } get(path) { return this.request("GET", path); } post(path, body) { return this.request("POST", path, body); } } const sdp = new SdpClient(process.env.SDP_API_KEY); const sdpAdmin = new SdpClient(process.env.SDP_ADMIN_API_KEY); ``` Use `sdp` for routine operations (create, mint, burn) and `sdpAdmin` for compliance operations (screening, freeze, seize, force-burn). Section 4 extends this client with `Idempotency-Key` header support when state-mutating execute endpoints come into play. Now confirm the setup by listing the wallets bound to your project: ```javascript title="list-wallets.js" const { data } = await sdp.get("/v1/wallets"); const custodyWallet = data.wallets.find( (wallet) => wallet.status === "active" && wallet.isRuntimeExecutionAllowed ); if (!custodyWallet) throw new Error("No active custody wallet can execute"); const signingCustodyWalletId = custodyWallet.id; const custodyAddress = custodyWallet.publicKey; console.log(`Custody wallets configured: ${data.wallets.length}`); console.log(`Deployment wallet: ${signingCustodyWalletId}`); ``` A working setup prints: ```text title="Output" Custody wallets configured: 2 Deployment wallet: cwlt_123 ``` If the response includes an active wallet with `isRuntimeExecutionAllowed: true`, your custody is bound and you are ready to deploy a token. If it returns no wallets, your custody provider has not finished provisioning. For the Anchorage pipeline, contact your onboarding lead. For the self-serve sandbox, wait a minute and re-run; Privy's initial wallet provision can lag the SDP organization creation by a few seconds. Keep both values. `signingCustodyWalletId` is the exact SDP Wallet record `id` (`cwlt_*`) used by issuance requests. It is not the provider-specific `walletId` or the Solana `publicKey`; `custodyAddress` is that public key and is used only where an on-chain address is required. ## 3. Design the token Before any API call, you decide three things: which template to start from, what compliance posture the token deploys with, and which authorities the custodian holds. The design is what your compliance officer reviews. The API calls in Section 4 are the mechanical follow-through. ### 1. Pick the template SDP ships three public **Tokenization Templates**, each prescribing a fixed set of [Token-2022](https://solana.com/docs/tokens/extensions) extensions tuned to an institutional product shape: - **`stablecoin`**. 6 decimals by default. Required extensions: `permanentDelegate` and `pausable`. **Token Access Control** allowlist off. The right template for a regulated dollar. - **`tokenized-security`**. 8 decimals. Required extensions: `permanentDelegate`, `pausable`, and `scaledUiAmount`. Token Access Control allowlist on; accounts open frozen. The right template for a regulated security. - **`custom`**. 9 decimals. No required extensions. Use only when neither template above fits. This tutorial uses `stablecoin`. The required extensions are what a US permitted payment stablecoin issuer needs to satisfy the GENIUS Act's technological-capability mandate covered in Section 5. ### 2. Set the compliance posture Four boolean flags on token creation determine the posture you deploy with: - **`isFreezable`**. Set to `true`. If `false` at create time, the token deploys with no freeze authority and freezing becomes impossible afterward. - **`isMintable`**. Set to `true` for an active stablecoin. You will mint as new fiat reserves arrive at the custody bank. - **`requiresAllowlist`**. Turns on Token Access Control's allowlist mode — only listed addresses may receive the token. Leave off for a payment stablecoin: the institutional pattern is screen-before-transfer, not gate-by-allowlist. The `tokenized-security` template flips this on automatically. - **`maxSupply`**. Optional cap in UI units (decimal string). Useful for pilot programs with a hard ceiling. Omit for uncapped supply. ### 3. Plan the authorities A regulated stablecoin has four on-chain authorities, each a distinct role on the mint. At deploy, SDP wires all four to your custody signer by default: - **`mint`**. Authorizes new supply. - **`freeze`**. Freezes and unfreezes individual token accounts. - **`permanentDelegate`**. Moves or burns any holder's tokens without their signature. The technological capability the GENIUS Act requires for lawful orders. - **`metadata`**. Updates on-chain token metadata. A fifth, the **pause authority**, lives on the `pausable` extension configuration. SDP defaults it to the `mint` authority if you don't set it explicitly. You can delegate or revoke any of these later via `POST /v1/issuance/tokens/{id}/authority`. Default-everything-to-custody is the institutional baseline. ### 4. Capture the design as a config Section 4 takes one input: the configuration object representing the design choices above. For the Treasury Pilot USD example this tutorial uses end-to-end: ```javascript title="token-config.js" const tokenConfig = { template: "stablecoin", name: "Treasury Pilot USD", symbol: "TPUSD", decimals: 6, signingCustodyWalletId, description: "GENIUS-compliant treasury pilot stablecoin.", maxSupply: "100000000", isFreezable: true, isMintable: true, }; ``` With your design captured, you're ready to make the first state-mutating call. ## 4. Create and deploy Two API calls take a design from object literal to a deployed regulated stablecoin on Solana: one to create the record, one to deploy on-chain. Both run from the Developer key (`sdp`). ### 1. Extend the client for Idempotency-Key Every state-mutating execute endpoint in SDP (deploy, mint, burn, seize, force-burn, authority) supports an `Idempotency-Key` HTTP header. A retry with the same key replays the original response. A retry with a different key is a new request. The header is how you make network-level retries safe under partial failure. Replace the `SdpClient` definition from Section 2 with this version: ```javascript title="sdp-client.js" class SdpClient { constructor(apiKey, baseUrl = BASE) { this.apiKey = apiKey; this.baseUrl = baseUrl; } async request(method, path, body, { idempotencyKey } = {}) { const headers = { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json", }; if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey; const res = await fetch(`${this.baseUrl}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined, }); const payload = await res.json(); if (!res.ok) throw new Error(`${payload.error.code}: ${payload.error.message}`); return payload; } get(path) { return this.request("GET", path); } post(path, body, opts) { return this.request("POST", path, body, opts); } } const sdp = new SdpClient(process.env.SDP_API_KEY); const sdpAdmin = new SdpClient(process.env.SDP_ADMIN_API_KEY); const idempotencyKey = (op) => `${op}-${crypto.randomUUID()}`; ``` Generate one key per logical operation, not per retry. Create the key once before the first attempt, store it in a variable, and reuse that same value for every retry of the same call. ### 2. Create the token record The first call writes the token's metadata to SDP's database. Nothing happens on Solana yet. The token starts in `status: "pending"` with no `mintAddress`: ```javascript title="create-token.js" const created = await sdp.post("/v1/issuance/tokens", tokenConfig); const tokenId = created.data.token.id; ``` A successful response: ```json title="response.json" { "data": { "token": { "id": "tok_e152fd38-...", "template": "stablecoin", "symbol": "TPUSD", "decimals": 6, "status": "pending", "signingCustodyWalletId": "cwlt_123", "mintAddress": null, "isFreezable": true, "isMintable": true, "requiresAllowlist": false, "maxSupply": "100000000" } } } ``` Save the `tok_*` ID; the deploy call uses it. ### 3. Deploy on-chain The deploy call submits a Token-2022 mint creation transaction to Solana via the custody signer, and updates the token record with the resulting on-chain addresses: ```javascript title="deploy-token.js" const deployKey = idempotencyKey("deploy"); const deployed = await sdp.post( `/v1/issuance/tokens/${tokenId}/deploy`, { signingCustodyWalletId }, { idempotencyKey: deployKey } ); const mintAddress = deployed.data.token.mintAddress; ``` A successful response carries the deployed token record with `status: "active"` and the on-chain `mintAddress` populated: ```json title="response.json" { "data": { "token": { "id": "tok_e152fd38-...", "status": "active", "signingCustodyWalletId": "cwlt_123", "mintAddress": "6V5bTuMsmXyhdY2Hj6VWQZsuacugACBmPqNtQgGtar8L", "mintAuthority": "ASy986dzvVqHHV2Ftfe9ZKshKZhEEQwuAoPoKMc64A9X", "freezeAuthority": "ASy986dzvVqHHV2Ftfe9ZKshKZhEEQwuAoPoKMc64A9X", "metadataAuthority": "ASy986dzvVqHHV2Ftfe9ZKshKZhEEQwuAoPoKMc64A9X", "extensions": { "defaultAccountState": "initialized", "permanentDelegate": "ASy986dzvVqHHV2Ftfe9ZKshKZhEEQwuAoPoKMc64A9X" }, "deployedAt": "2026-05-18T14:50:43.329Z" } } } ``` The `mintAuthority`, `freezeAuthority`, and `metadataAuthority` all resolve to the public key of the exact `signingCustodyWalletId` supplied above. The `permanentDelegate` is set at the same time and appears on the token record's `extensions.permanentDelegate` field rather than as a top-level authority. After deployment, the token's `signingCustodyWalletId` is historical attribution, not a preferred signer or fallback; authority-gated operations are checked against their live on-chain authority, and a standard burn uses its exact source wallet. Deploy can return a bare `500 INTERNAL_ERROR` with no `details` field. The most common cause on devnet is an unfunded custody wallet (Section 2 covered the airdrop fix), but any downstream Solana RPC failure surfaces this way. Fund the custody wallet via the faucet and re-run `/deploy` with the same `Idempotency-Key` to replay, or a fresh key for a new attempt. SDP treats different keys as different requests; the same key with the same params replays the prior result. ### 4. Confirm the deploy Before moving to Section 5, confirm the on-chain state matches the record: ```javascript title="confirm-deploy.js" const { data } = await sdp.get(`/v1/issuance/tokens/${tokenId}`); console.log(`Status: ${data.token.status}`); console.log(`Mint: ${data.token.mintAddress}`); ``` A working deploy prints: ```text title="Output" Status: active Mint: 6V5bTuMsmXyhdY2Hj6VWQZsuacugACBmPqNtQgGtar8L ``` The same state surfaces in the SDP dashboard's issuance view: ![Deployed token shown in the SDP dashboard's issuance page, with status active and the mint address surfaced.](/issue-a-regulated-stablecoin/02-deployed-token-dashboard.png) With the token deployed, Section 5 configures the compliance controls the GENIUS Act requires before any tokens move. ## 5. Configure compliance controls Every other section of this tutorial is engineering. This one is the regulatory backbone. Section 4(a)(5) of the GENIUS Act treats a permitted payment stablecoin issuer as a financial institution for purposes of the Bank Secrecy Act. That single sentence is what makes everything below load-bearing. The Act also requires you to have "[the technological capability to comply with all lawful orders to seize, freeze, burn or prevent the transfer of outstanding stablecoins](https://www.skadden.com/insights/publications/2025/07/us-establishes-first-federal-regulatory-framework)." The endpoints in this section map directly onto that statutory phrase, in the order you'll encounter them: screening first, freeze for per-account blocks, seize for compliance recovery, force-burn as the final remedy. The issuance selectors below reuse the exact `signingCustodyWalletId` from Section 2. SDP verifies that it controls the live freeze or permanent-delegate authority before signing. ### Screen before you move Every transfer destination and every allowlist entry gets screened before your application acts on it. One API call, one response. ```javascript title="screen-address.js" const response = await sdpAdmin.post("/v1/compliance/address-screenings", { address: destinationAddress, network: "solana", intent: "transfer_destination", }); for (const provider of response.data.screening.providers) { if (provider.status === "error" || provider.riskScore > THRESHOLD) { throw new TransferBlocked(provider); } } ``` SDP integrates with four screening providers: Range, Elliptic, TRM, and Chainalysis. One call fans out to all configured providers and returns a per-provider array. The pattern to write against is provider-agnostic: iterate over `providers[]`, fail closed on any `status: "error"` or threshold breach. The institutional reality is that a single provider can be unavailable for credential or rate-limit reasons; a fail-closed policy keeps you defensible even when one feed degrades. On the sandbox response this tutorial cites, Range and Elliptic return numeric scores, TRM returns a healthy status without scoring a fresh address, and Chainalysis returns an upstream credential error. Treat each provider's verdict as one signal; your policy should not depend on any single provider's availability. ### Freeze when a specific account needs to stop Freezing halts one holder's account without touching any other holder. The freeze is on-chain, enforced by the Token-2022 program. SDP signs the freeze instruction with the freeze authority that was set at deploy; the on-chain program does the enforcement. ```javascript title="freeze-account.js" const holderFreezeKey = idempotencyKey("freeze"); await sdpAdmin.post( `/v1/issuance/tokens/${tokenId}/freeze`, { signingCustodyWalletId, accountAddress: holderAddress, reason: "Court order #2026-IL-0142", }, { idempotencyKey: holderFreezeKey } ); ``` You pass the holder's wallet address; SDP derives the [Associated Token Account](https://solana.com/docs/tokens/basics/create-token-account) (the on-chain account that holds that holder's TPUSD balance) and freezes it. Once frozen, transfers to or from that account fail at the program level, not at SDP's gateway. Section 7 demonstrates the resulting error: an HTTP 502 `SOLANA_RPC_ERROR` from SDP that wraps the Token-2022 program error `custom program error: 0x11`, which is the program's `AccountFrozen` signal. The compliance verdict is in the wrapped message; the outer SDP code is upstream-call diagnostic. ### Seize for compliance recovery Seize moves tokens out of a holder's account without their signature, via the **permanent delegate** authority set at deploy. The permanent delegate is a Token-2022 extension that grants a named authority the right to transfer or burn any holder's tokens regardless of holder signature. This is the technological capability the GENIUS Act requires you to have ready for lawful orders. The institutional use case is a court-ordered transfer or a sanctions matter where you must recover specific tokens to a controlled wallet. ```javascript title="seize-tokens.js" const seizeKey = idempotencyKey("seize"); await sdpAdmin.post( `/v1/issuance/tokens/${tokenId}/seize`, { signingCustodyWalletId, seize: { source: holderAddress, destination: custodyAddress, amount: "50000.00", memo: "OFAC matter 2026-04-018", }, }, { idempotencyKey: seizeKey } ); ``` The memo persists on the transaction record. It's the audit attribution your compliance officer reads months later when she reconciles the action against the court order or sanctions notice that authorized it. ### Force-burn as the final remedy Force-burn destroys tokens at a holder's account. Same authority as seize, different effect. Reach for it only when the tokens cannot be recovered to a controlled wallet, for example a self-custodied address whose key you cannot obtain. ```javascript title="force-burn.js" const forceBurnKey = idempotencyKey("force-burn"); await sdpAdmin.post( `/v1/issuance/tokens/${tokenId}/force-burn`, { signingCustodyWalletId, forceBurn: { source: holderAddress, amount: "12000.00", memo: "regulator-ordered burn 2026-04-021", }, }, { idempotencyKey: forceBurnKey } ); ``` ### The institutional pattern that holds this together Every endpoint in this section requires the `tokens:admin` permission. That is by design. Keep two project-scoped API keys: - **Developer key**: routine create, mint, and burn calls. - **Admin key**: screen, freeze, seize, and force-burn. The compliance officer reviewing this design sees a clean separation: routine issuance cannot escalate into compliance actions without a deliberate credential change. Every call writes an entry through SDP's audit service. That trail is the artifact a regulator will eventually ask for, and it is not opt-in. ## 6. Mint the first tokens Minting is how new supply enters circulation. For a regulated stablecoin, every mint corresponds to a fiat reserve event: dollars arriving at the custody bank, attested by the reserve custodian, and recorded against an issuance ticket. The mint call is small. What you put in the memo field is what your compliance officer reconciles against the reserve report. ### 1. Send the mint call Minting requires the `tokens:write` permission, so the Developer key (`sdp`) is the right credential: ```javascript title="mint-tokens.js" const mintKey = idempotencyKey("mint"); const minted = await sdp.post( `/v1/issuance/tokens/${tokenId}/mint`, { signingCustodyWalletId, mint: { destination: custodyAddress, amount: "1000000", memo: "Reserve mint Q2-2026, reference: ANB-RESV-2026-04-091", }, }, { idempotencyKey: mintKey } ); ``` Four field rules to keep clear: - **`signingCustodyWalletId`** is the exact SDP Wallet `id` from Section 2. SDP verifies that this wallet controls the current mint authority; the historical deployment wallet is not used as a fallback. - **`destination`** is a Solana base58 address. For the initial issuer-holds-supply pattern, this is your custody wallet's public key. For subsequent mints to client wallets, it's the recipient's wallet. - **`amount`** is a decimal string in UI units (post-decimals). Not a BigInt, not a number. `"1000000"` mints one million TPUSD because the token has 6 decimals. - **`memo`** is up to 100 characters. SDP stores it on the transaction record. This is the institutional audit attribution: the reference your compliance officer will use months later when reconciling the mint against the reserve custodian's monthly report. ### 2. Read the response A successful mint returns the transaction record and the token account that received the tokens: ```json title="response.json" { "data": { "transaction": { "id": "ttx_88240bf5-...", "type": "mint", "status": "confirmed", "signature": "51XZb4Tqm1G4LmSPUwnLrxENWmmhW9UE15LhxrwMrS6vKL4avFzKBBBcPNEQuiMEVgxWRgbdjJERo6K4taQEM9XG", "params": { "destination": "ASy986dzvVqHHV2Ftfe9ZKshKZhEEQwuAoPoKMc64A9X", "amount": "1000000", "memo": "Reserve mint Q2-2026, reference: ANB-RESV-2026-04-091", "tokenAccount": "NLcEWVJ8oiHdbf6xuSLSAfKfJ1Gqcw4ksYHnkCyEy3t" } }, "tokenAccount": "NLcEWVJ8oiHdbf6xuSLSAfKfJ1Gqcw4ksYHnkCyEy3t" } } ``` The `tokenAccount` is the Associated Token Account that holds the newly minted tokens. SDP creates it on first mint to that destination if it doesn't already exist; subsequent mints to the same destination reuse it. ### 3. The reserve attestation tie-in The GENIUS Act requires a permitted payment stablecoin issuer to publish a monthly public attestation of reserve composition, signed by a registered public accounting firm, with separate CEO/CFO certification. The mint memo is what closes the loop on the engineering side of that obligation. Every mint memo should carry, at minimum, a reserve reference number that maps to a specific reserve event in the custodian's records. Two minimum-viable patterns: - **Single-deposit mint**: `"ANB-RESV-2026-04-091"`. The deposit reference the custody bank assigned when the fiat reserves were received. - **Batched mint against a reserve commitment**: `"Q2-2026 commitment tranche 3 of 6"`. A pre-arranged reserve commitment broken into scheduled releases. The pattern that does not work is mint memos that don't reconcile cleanly to a reserve record. The compliance officer reading the attestation report needs a one-to-one mapping; the auditor signing the report depends on it. ### 4. Confirm the supply ```javascript title="confirm-supply.js" const { data } = await sdp.get(`/v1/issuance/tokens/${tokenId}`); console.log(`Total supply: ${data.token.totalSupply}`); ``` Prints: ```text title="Output" Total supply: 1000000 ``` With tokens in circulation, Section 7 puts the compliance controls from Section 5 to a live test. ## 7. Test a transfer and a compliance action Section 5's compliance controls only matter if they enforce. This section moves real tokens between two wallets, then puts the freeze authority to a live test. The block is enforced on-chain by Token-2022, not by SDP's gateway. That distinction is the institutionally important detail and the reason the error envelope reads the way it does. ### 1. The clean transfer Transfers run on the `/v1/payments/transfers` endpoint family, separate from the issuance family that Section 4 used. The required permissions are `payments:write` and `wallets:read`, both of which the Developer key (`sdp`) carries: ```javascript title="transfer-tokens.js" const sourceCustodyWalletId = signingCustodyWalletId; const screening = await sdpAdmin.post("/v1/compliance/address-screenings", { address: destinationAddress, network: "solana", intent: "transfer_destination", }); for (const provider of screening.data.screening.providers) { if (provider.status === "error" || provider.riskScore > THRESHOLD) { throw new TransferBlocked(provider); } } const transfer = await sdp.post( "/v1/payments/transfers", { sourceCustodyWalletId, destination: destinationAddress, token: mintAddress, amount: "100", memo: "Treasury Pilot internal transfer #042", }, { idempotencyKey: idempotencyKey("transfer") } ); ``` Three field rules to keep clear: - **`sourceCustodyWalletId`** is the exact SDP Wallet `id` (`cwlt_*`) returned by `GET /v1/wallets`. It is not the wallet's public key or provider-specific `walletId`. - **`token`** is the on-chain mint address you got from Section 4's deploy response. It is not the SDP token ID (`tok_*`). - **`destination`** is any valid Solana base58 address. It does not have to be a wallet SDP knows about. A successful response includes the on-chain signature once the transaction confirms: ```json title="response.json" { "data": { "transfer": { "id": "xfr_f570debc-...", "type": "transfer", "direction": "outbound", "status": "confirmed", "custodyWalletId": "cwlt_123", "providerWalletId": "privy_jfxdbsq4bg6dxzsd7imgon3j", "signature": "ekNe4HK67AXptsx6krYATZd5xYeq8wYQShmN9oHUdAipVJVFELGwiLhdJNCAbyN2TPv9SwzBTNz6igoAfWTdCYP", "source": "ASy986dzvVqHHV2Ftfe9ZKshKZhEEQwuAoPoKMc64A9X", "destination": "2H2w3hSxAPTybuTL7LkhPbyk6bWYi94x2J4aqBhhyJL2", "token": "6V5bTuMsmXyhdY2Hj6VWQZsuacugACBmPqNtQgGtar8L", "amount": "100", "memo": "Treasury Pilot internal transfer #042" } } } ``` ### 2. Freeze the destination To demonstrate the on-chain block, freeze the destination's TPUSD account using the Admin key. Freeze requires `tokens:admin`, which the Developer key does not carry: ```javascript title="freeze-destination.js" const transferFreezeKey = idempotencyKey("freeze"); await sdpAdmin.post( `/v1/issuance/tokens/${tokenId}/freeze`, { signingCustodyWalletId, accountAddress: destinationAddress, reason: "Compliance hold pending OFAC review 2026-04-019", }, { idempotencyKey: transferFreezeKey } ); ``` The freeze records a frozen account in SDP's audit trail and applies the Token-2022 freeze instruction to the destination's Associated Token Account on-chain. The on-chain side is verifiable on Solana Explorer: ![Freeze transaction on Solana Explorer in devnet, showing the Token-2022 FreezeAccount instruction applied to the destination's associated token account.](/issue-a-regulated-stablecoin/03-freeze-on-chain-explorer.png) ### 3. Attempt the transfer again Run the same transfer call. This time it fails: ```javascript title="transfer-blocked.js" try { await sdp.post( "/v1/payments/transfers", { sourceCustodyWalletId, destination: destinationAddress, token: mintAddress, amount: "100", memo: "Compliance test, expecting block", }, { idempotencyKey: idempotencyKey("blocked-transfer") } ); } catch (error) { // handle the on-chain block (see step 4) } ``` The error envelope SDP returns: ```json title="response.json" { "error": { "code": "SOLANA_RPC_ERROR", "message": "Failed to sign and send transaction: RPC Error -32000: Invalid transaction: Transaction simulation failed: Error processing Instruction 1: custom program error: 0x11" } } ``` The outer error code is `SOLANA_RPC_ERROR`, but it is upstream-call diagnostic, not the compliance verdict. The actual reason lives in the wrapped message: `custom program error: 0x11`. That is Token-2022's `AccountFrozen` signal. The block was enforced on-chain by the program, not by SDP's gateway. ### 4. Parse the wrapped error The institutional pattern is to write your error handler against the wrapped on-chain code rather than the outer SDP code. The outer code tells you something went wrong upstream; the wrapped code tells you what. ```javascript title="parse-error.js" function isOnChainCompliance(error) { return error.message?.includes("custom program error: 0x11"); } try { // transfer attempt } catch (error) { if (isOnChainCompliance(error)) { // log to the compliance system, do not retry return reportToCompliance(error); } // anything else: treat as a transient RPC failure and apply your retry policy throw error; } ``` Token-2022's `0x11` is `AccountFrozen` (decimal 17). Treat it as a compliance verdict, not a retry-able failure. The same pattern (outer code is the family of failure, wrapped code is the actual reason) applies whenever SDP wraps a downstream Solana RPC error. ### 5. Cleanup For a production environment, unfreezing happens only after the underlying compliance review resolves. For sandbox testing, you can unfreeze immediately: ```javascript title="unfreeze-account.js" await sdpAdmin.post(`/v1/issuance/tokens/${tokenId}/unfreeze`, { signingCustodyWalletId, accountAddress: destinationAddress, }); ``` With the freeze cleared, the destination can receive transfers again. Section 8 covers the production transition: what changes between `sk_test_` and `sk_live_`, and which checks your compliance officer signs against before this same code runs on mainnet. ## 8. Production transition checklist The work between `sk_test_` and `sk_live_` is not a flag flip. It is the moment your compliance officer signs against the runbook your engineering team has been building since Section 2. Going to production means three things change mechanically and a longer list of things your institution attests to. ### 1. What changes mechanically Your sandbox setup is built around six artifacts that all swap for production equivalents: - **API keys**. Replace your project-scoped Developer and Admin keys with `sk_live_` keys minted from your production project. Use the Section 2 bootstrap pattern with a fresh Admin key, selecting the `default-production` project from `GET /v1/projects` instead of `default-sandbox`, and fresh project-scoped keys minted under it. - **Custody provider**. Production reads `provider` as your federally chartered custodian (Anchorage Digital Bank for most US institutional issuers), bound to your organization through the Anchorage onboarding flow. The Privy sandbox wallets do not carry over. - **Solana network**. Mainnet-beta replaces devnet. The custody address gets funded by your custody bank as part of standard operational support, not by a faucet. - **API base URL**. `https://api.solana.com` is the canonical production base. - **Screening providers**. Confirm the production provider mix matches your compliance team's policy. Run a `POST /v1/compliance/address-screenings` against a known address to verify each provider returns `status: "ok"` before live traffic. - **Idempotency-Key strategy**. In production, generate idempotency keys deterministically per operation (for example, `mint-${reserveDepositReference}`) rather than purely random per attempt. This lets your runbook resend safely without minting twice. ### 2. What your compliance officer signs off on Before any `sk_live_` key sees real traffic, the compliance officer needs evidence that the institutional posture from Section 1 is operational, not aspirational. The minimum sign-off list: - **GENIUS Section 4(a)(5) capabilities demonstrated**. Screening, freeze, seize, and force-burn each exercised on sandbox with a captured response in the audit trail. - **BSA/AML program documented**. Written risk-based policy with internal controls, ongoing customer due diligence, independent testing, and a US-located AML/CFT compliance officer named (proposed 31 CFR § 1033.210 under the FinCEN/OFAC implementing rules). - **Sanctions program ready**. OFAC screening provider configured and tested, technical capability to block transactions in place, secondary-market screening procedure defined. - **Reserve attestation cadence agreed**. Registered public accounting firm engaged, monthly examination schedule signed, CEO/CFO certification process defined. - **Audit trail review**. SDP's audit-service entries reviewed for completeness; mint memos from Section 6 reconcile cleanly to reserve references in the custody bank's records. - **Incident response runbook**. The operational playbook for a freeze, seize, or force-burn order. Court order arrives, who signs in to what credential, how long the chain of custody runs, where the documentation lands. ### 3. The cutover When the sign-off is in, the cutover itself is small. Repeat the Section 2 bootstrap with `environment: "production"`, mint your production keys, store them with your other live secrets, and update your runtime environment. Run the Section 2 wallet verification call against the production API base to confirm the wallets are bound. The first production mint corresponds to a real fiat reserve event at your custody bank. ## 9. What's next You have a deployed regulated stablecoin, the compliance controls configured, and a production transition checklist your compliance officer can sign against. What you have built is the issuance side of the institutional product. Two adjacent tutorials carry forward from here. The design and issuance pattern for a tokenized money market fund or treasury bond product under the `tokenized-security` template. Compliance posture differs from the stablecoin pattern in three ways: allowlist on by default, accounts open frozen, and an additional `scaledUiAmount` extension for accrued yield. Most of the Section 5 controls work identically; the differences sit in Sections 3 and 4. Picks up after Section 7. Covers transfer rails for institutional payment flows: bulk transfers, scheduled payments, on-ramp and off-ramp integration with custody bank settlement, and the policy-engine patterns that gate transfers at scale. ### Reference docs For deeper API surface beyond what this tutorial covers: - [SDP API reference](https://platform.solana.com/docs/reference/api). The generated endpoint inventory across all public API families. - [SDP Postman collection](https://platform.solana.com/docs/reference/postman-collection). Importable request bundle for the full API. - [Token-2022 program documentation](https://solana.com/docs/tokens/extensions). The on-chain program SDP wraps. - [GENIUS Act analysis](https://www.gibsondunn.com/the-genius-act-a-new-era-of-stablecoin-regulation/). Statutory interpretation covering each subsection of the Act. - [FinCEN/OFAC implementing rules NPRM](https://www.federalregister.gov/documents/2026/04/10/2026-06963/permitted-payment-stablecoin-issuer-anti-money-launderingcountering-the-financing-of-terrorism). The current implementing-rule source for the BSA designation in GENIUS Section 4(a)(5). --- ### End-to-end Payment Flow Source: https://platform.solana.com/docs/tutorials/end-to-end-payment-flow > A high-level payment flow from quote to settlement. This walkthrough covers a typical payment integration lifecycle: 1. Create or resolve the funding wallet. 2. Build and validate transfer intent (amount, destination, token). 3. Submit the transfer — SDP builds the transaction, signs with the source custody wallet, and submits it. 4. Capture the returned identifiers for observability. 5. Query transfer status until finalization. 6. Reconcile with your product ledger and user-facing state. Use the [Payments API reference](/docs/reference/api/payments) and [Wallets API reference](/docs/reference/api/wallets) for endpoint-level request and response details. --- ## Self-Hosting ### Overview 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, web dashboard, docs, database, and cache — on infrastructure you control, from prebuilt container images. You bring your own authentication, Solana RPC, and signing provider; SDP stays the same product you'd use as a managed service. This section covers installing the stack, generating its configuration, and keeping it running. ## What you run `infra/self-hosted/compose.yml` starts six services: | Service | Image | Purpose | |---|---|---| | `sdp-api` | `sdp-api` | The core API (default port `8787`). | | `sdp-web` | `sdp-web` | The dashboard web app (default port `3000`). | | `sdp-docs` | `sdp-docs` | This documentation site (default port `3001`). | | `sdp-migrate` | `sdp-api` | Runs database migrations once on startup, then exits. | | `postgres` | `postgres:16-alpine` | Bundled database (or point at an external one). | | `redis` | `redis:7-alpine` | Bundled cache (or point at an external one). | ## Start here - **[Quickstart](/docs/self-hosting/quickstart)** — install the stack and bring it up for the first time. - **[First devnet deployment](/docs/self-hosting/first-devnet-deployment)** — log into the dashboard, initialize a local wallet, and verify a wallet-scoped API key. - **[Configurator](/docs/self-hosting/configurator)** — generate a complete `.env` in your browser or terminal. - **[Environment reference](/docs/self-hosting/env-reference)** — what every configuration variable does and which ones are required. ## Operate - **[Upgrade & 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 automation alongside the stack. ## Requirements - A 64-bit Linux or macOS host with **Docker Engine** and **Docker Compose v2**. - Outbound network access to pull images and reach your Solana RPC and authentication providers. - A [Clerk](https://clerk.com) application for authentication and a Solana RPC endpoint. See the [environment reference](/docs/self-hosting/env-reference) for the full list. --- ### 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. This guide takes you from an empty host to a running Solana Developer Platform. It assumes **Docker Engine** and **Docker Compose v2** are installed and the Docker daemon is running. ## 1. Install The install script downloads `compose.yml` and `.env.example` for the latest release into `~/sdp` and verifies them against the release checksums. ```bash title="Terminal" curl -fsSL https://github.com/solana-foundation/solana-developer-platform/releases/latest/download/install.sh | bash ``` The script is open source — you can read it before running it, and verify the checksums and signature out of band. The commented header of `install.sh` documents the full verified-install flow and the environment variables you can override (`SDP_INSTALL_DIR`, `INSTALL_VERSION`, and others). Prefer to do it by hand? Download `compose.yml` and `.env.example` from the [latest release](https://github.com/solana-foundation/solana-developer-platform/releases/latest) into a working directory instead. ## 2. Generate your `.env` The stack reads its configuration from a `.env` file next to `compose.yml`. The [configurator](/docs/self-hosting/configurator) fills it in for you, generates the app secrets, and validates your answers. - **In your browser** — open the [configurator](/docs/self-hosting/configurator), answer the prompts, and download the `.env` into `~/sdp`. - **In your terminal** — run the configurator from the API image: ```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 ``` Use the same release tag you installed in place of `latest` so the configurator matches your `compose.yml`. See the [environment reference](/docs/self-hosting/env-reference) for what each variable means and which are required. ## 3. Bring up the stack ```bash title="Terminal" cd ~/sdp docker compose up -d ``` On startup, `sdp-migrate` runs the database migrations once, then the API, web, and docs services start. The first run pulls the images, so it may take a few minutes. ## 4. Verify ```bash title="Terminal" docker compose ps docker compose logs -f sdp-api ``` When the services report healthy, the dashboard is at `http://localhost:3000`, the API at `http://localhost:8787`, and these docs at `http://localhost:3001`. The API exposes a [health endpoint](/docs/reference/api/health) you can poll from a load balancer or uptime check. 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) — log into the dashboard, initialize a local wallet, and verify API access. - [Upgrade & 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, log into the dashboard, initialize a local wallet, and verify API access. This tutorial takes a fresh self-hosted install from healthy containers to the first useful devnet workflow: dashboard login, local wallet initialization, wallet-scoped API key creation, and an authenticated API call. Use this after the [Quickstart](/docs/self-hosting/quickstart). The path here is devnet-only and uses the default local signer. Provider-specific signing setup belongs in a later hardening pass. The public Solana devnet RPC endpoint is useful for smoke tests, but it can be rate-limited or incomplete for balance-heavy wallet views. Use a dedicated devnet RPC endpoint from your provider before treating this flow as reliable. ## Confirm the stack is healthy From the directory where the installer placed `compose.yml` and `.env`: ```bash title="Terminal" cd ~/sdp docker compose up -d docker compose ps curl http://localhost:8787/health ``` The health call should return a JSON response with `status: "ok"`. The dashboard is at `http://localhost:3000`, the API is at `http://localhost:8787`, and the self-hosted docs are at `http://localhost:3001`. ## Configure Clerk for dashboard login In your Clerk development app: - Copy the publishable key, secret key, and issuer into the [configurator](/docs/self-hosting/configurator). - Customize the session token (Clerk dashboard → **Sessions** → **Customize session token**) with the claims from the [Clerk setup guide](https://github.com/solana-foundation/solana-developer-platform/blob/main/apps/sdp-api/docs/self-hosting/clerk-setup.md). - Expose your local API with the webhook tunnel: reserve a free stable domain at [dashboard.ngrok.com/domains](https://dashboard.ngrok.com/domains), set it as `WEBHOOK_INGEST_DOMAIN` in `apps/sdp-api/.env.local`, and run `pnpm dev:webhooks`. - Create a webhook endpoint that points at `https:///webhooks/clerk/link-orgs` — Clerk delivers over the public internet and cannot reach `localhost` directly. - Copy the webhook signing secret into `CLERK_WEBHOOK_SECRET`. `CLERK_WEBHOOK_SECRET` is optional at raw env-validation level, but this tutorial requires it because the webhook is what creates the local SDP organization record after dashboard signup. ## Generate the devnet `.env` Open the [configurator](/docs/self-hosting/configurator), keep the defaults for the first deployment, and fill in: - `SOLANA_RPC_URL` with a devnet RPC endpoint. - Clerk publishable key, secret key, issuer, and webhook secret. - Local signing with native fee payment. The signing step generates a local Solana signer in the browser and shows the public key to fund on devnet. Save the generated file as `~/sdp/.env`, then restart the stack: ```bash title="Terminal" cd ~/sdp docker compose up -d ``` Providers that are not configured remain unavailable, so the default devnet path exposes only the local signer. The local signer is a manual provider: after you create your organization, enable it by adding `{"sdp": {"providerOverrides": {"custody": {"local": true}}}}` to the organization's private metadata in the Clerk dashboard. ## Log into the dashboard Open `http://localhost:3000` and sign in through Clerk. Create or join an organization when prompted. After the Clerk webhook lands, the dashboard should show the organization and project context instead of onboarding errors. If the dashboard loads but organization state does not appear, check the API logs: ```bash title="Terminal" docker compose logs -f sdp-api ``` Look for Clerk webhook errors, session token claim mismatches, or missing `CLERK_WEBHOOK_SECRET`. ## Initialize a local custody wallet In the dashboard, open **Wallets**, choose the local provider, and create a wallet named `Devnet local wallet`. If you prefer the API path, use an admin key from the dashboard: ```bash title="Terminal" curl -X POST http://localhost:8787/v1/wallets/initialize \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "provider": "local", "walletLabel": "Devnet local wallet" }' ``` Copy the returned `walletId`. You will bind the API key to this wallet in the next step. ## Create a wallet-scoped API key Open **API keys** in the dashboard, create a sandbox key, and choose **Selected wallets**. Select the local wallet you just created and set it as the key's default wallet. Save the full key when the dashboard shows it. SDP only displays it once. ## Verify the key can read wallets Use the new key against your self-hosted API: ```bash title="Terminal" export SDP_API_KEY="sk_test_..." curl "http://localhost:8787/v1/wallets?view=summary" \ -H "Authorization: Bearer $SDP_API_KEY" ``` The response should include the local wallet you created. At this point you have a self-hosted devnet stack with dashboard auth, local custody signing, and an API key bound to the wallet it is allowed to use. ## Next steps - Use [Environment reference](/docs/self-hosting/env-reference) to move from localhost defaults to real domains, external Postgres, or external Redis. - Use [Upgrade & backup](/docs/self-hosting/upgrade-and-backup) before keeping meaningful operator data in the stack. - Use [External co-signers](/docs/self-hosting/external-co-signers) when a provider requires an additional signing process outside the default compose 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. Use the guided steps below to generate a ready-to-use `.env` for the default devnet self-hosting path. Everything runs in your browser — nothing is sent anywhere. Drop the file next to `compose.yml`, then run `docker compose up -d`. For the full operator walkthrough after download, continue with [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 self-hosted stack reads its configuration from a single `.env` file next to `compose.yml`. The [configurator](/docs/self-hosting/configurator) is the authoritative source of the field list, defaults, and validation — it always matches the release you installed. This page explains how the configuration is organized and the variables you are most likely to set by hand. The configurator groups variables into sections: | Section | What it covers | |---|---| | Basic | Core runtime — environment, deployment mode, Transactional Email sender and Resend key. | | Database | Bundled Postgres or an external database. | | Cache | Bundled Redis or an external cache. | | Solana RPC | Network and RPC endpoint. | | Authentication (Clerk) | Required Clerk keys and JWT settings. | | Signing provider | Which signer(s) to enable and their credentials. | | Fee payment | How transaction fees are paid. | | Secrets | App secrets, generated locally. | | Advanced | Image source, ports, and internal service URLs. | ## Required variables These must be set or the stack will refuse to start: | Variable | Description | |---|---| | `POSTGRES_PASSWORD` | Password for the bundled Postgres. Always required — the bundled `postgres` service starts even when the API points at an external `DATABASE_URL`. | | `API_KEY_PEPPER` | Server-side pepper for API key hashing. Generate with `openssl rand -hex 32`. | | `CUSTODY_ENCRYPTION_KEY` | Encryption key for custody material at rest. Generate with `openssl rand -base64 32`. | | `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | Clerk publishable key for the web app. | | `CLERK_SECRET_KEY` | Clerk secret key for the API. | | `CLERK_ISSUER` | Clerk issuer URL used to validate tokens. | | `SOLANA_RPC_URL` | Solana RPC endpoint the API calls. | | `SIGNING_PROVIDER` | The default signing provider. Each non-local provider adds its own required credentials. | The configurator generates `API_KEY_PEPPER` and `CUSTODY_ENCRYPTION_KEY` for you and can auto-generate `POSTGRES_PASSWORD`. ## Database In the configurator, `DATABASE_MODE` chooses between the bundled Postgres and an external database. It is a generation-time selector and is not written to `.env`. For the bundled database, set `POSTGRES_DB`, `POSTGRES_USER`, and `POSTGRES_PASSWORD`; the API and migration services build their connection string from them. To use an external database, set `DATABASE_URL` to your own `postgresql://…` string and the API and migrations connect there instead. Note that `compose.yml` always starts the bundled `postgres` container — with an external `DATABASE_URL` it goes unused, and `POSTGRES_PASSWORD` is still required for it to start. The configurator hides `POSTGRES_PASSWORD` once you select an external database but still emits an auto-generated value for it, since the bundled `postgres` container needs one to start even when nothing connects to it. ## Cache Like `DATABASE_MODE`, `CACHE_MODE` is a configurator-only selector and is not written to `.env`. To use an external cache, set `REDIS_URL` to your own `redis://…` endpoint; the bundled `redis` container still starts but goes unused. ## Signing providers `SIGNING_PROVIDER` is the default signer written to `.env`. Supported values are `local`, `fireblocks`, `privy`, `coinbase_cdp`, `para`, `turnkey`, and `utila`. The configurator also tracks a `SIGNING_PROVIDERS` selection to decide which fields to show, but that key is configurator-only and is not written to `.env`. Each non-local provider requires its own credentials — for example Fireblocks needs an API key, secret, and vault ID — and the configurator only prompts for the providers you enable. For provider-managed co-signer runtimes that live outside the default stack, see [External co-signers](/docs/self-hosting/external-co-signers). ## Advanced | Variable | Default | Description | |---|---|---| | `SDP_IMAGE_REGISTRY` | `ghcr.io/solana-foundation/sdp` | Registry the service images are pulled from. | | `SDP_VERSION` | `latest` | Image tag for every SDP service. Pin this to a release tag 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` | Trust the first `X-Forwarded-For` address outside Cloud Run. Enable only behind an ingress that replaces caller-supplied forwarding headers; direct deployments share the fail-closed unidentified quota bucket. | `SDP_API_BASE_URL` defaults to the in-network service name `http://sdp-api:8787` for server-to-server calls. The browser-facing `NEXT_PUBLIC_*` URLs default to `localhost` (for example `http://localhost:8787` and `http://localhost:3000`) and need changing when you put the services behind your own domains or ingress. --- ### 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. This page covers upgrading to a new release and backing up the data that matters on a self-hosted stack. ## Upgrade A release ships new container images plus an updated `compose.yml`. To upgrade: 1. **Refresh the install files.** Re-run the install script. It replaces `compose.yml` with the new release's version (verified against the release checksums) and leaves any existing `.env` and `.env.example` in place, printing a link to the new release's `.env.example` 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 ``` 2. **Pin the version.** Set `SDP_VERSION` in `.env` to the release tag you are upgrading to (rather than `latest`) so every service runs the same, known build. 3. **Pull and restart.** ```bash title="Terminal" cd ~/sdp docker compose pull docker compose up -d ``` `sdp-migrate` runs any new database migrations once before the API starts. 4. **Verify** the services are healthy, as in the [Quickstart](/docs/self-hosting/quickstart#4-verify). To upgrade to a specific release instead of the latest, set `INSTALL_VERSION` when running the script and the matching `SDP_VERSION` in `.env`. ### Roll back Set `SDP_VERSION` back to the previous tag and run `docker compose up -d`. Note that migrations are not automatically reversed; if a release applied a migration, restore from a backup taken before the upgrade rather than relying on a version downgrade alone. ## Backup Two things hold your state: - **The Postgres database** — all platform data. With the bundled database it lives in the `sdp-postgres-data` Docker volume; with an external `DATABASE_URL`, back it up with your database provider's tooling instead. - **Your `.env`** — secrets and configuration. Store it somewhere safe (a secret manager); it cannot be regenerated identically. Redis is a cache and does not need backing up. ### Back up the database Take a logical dump with `pg_dump` from the running container. Running it through `sh -c` lets `pg_dump` read `POSTGRES_USER` and `POSTGRES_DB` from inside the container, so custom values are honored: ```bash title="Terminal" cd ~/sdp docker compose exec -T postgres \ sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > sdp-backup.sql ``` Keep these dumps off-host and on a schedule that matches your recovery needs. ### Restore the database A plain `pg_dump` includes the full schema and the migration history, so restore into an **empty** database and let the stack start on top of it. Do not restore over an already-migrated database, or the dump's schema collides with the one `sdp-migrate` created on the previous `up`. ```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 already recorded in the restored dump. Restoring a dump taken at the same release leaves it with nothing to do; restoring an older dump before an upgrade lets it apply only the newer migrations. Restore the matching `.env` alongside it so secrets such as `CUSTODY_ENCRYPTION_KEY` line up with the data they protect. For an external `DATABASE_URL`, restore through your database provider rather than 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 self-hosting problems surface at startup. Start by looking at the service state and logs: ```bash title="Terminal" cd ~/sdp docker compose ps docker compose logs -f sdp-api ``` ## Docker is not ready The install script checks for Docker up front. If you see that Docker is not installed, that Compose v2 is unavailable, or that the daemon is unreachable, install or start Docker and retry — see the [Docker Engine](https://docs.docker.com/engine/install/) and [Compose](https://docs.docker.com/compose/install/) docs. ## A required variable is missing `compose.yml` fails fast when a required variable is unset, with a message such as `set POSTGRES_PASSWORD in .env`. Make sure a `.env` exists next to `compose.yml` and fills in every required variable. The [configurator](/docs/self-hosting/configurator) produces a complete file; the [environment reference](/docs/self-hosting/env-reference) lists what is required. ## Images won't pull If `docker compose pull` reports access denied or a missing manifest: - Confirm `SDP_IMAGE_REGISTRY` and `SDP_VERSION` point at a registry and tag you can reach. - Pull a single image directly to see the underlying error, e.g. `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, web, and docs default to ports `8787`, `3000`, and `3001`. If another process holds one of them, the service won't bind. Override the host port in `.env` with `SDP_API_PORT`, `SDP_WEB_PORT`, or `SDP_DOCS_PORT` and run `docker compose up -d` again. ## Migrations failed `sdp-migrate` runs once before the API starts; if it fails, the API stays down. Inspect it with: ```bash title="Terminal" docker compose logs sdp-migrate ``` A failure here is usually a database connection problem — check `POSTGRES_*` (or `DATABASE_URL`) and that the `postgres` service is healthy in `docker compose ps`. ## Database connection refused The API and migration services wait for Postgres to report healthy before starting. If they still can't connect, verify the credentials in `.env`, and for an external database (`DATABASE_URL` set) that it is reachable from inside the Docker network. ## Authentication errors in the dashboard Sign-in problems point at the Clerk configuration. Re-check `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`, `CLERK_SECRET_KEY`, and `CLERK_ISSUER` against your Clerk application, and confirm the keys match the environment (development vs production) you intend to run. ## Still stuck? Capture the failing service's logs and your (secret-redacted) `.env` settings, 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 automation outside the default self-hosted SDP stack. Some signing providers need an operator-managed co-signer or automation process in addition to the SDP API. Run those processes outside the default self-hosted SDP stack. `infra/self-hosted/compose.yml` is intentionally unchanged by default. It starts the SDP API, web app, docs app, database, cache, and migrations. Provider co-signers are separate operational services because they usually have their own secret handling, identity, ingress, and availability requirements. ## Production pattern Use a managed container service such as Google Cloud Run, or the equivalent in your cloud, for provider-specific co-signer runtimes. - Use the provider- or operator-supplied co-signer image and runtime. - Store co-signer secrets in a cloud secret manager, not in the SDP compose file. - Run the co-signer with a dedicated service account that can read only the secrets it needs. - Configure ingress and invoker IAM for the provider flow. Prefer private or authenticated ingress when the provider does not require a public callback. - Set `minScale` to `1` when callback latency or provider availability matters; use scale-to-zero only when cold starts are acceptable. - Keep development, staging, and production co-signers separated 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 readiness Utila is the motivating future provider for this pattern. When Utila signer support lands, the co-signer should be hosted as an external operator service, for example on Cloud Run, while SDP keeps its API, web, docs, database, and cache inside the normal self-hosted stack. This page does not make Utila a live SDP signing provider. Do not set `SIGNING_PROVIDER=utila` until the Utila signer integration explicitly adds that provider option and the required runtime configuration. Operator scaffolding for a Utila Cloud Run deployment lives in `infra/utila/cloud-run/`. That scaffold is intended to host the co-signer endpoint before Utila becomes a default self-hosted service. Operators can deploy their own co-signer image, protect the endpoint with Cloud Run IAM or runtime-level request verification, and pass the resulting service URL and token into a custom SDP Utila integration. --- ## Reference ### Overview Source: https://platform.solana.com/docs/reference > API reference, token types, provider onboarding, and integration resources. Resources for integrating with SDP: API endpoints, tokenization templates, provider onboarding, AI consumption, and the Postman collection. --- ### Tokenization Templates Source: https://platform.solana.com/docs/reference/issuance-token-types > How to choose a token template, and the default controls each one applies. The public issuance surface currently exposes three **Tokenization Templates**: | Template | Defaults | Notes | | --- | --- | --- | | `stablecoin` | 6 decimals, permanent delegate + pausable extensions, default account state initialized | Intended for fiat-backed token patterns with compliance controls. | | `tokenized-security` | 8 decimals, allowlist required, permanent delegate + pausable + scaled UI amount extensions, default account state frozen | Regulated asset flows with stronger defaults. | | `custom` | 9 decimals default (up to 18 max), extension set is fully configurable | Use when your requirements do not match the stablecoin or tokenized-security templates. | These defaults are sourced from the API implementation in `apps/sdp-api/src/services/issuance/templates/definitions.ts`. Each template also sets the token's **Token Access Control** mode — `allowlist`, `blocklist`, or `disabled` — enforced on-chain over who may hold or receive the token. This is distinct from a wallet's own destination allowlist, which is a wallet-policy concern. ## Selecting a template Each template is a vetted, fixed composition of Token-2022 extensions for a specific class of asset, rather than an à-la-carte menu. The appropriate template is determined by the nature of the instrument being issued. The `custom` template applies to assets that do not fit the stablecoin or tokenized-security models. ### Stablecoin Intended for tokens pegged to a fiat currency and redeemable at par (1:1). The template applies the compliance controls typical of regulated fiat tokens: a **pausable** extension (transfers can be frozen in an emergency) and a **permanent delegate** (tokens can be seized or clawed back in response to a compliance event). A blocklist applies by default. Stablecoins cannot pay interest or yield to holders. This reflects payment-stablecoin frameworks such as the U.S. GENIUS Act and the EU's MiCA, which prohibit issuers from paying interest on the token itself. Interest- or yield-bearing products are issued as tokenized securities (typically funds) rather than stablecoins. ### Tokenized security Intended for regulated instruments — equity, debt, or fund shares. The template applies the stablecoin controls together with stronger defaults: an **allowlist is required**, new accounts are **frozen by default** (holders must be approved before transacting), and the **scaled UI amount** extension is added for corporate actions such as splits or share-class rebasing. Interest- and yield-bearing instruments (for example debt or fund tokens) also fall under this template. ### Custom Corresponds to the **Non-Security Digital Asset** classification in the issuance interface — assets that are neither fiat stablecoins nor regulated securities, such as commodities, real estate, or collectibles. The template exposes the full Token-2022 extension set (transfer fees, transfer hooks, non-transferable behaviour, interest, and others) and applies where the stablecoin and tokenized-security templates do not match the instrument. ### Summary | Instrument | Template | | --- | --- | | Pegged to a fiat currency, redeemable 1:1, with no yield | `stablecoin` | | Regulated security (equity, debt, or fund), or an instrument that pays a return | `tokenized-security` | | Any other asset, or one requiring extensions the other templates do not offer | `custom` (Non-Security Digital Asset) | Token-2022 extensions are set when the mint is created and cannot be changed afterward. The template selection is therefore permanent for a given token; changing it requires issuing a new token. This page provides general guidance and is not legal advice; regulatory treatment depends on the jurisdiction and on how the instrument is structured. ## Amount format Mint, burn, seize, and force-burn request bodies accept `amount` as a **decimal string in UI units** — the human-readable token value (for example `"1"` or `"1.5"`). SDP converts to on-chain base units using the token's `decimals`. This differs from wallet balance responses, which return both raw `amount` and `uiAmount`. --- ### 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 currently supports infrastructure provider integrations across custody, RPC, compliance, and ramps. We will add future integration categories as SDP expands. Onboarded providers are presented as available integrations, not endorsed or recommended partners. Institutions using SDP are responsible for their own provider due diligence. ## Start here Select the category that matches your integration, complete the intake form, and review the criteria before opening 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) | | Ramp | [Complete ramp intake](https://solanafoundation.typeform.com/to/sxTGbwXt) | [Download ramp criteria PDF](/provider-onboarding/ramp-criteria.pdf) | If your integration is for compliance or another infrastructure category without a published intake, contact the SDP team before opening 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 category, documentation completeness, and required technical validation. ## Evaluation Each category uses the finalized criteria linked above. Every review starts with hard gates, followed by scored evaluation when applicable. At a high level, 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. - Required regulatory, compliance, or security posture for the category. Providers that do not clear the required gates may be tracked for future support, but should not be presented as available in SDP until gaps are resolved. A provider that clears evaluation becomes available per the deployment's configuration — either generally available to every organization, or gated behind a per-organization Manual Provider Activation when commercial or operational onboarding is still required. ## Access requirements Provide access suitable for repeatable validation, including: - Sandbox API keys or equivalent credentials. - Test accounts, wallets, endpoints, or settlement rails when required. - Supported Solana networks. - Known rate limits, feature flags, and sandbox limitations. - Technical support contact for review. ## Pull request expectations An SDP infrastructure provider contribution usually includes: - Provider metadata and capability registration. - API support for setup, configuration, and category-specific runtime operations. - Dashboard support where applicable. - 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 after intake is complete, or after the SDP team has created a tracking issue while intake is being finalized. --- ### Documentation for AI Source: https://platform.solana.com/docs/reference/docs-for-ai > Public machine-readable entry points and guidance for agents and AI systems consuming SDP docs and APIs. Use this page when you need the public AI-facing entry points for Solana Developer Platform. ## Machine-readable resources - [llms.txt](/docs/ai/llms.txt): Concise discovery file with canonical URLs, supported surfaces, and key starting pages. - [llms-full.txt](/docs/ai/llms-full.txt): Expanded docs map generated from the docs navigation and public API reference. - [OpenAPI](/docs/reference/api): Human-readable API reference generated from the public contract. - [OpenAPI JSON](https://api.solana.com/openapi.json): Machine-readable API contract for the public SDP API. - [API llms.txt](https://api.solana.com/llms.txt): API-only discovery entry point for endpoint families and authentication expectations. - [Postman Collection](/docs/reference/postman-collection): Downloadable public collection generated from the same OpenAPI source. ## Recommended ingestion order 1. Start with [llms.txt](/docs/ai/llms.txt). 2. Expand to [llms-full.txt](/docs/ai/llms-full.txt) when you need broader site coverage. 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 sequencing, operational constraints, and supported patterns. ## Scope - These AI resources are limited to the supported public SDP surface. - Hidden or internal-only route families are intentionally excluded. - Public docs should be treated as the source of truth ahead of internal implementation details. ## Coverage focus - Wallets and custody - API key management - Projects - Token issuance and lifecycle operations - Payments, transfers, and ramps - Compliance screening --- ### Postman Collection Source: https://platform.solana.com/docs/reference/postman-collection > Download the public SDP API Postman collection generated from the OpenAPI contract. The Postman collection is generated from the same public OpenAPI contract that powers the [API reference](/docs/reference/api). It includes every end-user-facing family published there. Internal-only endpoint families such as `rpc`, `admin`, `onboarding`, `auth`, `organizations`, and `members` are intentionally excluded.
Download Postman collection {" · "} Open raw JSON
When you import the collection into Postman, set: - `baseUrl` to the SDP API environment you want to target - `sdpApiKey` to your bearer API key --- ## API ### API Reference Source: https://platform.solana.com/docs/reference/api > Endpoint index from the repository OpenAPI spec.
Download Postman collection {" · "} Open raw JSON
- [Health](/docs/reference/api/health) - [API Keys](/docs/reference/api/api-keys) - [Wallets](/docs/reference/api/wallets) - [Projects](/docs/reference/api/projects) - [Issuance](/docs/reference/api/issuance) - [Payments](/docs/reference/api/payments) - [Policies](/docs/reference/api/policies) - [Compliance](/docs/reference/api/compliance) - [Asset Profiles](/docs/reference/api/asset-profiles) --- ### Health Source: https://platform.solana.com/docs/reference/api/health > Service health and readiness endpoints. | Method | Endpoint | Summary | | --- | --- | --- | | `GET` | `/health` | Health check | | `GET` | `/health/ready` | Readiness check | --- ### API Keys Source: https://platform.solana.com/docs/reference/api/api-keys > API key management endpoints. | Method | Endpoint | Summary | | --- | --- | --- | | `GET` | `/v1/api-keys` | List API keys | | `POST` | `/v1/api-keys` | Create API key | | `DELETE` | `/v1/api-keys/{keyId}` | Deactivate API key | | `GET` | `/v1/api-keys/{keyId}` | Get API key | | `PATCH` | `/v1/api-keys/{keyId}` | Update API key | | `PUT` | `/v1/api-keys/{keyId}/policy-bindings` | Replace or clear API-key policy bindings | | `POST` | `/v1/api-keys/{keyId}/policy-profiles` | Create API-key policy profile | | `POST` | `/v1/api-keys/{keyId}/policy-profiles/{profileId}/revisions` | Create API-key policy revision | | `POST` | `/v1/api-keys/{keyId}/policy-profiles/{profileId}/revisions/{revisionId}/activate` | Activate API-key policy revision | | `POST` | `/v1/api-keys/{keyId}/rotate` | Rotate API key | --- ### Wallets Source: https://platform.solana.com/docs/reference/api/wallets > Wallet signing provider configuration and wallet management. | Method | Endpoint | Summary | | --- | --- | --- | | `DELETE` | `/v1/wallets` | Delete wallet | | `GET` | `/v1/wallets` | List wallets | | `POST` | `/v1/wallets` | Create wallet | | `GET` | `/v1/wallets/{walletId}` | Get wallet by ID | | `PATCH` | `/v1/wallets/{walletId}` | Update wallet | | `GET` | `/v1/wallets/aggregate` | Aggregate wallet balances | | `GET` | `/v1/wallets/approval-requests` | List wallet approval requests | | `GET` | `/v1/wallets/approval-requests/{approvalRequestId}` | Get wallet approval request | | `POST` | `/v1/wallets/approval-requests/{approvalRequestId}/approve` | Approve wallet approval request | | `POST` | `/v1/wallets/approval-requests/{approvalRequestId}/cancel` | Cancel wallet approval request | | `POST` | `/v1/wallets/approval-requests/{approvalRequestId}/reject` | Reject wallet approval request | | `GET` | `/v1/wallets/config` | Get wallet signing config | | `GET` | `/v1/wallets/configs` | List wallet signing configs | | `POST` | `/v1/wallets/default-wallet` | Set default wallet | | `POST` | `/v1/wallets/initialize` | Initialize wallet signing | | `GET` | `/v1/wallets/public-key` | Get wallet public key | | `POST` | `/v1/wallets/signer-check` | Check signer via simulated memo transaction | | `POST` | `/v1/wallets/switch` | Switch wallet signing provider | | `GET` | `/v1/wallets/switch-options` | List switch provider options | --- ### Projects Source: https://platform.solana.com/docs/reference/api/projects > Project and project member management. | Method | Endpoint | Summary | | --- | --- | --- | | `GET` | `/v1/projects` | List projects | | `GET` | `/v1/projects/{projectId}` | Get project | | `PATCH` | `/v1/projects/{projectId}` | Update project | | `GET` | `/v1/projects/{projectId}/api-keys` | List project API keys | | `POST` | `/v1/projects/{projectId}/api-keys` | Create project API key | | `GET` | `/v1/projects/{projectId}/members` | List project members | | `POST` | `/v1/projects/{projectId}/members` | Add project member | | `DELETE` | `/v1/projects/{projectId}/members/{memberId}` | Remove project member | | `PATCH` | `/v1/projects/{projectId}/members/{memberId}` | Update project member | --- ### Issuance Source: https://platform.solana.com/docs/reference/api/issuance > Token issuance, allowlists, and lifecycle operations. | Method | Endpoint | Summary | | --- | --- | --- | | `GET` | `/v1/issuance/templates` | List token templates | | `GET` | `/v1/issuance/templates/{templateId}` | Get token template | | `GET` | `/v1/issuance/tokens` | List tokens | | `POST` | `/v1/issuance/tokens` | Create token | | `GET` | `/v1/issuance/tokens/{tokenId}` | Get token | | `PATCH` | `/v1/issuance/tokens/{tokenId}` | Update token | | `GET` | `/v1/issuance/tokens/{tokenId}/allowlist` | List token allowlist | | `POST` | `/v1/issuance/tokens/{tokenId}/allowlist` | Add token allowlist entry | | `DELETE` | `/v1/issuance/tokens/{tokenId}/allowlist/{entryId}` | Remove token allowlist entry | | `GET` | `/v1/issuance/tokens/{tokenId}/allowlist/labels` | List token allowlist labels | | `GET` | `/v1/issuance/tokens/{tokenId}/audit` | Get asset audit history | | `POST` | `/v1/issuance/tokens/{tokenId}/authority` | Execute authority update | | `POST` | `/v1/issuance/tokens/{tokenId}/authority/prepare` | Prepare authority update | | `POST` | `/v1/issuance/tokens/{tokenId}/burn` | Execute burn | | `POST` | `/v1/issuance/tokens/{tokenId}/burn/prepare` | Prepare burn transaction | | `POST` | `/v1/issuance/tokens/{tokenId}/deploy` | Deploy token | | `POST` | `/v1/issuance/tokens/{tokenId}/deploy/confirm` | Confirm non-custodial deploy | | `POST` | `/v1/issuance/tokens/{tokenId}/deploy/prepare` | Prepare token deploy transaction | | `POST` | `/v1/issuance/tokens/{tokenId}/deploy/prepare-metadata` | Prepare metadata-URI follow-up transaction | | `POST` | `/v1/issuance/tokens/{tokenId}/force-burn` | Execute force burn | | `POST` | `/v1/issuance/tokens/{tokenId}/force-burn/prepare` | Prepare force burn transaction | | `POST` | `/v1/issuance/tokens/{tokenId}/freeze` | Freeze account | | `GET` | `/v1/issuance/tokens/{tokenId}/frozen` | List frozen accounts | | `GET` | `/v1/issuance/tokens/{tokenId}/holders` | List verified holders | | `POST` | `/v1/issuance/tokens/{tokenId}/holders` | Enroll a verified holder | | `GET` | `/v1/issuance/tokens/{tokenId}/metadata.json` | Get public token metadata JSON | | `POST` | `/v1/issuance/tokens/{tokenId}/mint` | Execute mint | | `POST` | `/v1/issuance/tokens/{tokenId}/mint/prepare` | Prepare mint transaction | | `POST` | `/v1/issuance/tokens/{tokenId}/pause` | Pause token transfers | | `POST` | `/v1/issuance/tokens/{tokenId}/seize` | Execute seize | | `POST` | `/v1/issuance/tokens/{tokenId}/seize/prepare` | Prepare seize transaction | | `POST` | `/v1/issuance/tokens/{tokenId}/supply/refresh` | Refresh cached token supply | | `GET` | `/v1/issuance/tokens/{tokenId}/transactions` | List token transactions | | `POST` | `/v1/issuance/tokens/{tokenId}/unfreeze` | Unfreeze account | | `POST` | `/v1/issuance/tokens/{tokenId}/unpause` | Unpause token transfers | | `GET` | `/v1/issuance/tokens/facets` | List token filter facets | | `GET` | `/v1/issuance/transactions` | List issuance transactions | --- ### Payments Source: https://platform.solana.com/docs/reference/api/payments > Wallet balances, transfer execution, policies, and ramps. | Method | Endpoint | Summary | | --- | --- | --- | | `GET` | `/v1/payments/ramps/offramp/currency` | List off-ramp currency support | | `GET` | `/v1/payments/ramps/onramp/currency` | List on-ramp currency support | | `POST` | `/v1/payments/ramps/onramp/quote` | Create on-ramp quote | | `POST` | `/v1/payments/ramps/sandbox/simulate` | Simulate sandbox transfer | | `GET` | `/v1/payments/recurring-payments` | List recurring payments | | `POST` | `/v1/payments/recurring-payments` | Create recurring payment | | `GET` | `/v1/payments/recurring-payments/{id}` | Get recurring payment | | `PATCH` | `/v1/payments/recurring-payments/{id}` | Update recurring payment | | `POST` | `/v1/payments/recurring-payments/{id}/activate` | Activate recurring payment | | `POST` | `/v1/payments/recurring-payments/{id}/cancel` | Cancel recurring payment | | `POST` | `/v1/payments/recurring-payments/{id}/collect` | Collect recurring payment | | `POST` | `/v1/payments/recurring-payments/{id}/resume` | Resume recurring payment | | `GET` | `/v1/payments/subscription-plans` | List subscription plans | | `POST` | `/v1/payments/subscription-plans` | Create subscription plan | | `GET` | `/v1/payments/subscription-plans/{planId}` | Get subscription plan | | `PATCH` | `/v1/payments/subscription-plans/{planId}` | Update subscription plan | | `POST` | `/v1/payments/subscription-plans/{planId}/prepare-create` | Prepare subscription plan creation | | `GET` | `/v1/payments/subscriptions` | List subscriptions | | `POST` | `/v1/payments/subscriptions` | Create subscription | | `GET` | `/v1/payments/subscriptions/{subscriptionId}` | Get subscription | | `GET` | `/v1/payments/subscriptions/{subscriptionId}/collection-attempts` | List subscription collection attempts | | `POST` | `/v1/payments/subscriptions/{subscriptionId}/prepare-authorization` | Prepare subscription authorization | | `POST` | `/v1/payments/subscriptions/{subscriptionId}/prepare-cancel` | Prepare subscription cancellation | | `POST` | `/v1/payments/subscriptions/{subscriptionId}/prepare-collection` | Prepare subscription collection | | `POST` | `/v1/payments/subscriptions/{subscriptionId}/prepare-resume` | Prepare subscription resume | | `GET` | `/v1/payments/transfer-batches` | List transfer batches | | `POST` | `/v1/payments/transfer-batches` | Create transfer batch | | `GET` | `/v1/payments/transfer-batches/{batchId}` | Get transfer batch | | `POST` | `/v1/payments/transfer-batches/estimate` | Estimate transfer batch | | `GET` | `/v1/payments/transfers` | List transfers | | `POST` | `/v1/payments/transfers` | Execute transfer (custody) | | `GET` | `/v1/payments/transfers/{transferId}` | Get transfer | | `GET` | `/v1/payments/wallets/{walletId}/balances` | Get wallet balances | | `GET` | `/v1/payments/wallets/{walletId}/policies` | Get wallet policy | | `PUT` | `/v1/payments/wallets/{walletId}/policies` | Update wallet policy | | `GET` | `/v1/payments/wallets/{walletId}/policies/evaluations` | List wallet policy evaluations | | `GET` | `/v1/payments/wallets/{walletId}/policies/evaluations/{policyEvaluationId}` | Get wallet policy evaluation | | `GET` | `/v1/payments/wallets/{walletId}/policies/revisions` | List wallet policy revisions | | `GET` | `/v1/transactions` | List transactions | --- ### Policies Source: https://platform.solana.com/docs/reference/api/policies > Wallet and API-key policy-control inventory. | Method | Endpoint | Summary | | --- | --- | --- | | `GET` | `/v1/policies` | List policy controls | --- ### Compliance Source: https://platform.solana.com/docs/reference/api/compliance > Risk and compliance screening endpoints. | Method | Endpoint | Summary | | --- | --- | --- | | `POST` | `/v1/compliance/address-screenings` | Screen an address across configured compliance providers | --- ### Asset Profiles Source: https://platform.solana.com/docs/reference/api/asset-profiles > Issued-asset identity and metadata profiles, plus the public token metadata URI. | Method | Endpoint | Summary | | --- | --- | --- | | `GET` | `/v1/issuance/asset-profiles` | List asset profiles | | `POST` | `/v1/issuance/asset-profiles` | Create token with asset profile | | `DELETE` | `/v1/issuance/asset-profiles/{profileId}` | Archive asset profile | | `GET` | `/v1/issuance/asset-profiles/{profileId}` | Get asset profile | | `PATCH` | `/v1/issuance/asset-profiles/{profileId}` | Update asset profile | | `GET` | `/v1/issuance/asset-profiles/by-token/{tokenId}` | Get asset profile by token | | `GET` | `/v1/issuance/asset-profiles/field-options` | Get asset profile field options | ## Notes - Generated from docs source. Hidden or internal-only APIs are intentionally excluded.