DEVELOPER DOCUMENTATION
API documentation
Integrate invoices, checkout and payment notifications.
Search results
No results. Try an endpoint, field or guide name.
Quickstart
Create your first invoice.
- Prepare a store
Enable its payment methods, configure providers and back up the project wallets.
- Create an API credential
In your console’s Settings → API access, choose read/write and assign the project.
- Send the request
Use your API host and copy your project and store IDs. Send decimal amounts as strings.
- Open checkout
Redirect to
links.checkoutfrom the response. Verify settlement before fulfilling the order.
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new invoice.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: order-1042-attempt-1' \
--header 'Content-Type: application/json' \
--data-raw '{
"amount": "49.90",
"currency": "USD",
"order_id": "order-1042",
"exchange_rate_spread_percent": "0.5"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new invoice.
const body = `{
"amount": "49.90",
"currency": "USD",
"order_id": "order-1042",
"exchange_rate_spread_percent": "0.5"
}`;
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "order-1042-attempt-1",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new invoice.
$body = <<<'JSON'
{
"amount": "49.90",
"currency": "USD",
"order_id": "order-1042",
"exchange_rate_spread_percent": "0.5"
}
JSON;
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: order-1042-attempt-1", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new invoice.
headers = {
"Idempotency-Key": "order-1042-attempt-1",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"amount": "49.90",
"currency": "USD",
"order_id": "order-1042",
"exchange_rate_spread_percent": "0.5"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Examples use placeholders and do not send requests from this page. See every invoice field and the response format →
Project & store IDs
Where to find YOUR_PROJECT_ID and YOUR_STORE_ID.
Use the UUIDs from your console, not project or store names or their readable identifiers.
| Placeholder | Where to find it | Used for |
|---|---|---|
| YOUR_PROJECT_ID | Project → Settings → API IDs → Project API ID → Copy. Also shown in the store’s Basic tab. | Project-level and store-level requests. |
| YOUR_STORE_ID | Project → Stores → select a store → Basic → API IDs → Store API ID → Copy. | Invoice creation and store payment-method requests. |
- Creating an invoice requires both IDs, even for the default store. The store must belong to that project, and the API credential must have access to the project.
- Invoice creation, listing, detail and checkout return invoice_id: the same UUID sent in IPN/webhooks. Use it in invoice paths, not internal id or order_id. Since merchant 4.0.0, the old public_id response field is removed; update integrations before upgrading.
- The REST API does not provide project/store listing routes. Copy IDs in the console, or use the scoped list_projects and list_stores MCP tools in merchant 5.0.0+.
- Store → Basic → Store domains selects active merchant, pay and API hostnames. Returned checkout links and new callback links prefer that store, then the default store, then system defaults. Retired or unactivated names are never selected. Configure your SDK with the preferred API hostname; changing a preference does not redirect other active aliases.
Authentication & scope
Keep credentials on your server and grant only the access needed.
| Default host | Purpose |
|---|---|
| merchant.example.com | Merchant console and Settings |
| pay.example.com | Customer checkout |
| api.example.com | Merchant API requests |
Replace example.com with your domain. Existing installations keep their configured names; manage aliases in Settings → System.
Authorization: Bearer YOUR_MERCHANT_API_TOKEN| Setting | How it works |
|---|---|
| Access level | Read-only credentials can list and retrieve. Read/write credentials can also create invoices and update the documented asset policies. |
| Projects | Assign the projects the credential may access. Store and invoice IDs must belong to an assigned project. |
| IP restrictions | Optionally allow exact public IPv4 or IPv6 egress addresses in Settings → API access. |
| Credential storage | Keep tokens in your backend configuration. Never include a bearer credential in a browser or checkout link. |
Public checkout routes use the invoice’s public ID and expose only checkout-safe data. Console sessions and administrative controls are separate from merchant API credentials.
Assets & wallets
Choose payment methods independently for each store.
- Read project payment assets and their readiness.
- Enable the native chain and configure its wallet and providers.
- Browse token candidates and verify the contract or mint before enabling a token.
- Select the store’s ordered payment methods. New invoices use its ready selections.
Tokens share their native chain’s wallet. Wallet balances return exact atomic amounts and advisory fiat values. Use the returned readiness fields to determine which methods can receive payments.
Verified ERC-20 tokens use the supported EVM networks; verified SPL tokens use Solana. Native payment methods are available across the 30 integrated networks. Monero uses a project-bound external view-only wallet connection.
Receive APIs and native/token coverage
| Rail | Support | Evidence | Requirements |
|---|---|---|---|
| Native payment rails | supported | Transaction-scanned | BTC, SOL, ETH (Ethereum/Base/Arbitrum/OP), BNB, HYPE, AVAX, and POL; Bitcoin outputs, canonical EVM transactions/receipts, and parsed Solana transfers provide invoice evidence. |
| ERC-20 token rails | supported | Transaction-scanned | Ethereum, Base, BNB Chain, HyperEVM, Avalanche, Polygon, Arbitrum, and Optimism require on-chain verification; indexed Transfer logs provide payment attribution. |
| SPL token rails | supported | Transaction-scanned | Solana candidates require mainnet and mint verification; exact token-balance deltas in parsed transactions provide payment attribution. |
| Additional UTXO native rails | supported | Transaction-scanned | BCH/LTC/DOGE use Esplora; BCH/DOGE also accept Bitcore, LTC/DOGE/DASH accept BlockCypher, Dash accepts Insight, and transparent ZEC accepts zcash-explorer. All also accept complete retained Core-compatible node-rpc blocks. Raw mode requires 1–48 confirmations, not mempool detection. Shielded Zcash is not supported. |
| Indexed account native rails | supported | Transaction-scanned | TRON uses tron-indexer or solidified node-rpc; XRP uses xrpl-jsonrpc; Stellar uses stellar-horizon or retained Stellar node-rpc ledgers; Cosmos Hub uses cometbft-jsonrpc; Algorand uses algorand-indexer or algod node-rpc; Hedera requires hedera-mirror, not an EVM relay. |
| Native ledger payment rails | supported | Transaction-scanned | Aptos uses aptos-rest; Sui uses sui-graphql; NEAR uses near-jsonrpc; Kaspa uses kaspa-rest. Polkadot Asset Hub accepts substrate-rest or metadata-aware finalized node-rpc; Tezos accepts tezos-tzkt or full Octez node-rpc operations. Native receipts only; archive retention is required for older invoices. |
| Cardano and TON native rails | supported | Transaction-scanned | Cardano requires cardano-koios and TON requires toncenter-v3. XRP tags, Stellar memo IDs, and TON invoice comments are returned as destination_tag and must be sent exactly. |
| Settlement integrity | supported | Independent verification | By default, final settlement requires two independent providers to agree on exact transaction/event, amount, canonical block or slot and finality. Raw and shared EVM windows also verify complete coverage. An administrator may explicitly opt a chain into one trusted provider; this removes the independent cross-check, not identity, completeness or finality checks. |
| Monero native rail | supported | Project-bound view-only wallet RPC | One dedicated external watch-only wallet-RPC behind an HTTPS method-allowlisting gateway creates account-0 subaddresses. The configured mainnet daemon threshold (default 2 independent sources, optional 1) provides settlement evidence. Native --restricted-rpc is incompatible with create_address; wallet backup and absence of a spend key are explicitly operator-attested, while no key material is sent to Wholly Crypto. |
Exchange balances and per-asset wallet-or-exchange sweep choices are available in the console, not through the public v1 API. See exchange setup.
Invoice lifecycle
Payment evidence, settlement and order fulfilment.
| Status | Meaning |
|---|---|
| new | Awaiting a payment |
| processing | Payment observed; accepted amount or finality pending |
| settled | Accepted under invoice settlement policy, or manually |
| expired | Deadline passed; late monitoring may continue |
| invalid | Payment cannot be accepted automatically |
| cancelled | Cancelled; only explicit reconciliation can reopen it |
amount_status records none, partial, paid or overpaid. timing_status distinguishes on-time and late payments. Store rules decide the required confirmations and accepted underpayment tolerance.
Use the invoice’s invoice_id with the invoice detail route. A checkout redirect alone is not proof of settlement. Review exceptions through reconciliation.
Safe retries
Invoice creation requires Idempotency-Key. After a timeout, retry with the same credential, key and exact request body. Use a new key only for a new invoice.
EVM payment scanning
Shared native-block and ERC-20 discovery groups recent invoices separately from older catch-up work. Each invoice retains its durable history cursor. Token queries use at most 100 blocks per request and shrink for stricter provider limits. Two independent providers verify each window by default. Settings → Chain connections → Details can opt a chain into one trusted source, without independent cross-checking; canonical transaction, amount and confirmation checks remain. Connection details distinguish scanner delays, history restrictions and quota cooldowns from basic node health. Public RPC capacity is not guaranteed.
IPN & webhooks
Receive and verify payment events.
IPN receives every generated invoice event at the invoice's effective ipn_url. Webhooks receive only the events selected for each enabled store endpoint. Both POST the same JSON snapshot; they are independent, so enabling both can notify your application twice.
Set ipn_url when creating an invoice, or inherit the store default. IPN uses the Store → IPN secret; each Store → Webhooks endpoint has its own secret. Neither is your API key.
When should I fulfil an order?
For event-based handling, use event_type = invoice.settled together with status = settled to trigger an order check. Verify the current invoice and fulfil each order only once.
status is the invoice state when the event was created. event_type tells you what happened. payment.received can carry processing or settled; it does not mean a second payment and is not an independent fulfilment signal.
Which events and statuses are sent?
| Event in settings/history | Body status | Meaning |
|---|---|---|
| invoice.created | new | Invoice created and awaiting payment. Also used when a controlled reopen returns an invoice to new. |
| payment.received | Resulting invoice status | A payment was recorded or the received amount increased. Usually processing or settled; this event alone is not proof of settlement. |
| invoice.processing | processing | Payment detected, but the accepted amount or required finality is not yet met. Partial payments are included. |
| invoice.settled | settled | Settlement policy met, or accepted manually. Check resolution and your order before fulfilment. |
| invoice.expired | expired | Payment deadline passed. A late payment can still change the status while monitoring continues. |
| invoice.invalid | invalid | Cannot be accepted automatically, payment evidence was lost, or a merchant rejected it. Review the invoice. |
| invoice.cancelled | cancelled | Invoice cancelled. Do not fulfil; a cancellation does not refund an on-chain payment. |
Why Ethereum and Solana can send different event flows
Confirmations arrive later (Ethereum example)
| Sequence | event_type | status |
|---|---|---|
| 1 | invoice.created | new |
| 2 | payment.received | processing |
| 2 | invoice.processing | processing |
| 3 | invoice.settled | settled |
Already final when detected (Solana example)
| Sequence | event_type | status |
|---|---|---|
| 1 | invoice.created | new |
| 2 | payment.received | settled |
| 2 | invoice.settled | settled |
These show event creation, not guaranteed delivery order. Either flow can happen on other chains depending on detection timing and settlement policy. Do not require a processing event before settled.
Fulfil once: receiver example and duplicate protection
| Approach | How to handle it |
|---|---|
| Event-based receiver | Keep distinct events by signed event_id, then select invoice.settled with status = settled. Do not discard this event because payment.received with the same sequence arrived first. |
| SDK order-state inbox | The supplied PHP, Python and Node receiver examples collapse project + invoice_id + sequence. Process the saved state regardless of event_type, fetch the current invoice, and fulfil once if settled. Do not add an invoice.settled-only filter after this collapse. |
A retry keeps event_id and the original body. Different events can share sequence but have different event_id values. Deduplicate deliveries by signed event_id for event-based handling; independently guard fulfilment by configured installation/project + invoice_id and your order. A later re-settlement must not credit the order twice.
HTTP receiver:
Verify raw-body signature, timestamp and configured project/store scope.
Save to a durable inbox; deduplicate the signed event_id.
Return HTTP 2xx only after persistence succeeds.
Event-based background worker:
Other events go to status/reconciliation handling, not fulfilment.
Continue here only for event_type = invoice.settled and status = settled.
Fetch the current invoice from your configured API origin.
Check settled status, project/store, order, amount, currency and review policy.
In one database transaction:
Lock the order and check the scoped invoice has not been fulfilled.
Credit/complete once and save the fulfilment record.
Queue any external fulfilment with the same business idempotency key.
SDK order-state worker:
Use the same current-invoice checks and fulfil-once transaction.
Do not filter event_type after collapsing events by invoice revision.Pseudocode, not a drop-in receiver.
All invoice states and payment exceptions
| Field | Values | Meaning |
|---|---|---|
| status | new, processing, settled, expired, invalid, cancelled | Invoice state at event creation; not necessarily its current state at delivery. |
| amount_status | none, partial, paid, overpaid | Amount received, including accepted tolerance. paid is not confirmation finality. |
| timing_status | on_time, late | Whether payment met the invoice deadline. |
| resolution | automatic, manually_settled, manually_invalidated | Whether normal rules or a manual acceptance/rejection determined the result. |
| requires_review | false, true | Exception hint, not another invoice status or automatic permission to fulfil/refund. |
| Situation | Handling |
|---|---|
| Underpayment / tolerance | Under automatic rules, partial does not settle. paid can include an accepted shortfall, but finality is still required. Use invoice status, not an amount comparison alone. |
| Overpayment | overpaid can coexist with settled and requires_review = true. Apply your excess-payment policy; never credit the order twice or automatically refund an unverified address. |
| Late payment | expired can later change while monitoring continues. timing_status = late flags review; do not automatically reopen or ship a cancelled order. |
| Manual acceptance | invoice.settled can have resolution = manually_settled without qualifying on-chain funds. Decide whether your integration accepts this override; payment summary fields may be null. |
| Reorg / invalidation | A newer revision can invalidate earlier payment evidence. Re-fetch current state and handle reversal through reconciliation. Do not ignore it just because the order was once settled. |
| Zero confirmations / zero amount | Zero-confirmation settlement can happen on detection and carries reorg risk. An explicitly allowed zero-amount invoice settles without a payment. Neither requires a payment.received event first. |
Use status = settled for fulfilment, not amount_status = paid and not a checkout redirect. With zero required confirmations, settlement can happen on detection; that carries reorg risk.
Underpaid is amount_status = partial; overpaid is overpaid. paid means the accepted minimum, including the invoice's underpayment tolerance, has arrived. These are amount states, not invoice statuses. late is a timing_status, not a separate event.
A typical flow is new → processing → settled, but intermediate states can be skipped. An explicitly allowed zero-amount invoice settles without payment and keeps amount_status = none. Manual acceptance is marked manually_settled.
Callbacks are immutable snapshots, not live status responses. They may arrive late, out of order or more than once. Payment and status events can share an invoice sequence and the same invoice-state fields, but have different signed event_id and event_type values. Confirmation counts do not produce a guaranteed callback for every block.
What you receive
{
"invoice_id": "11111111-2222-4333-8444-555555555555",
"status": "settled",
"amount_status": "paid",
"timing_status": "on_time",
"resolution": "automatic",
"sequence": 3,
"amount": "49.9",
"currency": "EUR",
"order_id": "order-1042",
"payload_version": 2,
"event_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"event_type": "invoice.settled",
"occurred_at": "2026-09-14T12:05:00Z",
"project_id": "11111111-1111-4111-8111-111111111111",
"store_id": "22222222-2222-4222-8222-222222222222",
"description": "Annual plan",
"email": "ada@example.test",
"customer": {
"firstname": "Ada",
"lastname": "Lovelace",
"countryiso2": "GB"
},
"metadata": {
"firstname": "Ada",
"lastname": "Lovelace",
"countryiso2": "GB",
"cart_id": "cart-681"
},
"created_at": "2026-09-14T12:00:00Z",
"updated_at": "2026-09-14T12:05:00Z",
"expires_at": "2026-09-14T12:15:00Z",
"monitoring_expires_at": "2026-09-21T12:15:00Z",
"settled_at": "2026-09-14T12:05:00Z",
"paid_chain": "ethereum",
"paid_asset": "USDC",
"paid_asset_amount": "58.17342",
"paid_asset_amount_received": "58.17342",
"paid_payment_method_id": "33333333-3333-4333-8333-333333333333",
"settlement_exchange_rate": {
"rate": "1.17",
"units": "asset_per_invoice_currency",
"currency": "EUR",
"symbol": "USDC",
"observed_at": "2026-09-14T12:05:00Z",
"as_of": "2026-09-14T12:04:30Z",
"pricing_provider": "kraken",
"asset_provider": "kraken",
"pricing_fetched_at": "2026-09-14T12:04:30Z",
"asset_fetched_at": "2026-09-14T12:04:30Z",
"stale": false,
"is_fixed": false,
"reference_currency": "USD",
"uses_reference_proxy": false
},
"cancelled_at": null,
"exchange_rate_spread_percent": "0.5",
"underpayment_tolerance_percent": "1",
"reason_code": "payment_confirmed",
"requires_review": false,
"links": {
"checkout": "https://pay.example.com/invoice/11111111-2222-4333-8444-555555555555",
"invoice": "https://api.example.com/v1/projects/11111111-1111-4111-8111-111111111111/invoices/11111111-2222-4333-8444-555555555555",
"payments": "https://api.example.com/v1/projects/11111111-1111-4111-8111-111111111111/invoices/11111111-2222-4333-8444-555555555555/payments"
},
"payment_info": {
"active_payment_method_id": "33333333-3333-4333-8333-333333333333",
"method_count": 1,
"methods_truncated": false,
"methods": [
{
"payment_method_id": "33333333-3333-4333-8333-333333333333",
"payment_rail": "onchain",
"chain_slug": "ethereum",
"network": "mainnet",
"caip_network_id": "eip155:1",
"asset_id": "44444444-4444-4444-8444-444444444444",
"asset_key": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"caip_asset_id": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"asset_name": "USD Coin",
"symbol": "USDC",
"asset_kind": "token",
"asset_decimals": 6,
"contract_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"token_standard": "erc20",
"destination_address": "0x1111111111111111111111111111111111111111",
"destination_tag": null,
"status": "paid",
"amounts": {
"expected_amount": "58.17342",
"expected_amount_atomic": "58173420",
"received_amount": "58.17342",
"received_amount_atomic": "58173420",
"confirmed_amount": "58.17342",
"confirmed_amount_atomic": "58173420",
"unconfirmed_amount": "0",
"unconfirmed_amount_atomic": "0",
"minimum_payment_amount": "57.591686",
"minimum_payment_amount_atomic": "57591686",
"remaining_amount": "0",
"remaining_amount_atomic": "0",
"remaining_to_full_amount": "0",
"remaining_to_full_amount_atomic": "0",
"overpaid_amount": "0",
"overpaid_amount_atomic": "0"
},
"acceptance": {
"finality_mode": "confirmations",
"required_confirmations": 2,
"observed_confirmations": 2,
"underpayment_tolerance_percent": "1"
},
"quote": {
"effective_rate": "1.1658",
"reference_rate": "1.16",
"units": "asset_per_invoice_currency",
"currency": "EUR",
"symbol": "USDC",
"exchange_rate_spread_percent": "0.5",
"quote_expires_at": "2026-09-14T12:15:00Z",
"provenance_available": true,
"rounding": "up",
"unrounded_payment_amount": "58.17342",
"rounding_adjustment": "0",
"pricing_provider": "kraken",
"asset_provider": "kraken",
"pricing_fetched_at": "2026-09-14T11:59:30Z",
"asset_fetched_at": "2026-09-14T11:59:30Z"
},
"market_rate_at_event": {
"rate": "1.17",
"units": "asset_per_invoice_currency",
"currency": "EUR",
"symbol": "USDC",
"observed_at": "2026-09-14T12:05:00Z",
"pricing_provider": "kraken",
"asset_provider": "kraken",
"pricing_fetched_at": "2026-09-14T12:04:30Z",
"asset_fetched_at": "2026-09-14T12:04:30Z",
"as_of": "2026-09-14T12:04:30Z",
"stale": false,
"is_fixed": false,
"reference_currency": "USD",
"uses_reference_proxy": false
},
"payment_count": 1,
"payments_truncated": false,
"payments": [
{
"payment_id": "55555555-5555-4555-8555-555555555555",
"payment_method_id": "33333333-3333-4333-8333-333333333333",
"transaction_id": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"payment_hash": null,
"event_index": 0,
"payment_rail": "onchain",
"chain_slug": "ethereum",
"network": "mainnet",
"asset_id": "44444444-4444-4444-8444-444444444444",
"asset_key": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"caip_asset_id": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"symbol": "USDC",
"asset_decimals": 6,
"amount": "58.17342",
"amount_atomic": "58173420",
"status": "final",
"counts_towards_received": true,
"confirmations": 2,
"block_height": 26000000,
"observed_at": "2026-09-14T12:04:30Z",
"chain_time": "2026-09-14T12:04:20Z",
"finalized_at": "2026-09-14T12:05:00Z",
"explorer_name": "Etherscan",
"explorer_url": "https://etherscan.io/tx/0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
],
"links": {
"payments": "https://api.example.com/v1/projects/11111111-1111-4111-8111-111111111111/invoices/11111111-2222-4333-8444-555555555555/payments?payment_method_id=33333333-3333-4333-8333-333333333333"
}
}
]
}
}amount is the original invoice total. payment_info describes observed crypto transfers, amounts still missing and locked rates. Version 2 also signs the event name, event ID and project/store scope.
All callback fields and extra invoice data
| Field | Type | Meaning |
|---|---|---|
| invoice_id | UUID | Public invoice UUID, used by the authenticated invoice detail route |
| status | string | Snapshot invoice state: new, processing, settled, expired, invalid, cancelled |
| amount_status | string | none, partial, paid, or overpaid; paid includes the accepted underpayment tolerance, not confirmation finality |
| timing_status | string | on_time or late |
| resolution | string | automatic, manually_settled, or manually_invalidated |
| sequence | integer | Increasing invoice revision; different events can share one revision. Compare without losing integer precision |
| amount | decimal string | Original invoice total, not the received crypto amount; preserve decimal precision |
| currency | string | Currency of amount, e.g. EUR for a EUR invoice paid with USDC |
| order_id | string | null | Merchant order reference |
| payload_version | integer | 2 for newly generated 4.1.0+ events; absent on retained legacy events |
| event_id | UUID | Signed event identity, unchanged on retries and manual redelivery |
| event_type | string | One of the seven subscription events |
| occurred_at | timestamp | When this immutable event was created, not delivery time |
| project_id | UUID | Merchant project scope; match to configured receiver |
| store_id | UUID | Merchant store scope; match to configured receiver |
| description | string | null | Original invoice description |
| string | null | Optional customer email at event creation | |
| customer | object | Recognized optional customer metadata fields; no guessed or enriched personal data |
| metadata | object | Original merchant metadata as it existed at event creation |
| created_at | timestamp | Invoice creation time |
| updated_at | timestamp | Invoice state update time |
| expires_at | timestamp | Invoice payment deadline |
| monitoring_expires_at | timestamp | Late-payment monitoring deadline |
| settled_at | timestamp | null | Settlement time |
| paid_chain | string | null | 4.1.2+: chain slug of the proven settling method, e.g. ethereum; null without a saved qualifying settlement |
| paid_asset | string | null | 4.1.2+: native coin or token ticker, e.g. BTC, ETH or USDC; a display label, not unique asset identity |
| paid_asset_amount | decimal string | null | 5.0.1+: full locked amount requested in paid_asset units, before subtracting tolerance; saved at settlement |
| paid_asset_amount_received | decimal string | null | 5.0.1+: total valid amount received for the winning method at settlement, including accepted shortfalls/excess; frozen, not a live balance |
| paid_payment_method_id | UUID | null | 4.1.2+: settling intent ID; matches payment_info.methods[].payment_method_id and its exact network/contract |
| settlement_exchange_rate | object | null | 4.1.2+: saved before-spread market snapshot at settlement, with explicit units, currency, source timestamps and quality flags; never repriced on delivery |
| cancelled_at | timestamp | null | Cancellation time |
| exchange_rate_spread_percent | decimal string | Locked spread, not the current store default |
| underpayment_tolerance_percent | decimal string | Locked invoice tolerance; each method also reports its effective tolerance |
| reason_code | string | null | Machine-readable state-transition reason |
| requires_review | boolean | Payment exception hint; not permission to fulfil or refund automatically |
| links | object | Checkout, authenticated invoice and payments URLs at event creation. Store → Basic domain preferences apply, then the default store, then the global primary; only active role-matched domains are used. Retries retain the original signed links; null if no active host record. |
| payment_info | object | Actual observed methods, exact amounts, locked quote, advisory market snapshot and bounded payment observations; see field groups below |
Settlement summary: settlement_exchange_rate
| Field | Type | Meaning |
|---|---|---|
| rate / units / currency / symbol | strings | Before-spread asset units per one invoice currency unit. Decimal string, not a payment amount or executed trade. |
| observed_at / as_of | timestamps | Settlement capture time / older source timestamp. Do not treat cached data as a live tick. |
| pricing_provider / asset_provider / pricing_fetched_at / asset_fetched_at | strings / timestamps | Fiat and asset pricing sources and their fetch times, saved at settlement. |
| stale / is_fixed / uses_reference_proxy / reference_currency | booleans / string | Same quality flags as market_rate_at_event. Fixed project prices are labelled; reference currency is USD. |
| Missing snapshot or price | null | No guessed historical rate. Before settlement all summary fields are null; missing prices alone leave proven paid_* identifiers available. |
Payment methods: payment_info
| Field | Type | Meaning |
|---|---|---|
| active_payment_method_id | UUID | null | Winning or selected observed method. Null before detection or after invalidation; no default method is guessed. |
| method_count / methods_truncated | integer / boolean | Total observed methods and whether the embedded methods list is incomplete. |
| methods[] | object[] | At most eight observed methods, active method first. No cross-asset totals. |
| payment_method_id / payment_rail | UUID / string | Invoice intent identity and onchain or lightning transport. |
| chain_slug / network / caip_network_id | string | Network identity. Always pair token identity with its network. |
| asset_id / asset_key / caip_asset_id | UUID / string / nullable string | Verified registry identity; symbols alone are not unique. |
| asset_name / symbol / asset_kind | string | Asset display name, ticker and native or token kind. |
| contract_address / token_standard | string | null | Token contract or mint and standard; null for native assets. |
| asset_decimals | integer | Atomic precision; Lightning BTC uses 11. |
| destination_address / destination_tag | string | null | Public receiving address and required memo/tag. Address is null for Lightning; never a private key. |
| status | string | Method state: pending, partial, paid, overpaid, expired or invalid. Paid is not by itself invoice settlement. |
| payment_count / payments_truncated / payments[] | integer / boolean / object[] | Total observations and latest five or fewer. Each observation is described below. |
| links.payments | HTTPS URL | null | Authenticated paginated history for this method on the configured API origin. |
Exact amounts: methods[].amounts
| Field | Type | Meaning |
|---|---|---|
| expected_amount | decimal string | Full locked quote, after spread and upward rounding. |
| received_amount / confirmed_amount | decimal strings | Valid detected funds / funds meeting this method's confirmation or finality policy. |
| unconfirmed_amount | decimal string | max(received - confirmed, 0). Not an additional amount to send. |
| minimum_payment_amount | decimal string | Accepted threshold after tolerance. May be below the full quote. |
| remaining_amount | decimal string | max(minimum accepted - received, 0). Additional funds needed to reach the accepted threshold, not confirmation progress. |
| remaining_to_full_amount | decimal string | max(full quote - received, 0), ignoring tolerance. |
| overpaid_amount | decimal string | max(received - full quote, 0). Does not authorize an automatic refund. |
| Every amount's *_atomic companion | integer string | Exact smallest-unit representation. Use decimal or integer libraries; never float or JavaScript Number for money. |
Confirmation policy: methods[].acceptance
| Field | Type | Meaning |
|---|---|---|
| finality_mode / required_confirmations | string / integer | Locked confirmations or finalized policy. Zero confirmations is explicitly allowed by merchant policy, not universal network finality. |
| observed_confirmations | integer | null | Minimum among valid observations, not just the newest transfer. Null for Lightning or no valid observations. |
| underpayment_tolerance_percent | decimal string | Effective method tolerance. Lightning uses zero even when the invoice has a nonzero on-chain tolerance. |
Rates: methods[].quote and market_rate_at_event
| Field | Type | Meaning |
|---|---|---|
| quote.effective_rate / units / currency / symbol | strings | Locked asset_per_invoice_currency rate including spread; currency and symbol state the direction explicitly. |
| quote.exchange_rate_spread_percent / quote_expires_at | decimal string / timestamp | Locked spread and quote deadline. Never replaced with current store settings. |
| quote.reference_rate / unrounded_payment_amount / rounding_adjustment | decimal string | null | Before-spread reference, payment amount before rounding, and upward adjustment in asset units. |
| quote.pricing_provider / asset_provider / pricing_fetched_at / asset_fetched_at | string or timestamp | null | Original currency and asset pricing sources/times. No API keys or provider credentials. |
| quote.provenance_available / rounding | boolean / string | False for old invoices without a saved source snapshot; rounding is up. |
| market_rate_at_event | object | null | Advisory cached market snapshot when this event was made. Missing data stays null; it never changes invoice amounts or delays for a network fetch. |
| market_rate_at_event.rate / units / currency / symbol | strings | Before-spread market rate, with the same explicit direction as quote. |
| market_rate_at_event.observed_at / as_of / pricing_fetched_at / asset_fetched_at | timestamps | Event snapshot time / older of the two source times / each source time. |
| market_rate_at_event.pricing_provider / asset_provider | strings | Cached currency and asset sources, including configured custom-token prices. |
| market_rate_at_event.stale / is_fixed / uses_reference_proxy / reference_currency | booleans / string | Whether cache is stale, token price fixed or USD reference uses a stablecoin proxy. Reference currency is USD. Stale is advisory, never a fresh quote. |
Transfer records: methods[].payments[] and GET …/payments
| Field | Type | Meaning |
|---|---|---|
| payment_id / payment_method_id | UUID | Observation identity / parent intent identity. Use payment_id for history deduplication. |
| transaction_id / payment_hash / event_index | string | null / integer | On-chain hash and transfer/log/output index, or Lightning hash. Lightning has no transaction or explorer link. |
| payment_rail / chain_slug / network / asset_id / asset_key / caip_asset_id / symbol / asset_decimals | strings / UUID / integer | Same asset and network identifiers as the containing method. |
| amount / amount_atomic | decimal / integer strings | This transfer's exact value, never a fiat conversion. |
| status / counts_towards_received | string / boolean | detected, confirming and final count; reorged, replaced and invalid do not. Keep invalidated history for reconciliation. |
| confirmations / block_height | integer | null | Observation's block data; confirmations null for Lightning. |
| observed_at / chain_time / finalized_at | timestamp | null | First seen locally, trusted chain time if available, and policy-final time if reached. |
| explorer_name / explorer_url | string | null | Validated public block-explorer reference, where supported. |
Merchant 5.13.3 excludes verified internal gas-funding transfers from customer payment totals, payment_info, the invoice payments API, refund limits and payment.received events. Their chain/treasury records remain available for wallet accounting. Ordinary transfers and genuine overpayments still count. Existing signed callback bodies are never rewritten. If a historical settlement depended on internal funding instead of customer funds, reconciliation emits invoice.invalid with reason_code internal_gas_funding_excluded; review it instead of fulfilling again.
Merchant 4.1.0 adds payload_version 2 without moving or changing the original nine fields. Previously queued events keep their original body and may have no payload_version. event_id, event_type and project/store IDs are now inside the signed body; transport event/delivery headers remain unsigned.
payment_info describes observed payments, not every offered checkout option. Before detection its active_payment_method_id is null and methods is empty. Reorged/invalid observations can remain in methods even after the active method becomes null. Never add amounts from different assets or networks together.
All amounts, atomic integers, rates and percentages are strings. received_amount includes valid funds awaiting confirmation; confirmed_amount meets that method's finality policy. remaining_amount is max(minimum_payment_amount minus received_amount, 0); remaining_to_full_amount is max(expected_amount minus received_amount, 0). Example: 100 USDC expected, 99 received, 1% tolerance means remaining_amount 0 and remaining_to_full_amount 1. Finality is still required.
quote is the locked invoice calculation: asset units per one invoice currency unit. Spread is applied before upward rounding. Use expected_amount_atomic for exact payment comparison; a displayed rate alone may not reproduce upward rounding. Old invoices without stored source provenance expose null source/reference/rounding fields and provenance_available false, never today's data presented as a historical quote.
market_rate_at_event is advisory cached data before spread, frozen at event creation. It has source times, stale and reference-proxy flags; it is null if no usable cached pair exists. No live rate request blocks a notification, and this market observation never changes the amount owed. Fixed custom tokens are labelled is_fixed; DEX tokens use their project-specific source, not a same-symbol token.
Top-level paid_chain, paid_asset, paid_payment_method_id and settlement_exchange_rate (4.1.2+) identify the proven winning method after settlement, not a selected checkout option or a sum of different methods. Before settlement, after invalidation, for older unsnapshotted settlements, or for manual acceptance without qualifying policy-final funds, the summary fields are null. Symbols are display labels: follow the method ID for the exact network/asset/contract identity.
Merchant 5.0.1 adds paid_asset_amount and paid_asset_amount_received as exact decimal strings in paid_asset units; payload_version remains 2. paid_asset_amount is the full locked quote, including spread and upward rounding, never the tolerance threshold or a remaining balance. paid_asset_amount_received is the winning method's total valid receipts at settlement, including funds awaiting confirmation and any accepted shortfall or excess. Example: 100 USDC quoted, 99 received and accepted with tolerance gives 100 and 99, not 99 and 99. Both are frozen with the settlement snapshot; use payment_info.methods[].amounts for receipts at each event or the payments API for current records. They are null without a qualifying snapshot and for pre-5.0.1 snapshots; old queued event bodies are unchanged. Never convert exact decimal strings to floating point for accounting.
settlement_exchange_rate is the before-spread cached market observation captured with settlement, not the locked invoice quote or an executed exchange trade. Its shape matches market_rate_at_event; 1.17 asset_per_invoice_currency with EUR/USDC means 1 EUR = 1.17 USDC. Source timestamps and stale/fixed/proxy flags describe its quality. A missing pair leaves the rate null but a proven method still has paid_* fields. It never changes the amount owed or waits for a live provider call. Later payments using the same method, retries and redelivery cannot replace the saved snapshot, including a saved null rate. A genuine re-settlement or changed settling method captures a new snapshot; observed_at identifies that capture, while settled_at may retain the first settlement time. Old event bodies stay unchanged.
At most eight observed methods and five latest payment observations per method are included, with counts and truncation flags. Payload budgeting can reduce those arrays further. A payment observation is one transfer/log/UTXO output, not necessarily a unique transaction hash. Use GET /v1/projects/YOUR_PROJECT_ID/invoices/{invoice_id}/payments with payment_method_id, limit and offset for the complete current history. The invoice detail retains every quoted method and its quote_details. API links require your configured host and credentials, never forward a bearer token to an arbitrary callback-provided URL.
Lightning uses payment_hash instead of transaction_id; receiving address, explorer and observed confirmations are null. Its exact BTC amount uses 11 decimals (millisatoshis), and effective tolerance is zero. No BOLT11 payment preimage, wallet key, signing secret or provider credential is included. Customer/metadata fields belong only in merchant responses and signed callbacks, never public checkout; do not put credentials in metadata.
Receive safely
- Verify the exact raw body with the matching secret before parsing. Store → IPN supplies the IPN secret, including custom ipn_url deliveries. Each Store → Webhooks endpoint has its own secret. Neither is your API token; rotating one does not rotate the others.
- Check the signed timestamp (SDK default: five minutes in either direction) and match signed project/store IDs to your receiver configuration when present. Durably queue before returning HTTP 2xx. For per-event processing, v2 event_id is signed; header IDs alone are not replay protection because those headers are not signed. For order-state inboxes, deduplicate invoice_id and sequence and compare the original invoice state fields, not the entire v2 body: different event types/IDs can share a revision.
- In a worker, fetch the current invoice from your configured API origin, not an arbitrary callback link. Match the stored order, project/store, amount and currency, require current settled status, and apply your manual-acceptance and exception policy. Lock the order and fulfil once in a database transaction, independently of event deduplication.
- Never apply an older sequence over a newer one. Different events can share a revision; do not combine revision-level deduplication with an invoice.settled-only filter. Reopening/reconciliation can change status; sequence, not a fixed status ranking, orders updates. Record reversals for review rather than fulfilling again.
Receiver examples: PHP · Python · Node.js / TypeScript.
Signature verification & delivery rules
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWhollySignature(rawBody, header, signingSecret, toleranceSeconds = 300) {
const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || "");
if (!match) return false;
const timestamp = Number(match[1]);
if (!Number.isSafeInteger(timestamp)) return false;
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > toleranceSeconds) return false;
// rawBody must be the exact request Buffer, before JSON parsing.
const expected = createHmac("sha256", signingSecret)
.update(String(timestamp))
.update(".")
.update(rawBody)
.digest();
const presented = Buffer.from(match[2], "hex");
return timingSafeEqual(expected, presented);
}| Delivery rule | Details |
|---|---|
| Headers | Wholly-Signature, Wholly-Event-Id, and Wholly-Delivery-Id; Content-Type is application/json. |
| Signature | HMAC-SHA256 over <unix timestamp>.<exact raw body>; header format t=<timestamp>,v1=<64 lowercase hex>. |
| Success | Any HTTP 2xx response. Redirects are not followed; non-2xx responses are failures. |
| Timeouts | 5-second connect timeout and 10-second total request timeout. |
| Retry schedule | Up to 8 attempts for retryable failures: immediately, then delays of 10s, 1m, 5m, 15m, 1h, 6h, and 24h after the previous attempt finishes. IPN retries automatically; webhook automatic retry can be disabled per endpoint. |
| Target safety | Public HTTPS only. DNS is revalidated and pinned for delivery; local/private/reserved targets are rejected. |
| Event retention | Notification event payloads and delivery retention are scheduled for 90 days; retained details are purged in bounded batches. |
| Deduplication | Persist the signed invoice_id and sequence scoped to the configured project. Wholly-Event-Id identifies an event; Wholly-Delivery-Id identifies a delivery record (retries reuse it; manual redelivery creates another). Neither ID header is signed. |
| Event naming | Version 2 signs event_id and event_type in the body. Legacy queued events have neither. Different event types can share an invoice sequence; reconcile invoice state by revision, or deduplicate individual events by signed event_id. |
| Secret rotation | Rotation has no overlap or version header and immediately changes signatures for queued, retried, and manual deliveries. |
| Paused deliveries | Insufficient processing credits pause IPN/webhooks, including retries. Incoming payments continue; queued notifications resume after funding within their payload-retention period. |
AI assistants · MCP
Connect an assistant to your merchant installation.
Merchant 5.0.0 includes an opt-in MCP server on your configured API domain. It runs inside your installation, not through a shared Wholly Crypto relay.
- Open Settings → API access. Create a dedicated credential, assign only the projects the assistant needs, and start with read-only access. On an operator-hosted account, the operator enables the installation's MCP service first; you manage only your own credentials and grants.
- In AI connections · MCP, enable MCP, select the credential and save its MCP access. Existing credentials have no MCP access until explicitly enabled.
- Copy the MCP server URL into your client's remote HTTP server settings. With OAuth, sign in to your merchant console, review the client name and return address, choose a credential and approve. Your existing Basic Auth and TOTP protections still apply.
- Invoice creation additionally requires a read/write credential, Read + create invoices in its MCP policy, the mcp:invoice:create OAuth scope and explicit approval. An OAuth connection never gains projects added to a credential after approval.
{
"mcpServers": {
"whollycrypto": {
"url": "https://api.example.com/mcp"
}
}
}| Tool | Access | Purpose |
|---|---|---|
| list_projects | Read | Enabled projects assigned to the connection; limit/offset pagination. |
| list_stores | Read | Stores, IDs and enabled status within project_id; limit/offset pagination. |
| list_payment_methods | Read | Configured chain, token and Lightning methods for project_id + store_id. |
| get_wallet_balances | Read | Receiving addresses and cached balances, with freshness/availability fields; never wallet secrets. |
| list_invoices | Read | Project invoices, filtered by store, status or search; limit/offset pagination. |
| get_invoice | Read | Full invoice details and checkout link using project_id + invoice_id. |
| get_delivery_history | Read | Store IPN/webhook statuses, attempts and HTTP results. Optional invoice_id/kind filters; no secrets or callback bodies. |
| convert_amount | Read | Cached reference conversion using from, to and a decimal-string amount; not an invoice quote. |
| create_invoice | Explicit write | project_id, store_id, idempotency_key and invoice (the existing invoice creation body). invoice.payment_methods filters enabled store methods; 5.4.0+ ignores inactive/unaccepted choices and falls back to store defaults if none match. Chain-only selects all active accepted assets. Chain-scoped asset_tickers are supported from 5.3.0. Returns the normal invoice response. |
Protocol, OAuth & safety
Use Streamable HTTP over HTTPS. Negotiate an advertised protocol version and include MCP-Protocol-Version on subsequent POSTs. Send Content-Type: application/json and Accept: application/json, text/event-stream. Responses are finite JSON; reconnects do not need an MCP session ID.
OAuth uses short-lived access tokens (15 minutes), one-time S256 PKCE codes (5 minutes) and rotating refresh tokens (connection lifetime 30 days). Reusing an already-used refresh token revokes that connection. Reconnect after expiry, credential rotation, policy changes or a canonical API-domain change.
OAuth discovery is public only when MCP is enabled. The resource parameter must equal the canonical URL returned by discovery, including /mcp. Dynamic registration is supported; remote client-ID metadata documents and client secrets are not.
For clients supporting custom Authorization headers, an MCP-enabled merchant API token can be used as Bearer instead. It retains its separate REST permissions; prefer OAuth for a connection restricted to MCP. Never paste credentials in chat, URLs, tool arguments or source control.
MCP shares the credential's per-minute REST quota and exact source-IP restrictions, plus API-host IP restrictions. OAuth does not bypass a whitelist. For remote AI clients, allow their documented egress IPs or leave this restriction off deliberately. No web security challenge or caching should be applied to MCP/OAuth routes.
HTTP errors: 401 needs authentication, 403 denies origin/IP/permission, 404 means MCP is disabled or the wrong host, 405 means use POST, 413 means the 32 KiB body limit, 429 includes Retry-After. JSON-RPC errors use error.code; tool-level failures use result.isError=true even with HTTP 200. Successful results include content and structuredContent.
Lists default to 25 rows, maximum 100; offset is bounded to 1000000. Tool replies are bounded to 2 MiB. Expired grants, authorization requests and rate buckets are pruned automatically; at most 100 active OAuth connections are shown in settings.
Disabled projects/stores cannot be operated through MCP. The connection can list a store's enabled state, but reading its payment methods, delivery history or creating invoices requires an enabled store. Ordinary console project users cannot administer MCP.
Use a new idempotency_key for a new invoice; after a timeout retry the same credential, key and identical invoice object. Decimal amounts, spread, tolerance, confirmations and checkout appearance follow the REST invoice contract. MCP never bypasses merchant payment or credit policy.
The initial tools cannot reveal private keys/recovery phrases, send or sweep funds, issue refunds, resend callbacks, change payment methods, edit accounts/domains or manage billing. Treat invoice descriptions, customer fields and metadata as untrusted data, not agent instructions. Connected AI providers receive the data you authorize them to read.
| Method | Path | Contract |
|---|---|---|
| POST | /mcp | Authenticated JSON-RPC: initialize, ping, tools/list, tools/call. Notification requests return 202; batches are rejected. |
| GET / DELETE | /mcp | Authenticated 405: finite JSON responses, no standalone SSE stream and no server-side MCP session. |
| GET | /.well-known/oauth-protected-resource/mcp | Canonical resource URL and authorization-server discovery; also available at /.well-known/oauth-protected-resource. |
| GET | /.well-known/oauth-authorization-server | OAuth endpoints, authorization_code/refresh_token, S256 PKCE and supported scopes. |
| POST | /mcp/oauth/register | Public-client registration: client_name and exact redirect_uris. HTTPS or loopback HTTP only. No client secret or remote metadata fetching. |
| GET | /mcp/oauth/authorize | client_id, redirect_uri, response_type=code, resource, code_challenge, code_challenge_method=S256, optional scope/state; redirects to console approval. |
| POST | /mcp/oauth/token | Form-encoded authorization_code + code + code_verifier + redirect_uri, or refresh_token + refresh_token. Always include client_id and resource. |
| POST | /mcp/oauth/revoke | Form-encoded client_id and token. Revokes the matching access/refresh-token connection. |
Direct tool request example
Initialize and negotiate the protocol first through your MCP client. This shows a subsequent request.
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/mcp" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'MCP-Protocol-Version: 2025-11-25' \
--header 'Accept: application/json, text/event-stream' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_invoices",
"arguments": {
"project_id": "YOUR_PROJECT_ID",
"limit": 10
}
}
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const body = `{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_invoices",
"arguments": {
"project_id": "YOUR_PROJECT_ID",
"limit": 10
}
}
}`;
const response = await fetch("https://api.example.com/mcp", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"MCP-Protocol-Version": "2025-11-25",
"Accept": "application/json, text/event-stream",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$body = <<<'JSON'
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_invoices",
"arguments": {
"project_id": "YOUR_PROJECT_ID",
"limit": 10
}
}
}
JSON;
$ch = curl_init("https://api.example.com/mcp");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "MCP-Protocol-Version: 2025-11-25", "Accept: application/json, text/event-stream", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"MCP-Protocol-Version": "2025-11-25",
"Accept": "application/json, text/event-stream",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_invoices",
"arguments": {
"project_id": "YOUR_PROJECT_ID",
"limit": 10
}
}
}""".encode("utf-8")
request = Request("https://api.example.com/mcp",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Operator API
Provision hosted merchants with separate, scoped server-side keys.
Host several businesses and automate their setup through api.example.com/v1/operator. Available from 7.4.0 in Operator mode only. The ordinary Merchant API remains unchanged.
- Open Operator → Settings → Operator API and enable it (off by default). Create a separate credential with only the required permissions and hosted merchants.
- Keep the wc_operator_ key on your server. Use the API hostname, not the Operator panel hostname or a merchant key.
- Persist an Idempotency-Key and exact request body before every Operator POST. Read back the account after an uncertain outcome; never replace the key just to retry.
- Create a merchant with onboarding: direct and a password, or onboarding: invitation and no password. Then create projects/stores and issue a project-scoped merchant key for its checkout integration.
| Scope | Access |
|---|---|
| merchants.read / merchants.write | List/read and create/update hosted merchants. |
| users.read / users.write / users.security | Read/create/update users; change passwords or revoke sessions separately. Never creates an operator administrator. |
| invitations.read / invitations.write | List/read, create, replace and revoke one-time invitation/reset links. New users also need users.write; resets also need users.security. |
| credits.read / credits.write / fees.write | Read balances/ledger; grant or correct local credit; set prospective fees. Nonzero starting credits need credits.write. |
| topups.read / topups.write | Read or create hosted-merchant credit checkout requests. No API action can mark one paid. |
| projects.read / projects.write / reports.read | Provision merchant projects, stores, appearance and payment settings; read invoices, wallet balances and financial reports. |
| merchant_credentials.read / merchant_credentials.write | Manage ordinary scoped merchant keys. Powerful: those keys act independently after issuance. |
| events.read / webhooks.write / audit.read / health.read | Read lifecycle history; configure signed lifecycle callbacks; read audit/capabilities/node health. |
Onboarding, credits, permissions and safe retries
| Topic | Rule |
|---|---|
| Credentials | Optional expiry and exact IPv4/IPv6 allowlist; 60 requests/minute by default, configurable 1–600. Every request checks the issuing administrator and current scopes. HTTP 429 includes Retry-After. |
| Isolation | Keys access only their assigned hosted merchants. Creating merchants and viewing installation-wide reports require all-merchants access. The operator’s own business is excluded. |
| First sign-in | Direct accounts acknowledge that the host can access their wallet keys. require_password_change adds a first-login password change. Invitation acceptance requires explicit custody acknowledgement, then normal sign-in. Basic Auth and existing TOTP remain in force. |
| Invitations | New-user links last 48 hours; password-reset links last one hour. Tokens are one-use. Reissuing revokes the old link. SMTP acceptance is not a guarantee of inbox delivery; inspect email_delivery. |
| Safe retries | Every Operator POST needs a 16–128 character key (letters, digits, -, _ or .). Same key and exact URL/body returns the saved outcome. Different bytes return 409. Secrets/links are omitted on replay; rotate or reissue through a new explicit operation when needed. |
| Uncertain outcomes | operator_request_in_progress means an operation is running or was interrupted before its receipt was recorded. Inspect the resource and audit; do not blindly submit a new key. Completed receipts are compacted after 30 days; old keys still cannot run again. |
| Credits and fees | Decimal strings, at most six decimals. starting_credit is a one-time local grant. Adjustments need a signed amount, note and request_id, plus the HTTP retry key. fee_bps=100 means 1%; edits affect future invoices. Grants do not top up the installation’s own prepaid balance. |
| Pausing | enabled=false disables a hosted account and revokes console sessions. payments_paused=true stops new invoices. Existing payment monitoring continues. Project/store creation and automation retain the installation’s credit policy. |
| Not exposed | No wallet secrets, signing, sending, refunds, permanent deletion, TOTP reset, domain changes or server-level configuration. Ordinary invoice requests still use a merchant key and the Merchant API. |
Operator lifecycle webhooks
| Event | Data |
|---|---|
| merchant.created / merchant.updated | merchant_id, enabled, payments_paused, fee_bps. |
| user.created / user.updated | merchant_id, user_id, enabled. Updated event covers email, enabled status and admin role changes. |
| invitation.accepted / password_reset.completed | merchant_id, user_id, invitation_id. |
| topup.settled / credit.balance_changed | merchant_id, ledger_id, kind, amount and balance. Read merchant credit currency or ledger detail when reconciling. |
Operator lifecycle events are separate from invoice IPN/store webhooks. A subscription belongs to the Operator credential that created it, with up to 10 endpoints per key. Only future matching events are queued; use GET /events for retained history.
The body contains event_id, event_type, merchant_id, occurred_at and data. Verify Wholly-Signature over the exact raw body using the endpoint’s one-time signing_secret: HMAC-SHA256(secret, timestamp + '.' + raw_body), header t=...,v1=.... Enforce a short timestamp tolerance.
Use the SDK’s generic signature verifier, not its invoice-notification parser. Then validate merchant_id and event_type, persist/deduplicate event_id transactionally, and return 2xx only after durable acceptance. Wholly-Event-Id must match the signed body. Do not treat unsigned headers as business data.
Delivery is at least once, may arrive out of order, and is attempted up to 8 times. Read current resources to reconcile; occurred_at is not a monotonic sequence. Scope and enabled/expiry settings are rechecked before delivery. Disabled subscriptions pause already queued work but do not queue new events while disabled.
Events and delivery history are retained for 30 days. Installation automation policy can pause delivery. GET /webhooks/{id}/deliveries shows outcome and immutable payload; public API does not force-deliver an expired record.
{
"event_id": "55555555-5555-4555-8555-555555555555",
"event_type": "merchant.created",
"merchant_id": "11111111-1111-4111-8111-111111111111",
"occurred_at": "2026-10-01T12:00:00Z",
"data": {
"merchant_id": "11111111-1111-4111-8111-111111111111",
"enabled": true,
"payments_paused": false,
"fee_bps": 300
}
}Marketplace API
One checkout with protected vendor shares and controlled payouts.
One customer invoice, exact vendor shares and controlled same-asset payouts. Opt in per project. Version 8.0.0 supports Bitcoin mainnet and configured supported EVM native coins / standard ERC-20 tokens, not all receiving networks.
- Enable Marketplace in the project, choose participating stores and leave payouts paused while configuring it.
- Register vendors and independently verify their payout addresses. Destination creation is pending; approval needs destinations.approve.
- Create a separate Marketplace credential in Settings → API access. Ordinary Merchant, Operator and MCP credentials cannot spend Marketplace funds.
- Create an invoice with 1–100 positive decimal-string allocations whose total exactly equals the invoice amount. Every vendor needs an approved destination on a common supported store network.
- Wait for verified allocation availability. Prepare a payout, read its immutable recipients and cost limits, then approve its revision and exact plan_hash. Poll the payout or verify signed events.
| Topic | Rule |
|---|---|
| Custody | Customer funds enter project-controlled wallets first. The marketplace/server owner controls those keys until vendors are paid. This is not direct customer-to-vendor payment, escrow or a vendor login system. |
| Three vendors | A 300 USD invoice with three 100 USD shares and 4% commission allocates 96 USD-equivalent of the quoted crypto to each vendor, retaining 12 USD-equivalent gross commission. Crypto atomic units, not future fiat value, are guaranteed by the plan. |
| Fees | The existing processing charge applies once to the original settled invoice amount/currency. At a configured 1%, that is 3 USD of prepaid credits, leaving 9 USD-equivalent commission before chain fees. Vendor payouts do not create additional customer-sale fees. |
| Allocation | Largest-remainder allocation preserves every atomic unit. Commission rounds down; vendor net is the remainder. Allocation override, invoice override, vendor override and store/project defaults are snapshotted. No new exchange rate is applied at payout. |
| Protected funds | Ordinary sends, sweeps, exchange deposits and credit auto-top-ups exclude Marketplace receiving addresses. Network fees and token gas require separate free native coins. Cached wallet values are advisory; independent live backing is checked before signing. |
| Finality | A positive chain floor and two independent compatible providers are required for Marketplace payout proof, even if checkout accepts zero confirmations or a single scanner. Provider cooldowns defer payment verification/payout, not otherwise-valid invoice creation. |
| Exceptions | Underpayments accepted by ordinary checkout tolerance are held, not over-allocated. A privileged release may approve an exact smaller fully backed split. Overpayment alone never blocks the quoted vendor split on BTC or supported EVM native/ERC-20 methods. Excess stays held separately. Bitcoin returns protected change to the invoice address; separate project funds cover fees and any dust-floor top-up. Mixed methods, late/ambiguous/reorged payments and missing backing need review. |
| Refunds | Prepare a full same-asset refund of one unambiguous confirmed received method, then separately approve it as a payout. Unpaid allocations and commission reverse. Once every vendor is paid, fund the primary wallet address separately; there is no clawback. Partial vendor batches and mixed-asset refunds require reconciliation first. Partial-amount refunds and commission withdrawals are not exposed in 8.0.0. |
| Retries | Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result. |
| Revisions | Mutable records require their current revision. Approve/resume also requires expected_plan_hash and confirm:true. Preparation reserves funds for 15 minutes; approval allows one hour. Expired unsigned work releases safely. Signed/uncertain transfers retain identities and reservations. |
| Automatic payouts | Off by default. Per-asset policies authorize approved destinations, positive minimum, per-payout/rolling-24-hour principal and native-fee/gas limits, interval and confirmations. Changing/revoking authority stops unsent work; observation continues. Limits include reserved and uncertain plans. Billing-paused policies are rescheduled fairly. An invalid invoice is held for review without blocking other eligible invoice groups. |
| Pause and recovery | Low credits and transfer safety gates still pause spending. Monitoring and ledger reconciliation continue. Resume reviews the same retained plan. A canonical EVM revert proven by two independent providers can be replaced after explicit approval, within the remaining fee budget; original hashes and fees stay recorded. Unknown outcomes or token delivery mismatches remain held. Back up the whole database and keys; reconcile submitted hashes after restore before allowing payouts. |
| Identifiers and pagination | Use project UUID from Project Settings and store UUID from Stores → Basic → API IDs. Vendor/payout/asset IDs are returned by this API. invoice_id is the public invoice UUID. Paged resources return data and pagination {page,per_page,total,pages}; defaults 1/25, maximum 100 rows. Exact monetary values are strings. |
| Credential scope | Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps. |
Marketplace permissions
marketplace.read · vendors.write · destinations.write · destinations.approve · invoices.write · policies.write · commissions.override · payouts.write · payouts.approve · webhooks.write · reconciliation.write
Allocation states
pending · held · available · reserved · paid · refunding · refunded · shortfall
Payout states
awaiting_approval · queued · funding_gas · sending · confirming · partially_paid · paid · needs_attention · cancelled · expired
Marketplace events
Separate Marketplace signing secret, using the same Wholly signature headers and raw-body HMAC verification algorithm as invoice callbacks. Verify timestamp/signature, deduplicate event_id and return 2xx promptly. Delivery is at least once, up to eight attempts; history can redeliver the same event. Endpoint edits cancel old unsent deliveries instead of redirecting customer data. Invoice settlement is not vendor payout completion. Never send money solely because an external callback says to.
marketplace.invoice.createdmarketplace.allocations.availablemarketplace.allocations.heldmarketplace.payout.approvedmarketplace.payout.broadcastmarketplace.payout.confirmedmarketplace.payout.partialmarketplace.payout.failedmarketplace.payout.cancelledmarketplace.refund.preparedmarketplace.refund.confirmed
{
"event_id": "11111111-1111-4111-8111-111111111111",
"event_type": "marketplace.payout.confirmed",
"payload_version": 1,
"project_id": "YOUR_PROJECT_ID",
"occurred_at": "2026-10-06T12:00:00Z",
"data": {
"payout_id": "YOUR_PAYOUT_ID",
"asset_id": "YOUR_ASSET_ID",
"amount_atomic": "28800000",
"transactions": [
{
"transaction_id": "EXAMPLE_TRANSACTION_ID",
"explorer_url": "https://mempool.space/tx/EXAMPLE_TRANSACTION_ID"
}
]
}
}Errors & limits
Handle validation, quotas and retries predictably.
Check HTTP status and Content-Type before parsing a response. For a 429, wait at least the Retry-After duration before retrying.
| Limit | Details |
|---|---|
| Request rate | Per-credential quota: default 120 requests per UTC minute, configurable from 1 to 6000 in Settings → API. All authenticated v1 reads and writes, including idempotent retries and authorization/validation failures after authentication, share the allowance across domains, projects and processes. Invalid credentials, console routes and public checkout do not consume it. |
| Rate-limit headers | Authenticated v1 responses include X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds at the next UTC-minute boundary). Excess requests return JSON 429 rate_limit_exceeded and integer-second Retry-After. Wait at least that long and add retry jitter. Fixed windows permit bursts at minute boundaries; this is not a requests-per-second guarantee. |
| Merchant body | 32 KiB maximum at the application router. The edge may reject an oversized request before a JSON error envelope is produced. |
| Invoice list | limit defaults to 50 and accepts 1–100; offset accepts 0–1,000,000. Search is at most 100 characters. Results are newest first and include total/has_more metadata. |
| Store methods | At most 64 asset selections per store, enough for all 30 native chains plus the bounded verified-token catalog. Project policy, scanner capability, and a ready backed-up chain wallet still gate invoice creation. |
| Token discovery | Candidate limit defaults to 50 and accepts 1–100. Discovery results are not payment assets until on-chain verification succeeds. |
| Registered project tokens | At most 20 durable token assets per project. Already-registered assets can be reused without consuming another slot. |
| Idempotency | Required for invoice creation. 1–128 visible ASCII characters without spaces; keys are unique per store, and a replay must use the original credential and exact raw body. |
| Metadata | JSON object only, maximum 4,096 encoded bytes and five levels of nesting. |
| Callbacks | Public HTTPS URL up to 2,048 bytes. Notification request bodies are capped at 256 KiB; retained invoice event payloads are capped at 64 KiB with bounded payment histories. |
| Checkout assets | QR SVG responses are private and no-store because an underpayment changes the exact remainder. Revisioned PNG logos cache publicly for one year and are immutable. |
| API edge | Managed API upstream requests have a 30-second read timeout. Design callers for explicit timeouts shorter than their job budget. |
| Non-JSON failures | Malformed UUID/query extraction, wrong methods, and the 32 KiB length guard can return framework text/empty responses. Unknown /v1 paths currently return 404 console HTML; validate status and Content-Type before parsing. |
Error reference
| HTTP | Error code | Meaning |
|---|---|---|
| 400 | invalid_reconciliation_action | An exception status, reason, search or history-page filter is invalid. |
| 500 | reconciliation_unavailable | The exception queue or evidence could not be loaded. Retry the read with backoff. |
| 402 | billing_required | A verified paired credit account and live authorization are required for each new invoice. Insufficient prepaid credits do not block creation or incoming payments: IPN, webhooks and Sweep pause instead, while fees continue to accrue. Creation remains blocked for suspended accounts, expired/invalid billing verification, an unreachable credit service or an unauthorized invoice fiat basis. Fees use the original invoice fiat amount, not crypto received, spread, overpayment or network fees. That amount and independent conversion are registered before checkout creation. Existing monitoring and invoice retrieval continue during outages. After funding, queued notifications resume within their normal payload-retention period, and enabled sweep rules resume. Check Settings → Fees and retry failed creation with the same Idempotency-Key. |
| 400 | invalid_json | Malformed JSON, an unknown field, or a body that does not match the documented request. |
| 400 | idempotency_key_required | Create invoice omitted Idempotency-Key. |
| 400 | invalid_idempotency_key | Key is empty, over 128 bytes, non-ASCII, contains whitespace, or contains a control byte. |
| 400 | invalid_payment_request | A validated field or selected active method failed. Read error.message and error.details.payment_methods (PaymentMethodIssue[]) for the exact blocker. SDK 2.4.0+ adds safe actionable exception summaries and issue helpers; older PHP SDKs expose getApiMessage(). |
| 400 | invalid_invoice_status | List status is outside the six documented invoice states. |
| 400 | invalid_callback_url | The effective IPN target failed HTTPS, public-address, DNS, or SSRF validation. |
| 400 | invalid_wallet_request | A wallet/address preparation input is invalid. |
| 400 | invalid_token_asset | Token chain, candidate query, CoinGecko identity, catalog metadata, or contract/mint input is invalid. |
| 401 | authentication_required | Bearer token is absent, malformed, disabled, rotated, or unknown. |
| 403 | source_ip_denied | Credential IP restriction does not include the request's exact public source address. |
| 403 | source_ip_not_allowed | The hostname's source IP restriction excludes this client. An administrator can manage live-host allowlists in Settings → System; these apply in addition to credential IP restrictions. |
| 503 | source_access_unavailable | Hostname access verification is temporarily unavailable. Retry later; restrictions fail closed. |
| 403 / 409 / 500 | merchant_api_access_denied | Authorization failed: permission/project scope can be 403, disabled project/store can be 409, and an authorization backend failure can be 500. Operator receiving wallets are reserved for the Operator panel, not merchant API credentials or MCP, even with an older explicit project grant. |
| 403 | project_access_denied | A transactional create-time recheck found that the credential no longer has access to the project. |
| 404 | invoice_not_found | No invoice with that public ID exists in the authorized project, or checkout cannot expose it. |
| 404 | payment_resource_not_found | A project, store, asset, or wallet needed while preparing the invoice no longer exists. |
| 404 | token_candidate_not_found | The project is unavailable or the token is no longer present in the current matched discovery catalog. |
| 409 | idempotency_conflict | The store-scoped key already exists and either the credential differs or the exact raw request bytes differ. |
| 409 | store_unavailable | Project/store is disabled or unavailable. |
| 409 | no_ready_payment_methods | No store method is ready. Read error.message and error.details.payment_methods for chain_slug, asset_ticker and reason_code. Wallet backup/activation, installed adapter and pricing must be valid. From 6.0.6, scanner cooldowns, failed or stale health checks and missing provider quorum do not block creation. |
| 409 | payment_method_unavailable | A selected method became unavailable during the atomic create-time recheck. |
| 409 | store_payment_method_not_selected | A store confirmation override was requested for an asset that is not currently selected by that store. |
| 409 | wallet_unavailable | A payment wallet became unavailable during the atomic create-time recheck. |
| 409 | ipn_secret_required | An effective IPN URL exists but the store has no IPN signing secret. |
| 409 | payment_resource_not_ready | A required payment asset or wallet is disabled, unbacked-up, awaiting shared-account activation proof, exhausted, or otherwise not ready. |
| 409 | account_activation_unverified | XRP Ledger or Stellar account activation could not be proven against the configured number of healthy mainnet endpoints (default 2, optional 1); fund the exact account and retry verification. |
| 400 | invalid_monero_wallet_rpc | The HTTPS endpoint, exact mainnet primary address, label, or complete Digest/Basic/header authentication input is invalid. |
| 404 | monero_wallet_rpc_not_found | The project-scoped Monero wallet-RPC binding does not exist. |
| 409 | monero_wallet_rpc_not_ready | The Monero asset, two-daemon quorum, immutable binding, or explicit backup/view-only attestation is not ready. |
| 409 | monero_wallet_rpc_unavailable | Invoice creation requires an active, verified, attested project Monero wallet-RPC binding with a valid server-side credential. |
| 503 | lightning_unavailable | The store's only ready method is Lightning and its wallet or quote could not be verified. Retry with the same idempotency key. When another ready on-chain method exists, an unavailable Lightning method is omitted instead. |
| 422 | monero_wallet_rpc_verification_failed | The exact wallet, HTTPS pinning, synchronization, mainnet daemon quorum, or gateway method-denial proof failed. |
| 503 | monero_wallet_rpc_failed | The external watch-only wallet-RPC could not safely provision and re-read the invoice subaddress; no fallback address is fabricated. |
| 409 | token_chain_not_ready | The native chain asset is disabled, the discovery mapping changed during verification, or the project already has the current maximum of 20 registered token assets. |
| 503 | dex_price_unavailable | DEX provider unavailable, busy, rate-limited, stale response or malformed data. Retry after one minute; fixed pricing remains available. |
| 422 | invalid_dex_price | Invalid price-mode combination or selected pool cannot provide a qualifying price for the exact contract. Choose another pool or fixed USD pricing. |
| 422 | token_verification_failed | Every eligible node failed chain identity, contract code, decimals, balance-query, or mint verification. |
| 422 | invalid_store_confirmation_policy | The store override is unavailable for this finality mode, outside the returned chain-aware bounds, or requests unsupported zero-confirmation acceptance. |
| 409 | invoice_not_payable | The checkout invoice is terminal or its payment deadline has passed. |
| 409 | invoice_payment_method_locked | A valid payment already selected a different asset; continue with active_payment_method_id. |
| 409 | payment_method_not_payable | The selected method is complete or no longer accepts another payment. |
| 422 | payment_qr_unavailable | The checkout payment request is too large to encode as an SVG QR image. |
| 503 | payment_rates_unavailable | No fresh trustworthy quote is available for any ready payment method. |
| 500 | authentication_unavailable | Bearer authentication could not safely read or validate its stored credential. |
| 429 | rate_limit_exceeded | This credential exhausted its current UTC-minute allowance. Wait at least Retry-After seconds; retry invoice creation with the same idempotency key. |
| 500 | database_error / internal_error | Transient server-side failure; retry safely with the same idempotency key. |
API overview
Choose an endpoint for its fields, examples and response.
Invoices
POSTCreate invoice/v1/projects/{project_id}/stores/{store_id}/invoicesGETList invoices/v1/projects/{project_id}/invoicesGETRetrieve invoice/v1/projects/{project_id}/invoices/{invoice_id}GETList invoice payments/v1/projects/{project_id}/invoices/{invoice_id}/paymentsPayment methods
GETList project payment assets/v1/projects/{project_id}/payment-assetsPUTUpdate project asset policy/v1/projects/{project_id}/payment-assets/{asset_id}GETBrowse payment-token candidates/v1/projects/{project_id}/payment-token-candidatesPOSTVerify and register token/v1/projects/{project_id}/payment-token-assetsGETFind custom token DEX pools/v1/projects/{project_id}/payment-token-dex-poolsPOSTAdd or reprice custom token/v1/projects/{project_id}/payment-token-assets/customGETList store payment methods/v1/projects/{project_id}/stores/{store_id}/payment-assetsPUTReplace store payment methods/v1/projects/{project_id}/stores/{store_id}/payment-assetsPUTSet a store confirmation policy/v1/projects/{project_id}/stores/{store_id}/payment-assets/{asset_id}/confirmation-policyWallets
GETList project wallets and balances/v1/projects/{project_id}/walletsReconciliation
GETList payment exceptions/v1/projects/{project_id}/reconciliationGETRead reconciliation evidence/v1/projects/{project_id}/reconciliation/{invoice_id}Marketplace API
GETCapabilities/v1/marketplace/capabilitiesGETMarketplace report/v1/marketplace/overviewGETProject settings/v1/marketplace/settingsPOSTSave project settings/v1/marketplace/settingsGETList vendors/v1/marketplace/vendorsPOSTCreate vendor/v1/marketplace/vendorsGETGet vendor/v1/marketplace/vendors/{vendor_id}POSTUpdate vendor/v1/marketplace/vendors/{vendor_id}/updatePOSTArchive vendor/v1/marketplace/vendors/{vendor_id}/archiveGETVendor destinations/v1/marketplace/vendors/{vendor_id}/destinationsPOSTAdd payout destination/v1/marketplace/vendors/{vendor_id}/destinationsPOSTApprove destination/v1/marketplace/vendors/{vendor_id}/destinations/{destination_id}/approvePOSTDisable destination/v1/marketplace/vendors/{vendor_id}/destinations/{destination_id}/disableGETVendor balances/v1/marketplace/vendors/{vendor_id}/balancesGETMarketplace invoices/v1/marketplace/invoicesPOSTCreate multi-vendor invoice/v1/marketplace/invoicesGETInvoice and allocations/v1/marketplace/invoices/{invoice_id}GETInvoice payment evidence/v1/marketplace/invoices/{invoice_id}/paymentsGETInvoice allocations/v1/marketplace/invoices/{invoice_id}/allocationsPOSTCancel unpaid invoice/v1/marketplace/invoices/{invoice_id}/cancelPOSTHold allocations/v1/marketplace/invoices/{invoice_id}/allocations/holdPOSTReview and release allocations/v1/marketplace/invoices/{invoice_id}/allocations/releasePOSTRebind unpaid destination/v1/marketplace/invoices/{invoice_id}/allocations/{allocation_id}/destinationPOSTPrepare full refund/v1/marketplace/invoices/{invoice_id}/refundGETFind payout allocations/v1/marketplace/allocationsGETProject ledger balances/v1/marketplace/balancesGETImmutable journal/v1/marketplace/ledgerGETPayout history/v1/marketplace/payoutsPOSTPreview allocation plan/v1/marketplace/payouts/previewPOSTPrepare payout/v1/marketplace/payoutsGETPayout details/v1/marketplace/payouts/{payout_id}POSTApprove and send/v1/marketplace/payouts/{payout_id}/approvePOSTReview and resume payout/v1/marketplace/payouts/{payout_id}/resumePOSTCancel unsigned work/v1/marketplace/payouts/{payout_id}/cancelGETAutomatic payout policies/v1/marketplace/policiesPOSTAuthorize payout policy/v1/marketplace/policiesGETMarketplace webhooks/v1/marketplace/webhooksPOSTCreate Marketplace webhook/v1/marketplace/webhooksPOSTUpdate Marketplace webhook/v1/marketplace/webhooks/{webhook_id}/updatePOSTDisable Marketplace webhook/v1/marketplace/webhooks/{webhook_id}/disableGETMarketplace events/v1/marketplace/eventsGETRead retained event/v1/marketplace/events/{event_id}GETMarketplace delivery history/v1/marketplace/webhook-deliveriesGETDelivery details/v1/marketplace/webhook-deliveries/{delivery_id}POSTRedeliver same event/v1/marketplace/webhook-deliveries/{delivery_id}/resendOperator API
GETCapabilities/v1/operator/capabilitiesGETHealth/v1/operator/healthGETList Merchants/v1/operator/merchantsPOSTCreate Merchant/v1/operator/merchantsGETGet Merchant/v1/operator/merchants/{merchant_id}POSTUpdate Merchant/v1/operator/merchants/{merchant_id}GETList Users/v1/operator/merchants/{merchant_id}/usersPOSTCreate User/v1/operator/merchants/{merchant_id}/usersGETGet User/v1/operator/merchants/{merchant_id}/users/{user_id}POSTUpdate User/v1/operator/merchants/{merchant_id}/users/{user_id}POSTSet User Password/v1/operator/merchants/{merchant_id}/users/{user_id}/passwordPOSTRevoke User Sessions/v1/operator/merchants/{merchant_id}/users/{user_id}/revoke-sessionsGETList Invitations/v1/operator/merchants/{merchant_id}/invitationsPOSTCreate Invitation/v1/operator/merchants/{merchant_id}/invitationsGETGet Invitation/v1/operator/invitations/{invitation_id}POSTResend Invitation/v1/operator/invitations/{invitation_id}/resendPOSTRevoke Invitation/v1/operator/invitations/{invitation_id}/revokeGETGet Credits/v1/operator/merchants/{merchant_id}/creditsGETList Credit Ledger/v1/operator/merchants/{merchant_id}/credits/ledgerPOSTAdjust Credits/v1/operator/merchants/{merchant_id}/credits/adjustmentsGETList Topups/v1/operator/merchants/{merchant_id}/topupsPOSTCreate Topup/v1/operator/merchants/{merchant_id}/topupsGETGet Topup/v1/operator/merchants/{merchant_id}/topups/{topup_id}GETReports/v1/operator/reportsGETList Audit/v1/operator/auditGETList Events/v1/operator/eventsGETList Webhooks/v1/operator/webhooksPOSTCreate Webhook/v1/operator/webhooksPOSTUpdate Webhook/v1/operator/webhooks/{webhook_id}POSTRotate Webhook Secret/v1/operator/webhooks/{webhook_id}/rotateGETList Webhook Deliveries/v1/operator/webhooks/{webhook_id}/deliveriesGETList Projects/v1/operator/merchants/{merchant_id}/projectsPOSTCreate Project/v1/operator/merchants/{merchant_id}/projectsGETGet Project/v1/operator/merchants/{merchant_id}/projects/{project_id}POSTUpdate Project/v1/operator/merchants/{merchant_id}/projects/{project_id}GETList Stores/v1/operator/merchants/{merchant_id}/projects/{project_id}/storesPOSTCreate Store/v1/operator/merchants/{merchant_id}/projects/{project_id}/storesGETGet Store/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}POSTUpdate Store/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}GETGet Store Appearance/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/checkout-appearancePOSTUpdate Store Appearance/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/checkout-appearanceGETList Store Payment Assets/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/payment-assetsPOSTUpdate Store Payment Assets/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/payment-assetsGETList Store Webhooks/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/webhooksPOSTCreate Store Webhook/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/webhooksPOSTUpdate Store Webhook/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/webhooks/{webhook_id}GETList Invoices/v1/operator/merchants/{merchant_id}/projects/{project_id}/invoicesGETGet Invoice/v1/operator/merchants/{merchant_id}/projects/{project_id}/invoices/{invoice_id}GETList Wallets/v1/operator/merchants/{merchant_id}/projects/{project_id}/walletsGETList Wallet Addresses/v1/operator/merchants/{merchant_id}/projects/{project_id}/wallets/{wallet_id}/addressesGETList Merchant Credentials/v1/operator/merchants/{merchant_id}/api-credentialsPOSTCreate Merchant Credential/v1/operator/merchants/{merchant_id}/api-credentialsPOSTUpdate Merchant Credential/v1/operator/merchants/{merchant_id}/api-credentials/{credential_id}POSTRotate Merchant Credential/v1/operator/merchants/{merchant_id}/api-credentials/{credential_id}/rotatePOSTRevoke Merchant Credential/v1/operator/merchants/{merchant_id}/api-credentials/{credential_id}/revokePOSTCheck invitation token/v1/onboarding/invitations/checkPOSTAccept invitation or password reset/v1/onboarding/invitations/acceptCheckout
GETCheckout shell/GETHosted checkout page/invoice/{invoice_id}GETCheckout-safe invoice/checkout-api/invoices/{invoice_id}GETStore checkout preview/invoice/preview/{project_id}GETCheckout preview data/checkout-api/previews/{project_id}GETStore checkout image/checkout-api/invoices/{invoice_id}/appearance-images/{kind}/{revision}/image.pngGETStore preview image/checkout-api/previews/{project_id}/stores/{store_id}/appearance-images/{kind}/{revision}/image.pngGETRevisioned preview logo/checkout-api/previews/{project_id}/logo/{revision}/image.pngGETPayment QR image/checkout-api/invoices/{invoice_id}/payment-methods/{intent_id}/qr.svgGETRevisioned checkout logo/checkout-api/invoices/{invoice_id}/logo/{revision}/image.pngService
GETAPI service discovery/GETService health/healthzGETCapabilities/v1/operator/capabilitiesRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/capabilities" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/capabilities", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/capabilities");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/capabilities",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"api_version": "v1",
"operator_version": "7.4.0",
"scopes": [
"health.read"
],
"all_merchants": false,
"merchant_ids": [
"11111111-1111-4111-8111-111111111111"
],
"onboarding": [
"direct",
"invitation"
],
"write_methods": [
"POST"
],
"idempotency_required": true
}Response fields
Usage notes
- Requires health.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETHealth/v1/operator/healthRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/health" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/health", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/health");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/health",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"version": "7.4.0",
"nodes": []
}Response fields
Usage notes
- Requires health.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETList Merchants/v1/operator/merchantsRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Query parameters
pageoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
searchoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}Response fields
Usage notes
- Requires merchants.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTCreate Merchant/v1/operator/merchantsRead + write
Create a hosted merchant and first administrator atomically, directly with a password or by invitation.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Example shop",
"email": "admin@example.test",
"onboarding": "direct",
"password": "REPLACE_WITH_A_UNIQUE_TEMPORARY_PASSWORD",
"require_password_change": true,
"currency": "EUR",
"starting_credit": "0",
"external_id": "customer-1042"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"name": "Example shop",
"email": "admin@example.test",
"onboarding": "direct",
"password": "REPLACE_WITH_A_UNIQUE_TEMPORARY_PASSWORD",
"require_password_change": true,
"currency": "EUR",
"starting_credit": "0",
"external_id": "customer-1042"
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"name": "Example shop",
"email": "admin@example.test",
"onboarding": "direct",
"password": "REPLACE_WITH_A_UNIQUE_TEMPORARY_PASSWORD",
"require_password_change": true,
"currency": "EUR",
"starting_credit": "0",
"external_id": "customer-1042"
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"name": "Example shop",
"email": "admin@example.test",
"onboarding": "direct",
"password": "REPLACE_WITH_A_UNIQUE_TEMPORARY_PASSWORD",
"require_password_change": true,
"currency": "EUR",
"starting_credit": "0",
"external_id": "customer-1042"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
onboardingdirect | invitationrequireddirect requires password, sends no invitation email. invitation omits password.
passwordstring · direct only12–128 characters (at most 512 UTF-8 bytes); never returned or emailed. Use require_password_change for temporary passwords.
require_password_changebooleanoptionalRequires a new password at first login. Every directly provisioned account must acknowledge hosted wallet custody.
currencyfiat codeoptionalPrepaid account currency; defaults to regional currency and cannot subsequently change.
fee_bpsintegeroptional0–10000; 100 means 1%. Uses the operator default when omitted. Requires fees.write.
starting_creditdecimal stringoptionalExact one-time local grant. Nonzero requires credits.write. Does not refill the operator's installation balance.
send_invitation_emailbooleanoptionalInvitation only. Requires configured SMTP; response distinguishes relay acceptance from account creation.
Response 201 or 200 application/json
Example response
{
"merchant_id": "11111111-1111-4111-8111-111111111111",
"user_id": "22222222-2222-4222-8222-222222222222",
"merchant": {
"id": "11111111-1111-4111-8111-111111111111",
"name": "Example shop",
"currency": "EUR",
"balance": "0"
},
"onboarding": "direct",
"access_link": null,
"email_delivery": {
"status": "not_requested"
},
"custody_acceptance_required": true
}Response fields
Usage notes
- Requires merchants.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Global merchant access required. Explicit fee overrides also need fees.write; nonzero starting_credit needs credits.write. Invitation onboarding also needs invitations.write. No automatic login, no Basic Auth bypass, no retroactive credit on retry.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETGet Merchant/v1/operator/merchants/{merchant_id}Read only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"id": "11111111-1111-4111-8111-111111111111",
"name": "Example shop",
"currency": "EUR",
"balance": "10",
"fee_bps": 300,
"enabled": true,
"payments_paused": false,
"external_id": "customer-1042"
}Response fields
Usage notes
- Requires merchants.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTUpdate Merchant/v1/operator/merchants/{merchant_id}Read + write
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"payments_paused": true
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"payments_paused": true
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"payments_paused": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"payments_paused": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
namefieldsoptionalDisabling revokes sessions. payments_paused blocks new invoices, not scanning existing payments. Fee edits require fees.write and affect future invoices; account currency cannot change.Details
Disabling revokes sessions. payments_paused blocks new invoices, not scanning existing payments. Fee edits require fees.write and affect future invoices; account currency cannot change.
enabledfieldsoptionalDisabling revokes sessions. payments_paused blocks new invoices, not scanning existing payments. Fee edits require fees.write and affect future invoices; account currency cannot change.Details
Disabling revokes sessions. payments_paused blocks new invoices, not scanning existing payments. Fee edits require fees.write and affect future invoices; account currency cannot change.
payments_pausedfieldsoptionalDisabling revokes sessions. payments_paused blocks new invoices, not scanning existing payments. Fee edits require fees.write and affect future invoices; account currency cannot change.Details
Disabling revokes sessions. payments_paused blocks new invoices, not scanning existing payments. Fee edits require fees.write and affect future invoices; account currency cannot change.
fee_bpsfieldsoptionalDisabling revokes sessions. payments_paused blocks new invoices, not scanning existing payments. Fee edits require fees.write and affect future invoices; account currency cannot change.Details
Disabling revokes sessions. payments_paused blocks new invoices, not scanning existing payments. Fee edits require fees.write and affect future invoices; account currency cannot change.
external_idfieldsoptionalDisabling revokes sessions. payments_paused blocks new invoices, not scanning existing payments. Fee edits require fees.write and affect future invoices; account currency cannot change.Details
Disabling revokes sessions. payments_paused blocks new invoices, not scanning existing payments. Fee edits require fees.write and affect future invoices; account currency cannot change.
Response 200 application/json
Example response
{
"id": "11111111-1111-4111-8111-111111111111",
"name": "Example shop",
"currency": "EUR",
"balance": "10",
"fee_bps": 300,
"enabled": true,
"payments_paused": false
}Response fields
Usage notes
- Requires merchants.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETList Users/v1/operator/merchants/{merchant_id}/usersRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Query parameters
pageoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
searchoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}Response fields
Usage notes
- Requires users.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTCreate User/v1/operator/merchants/{merchant_id}/usersRead + write
Add a merchant administrator or a user restricted to selected projects.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "staff@example.test",
"display_name": "Store team",
"onboarding": "invitation",
"access_level": "projects",
"project_ids": [
"11111111-1111-4111-8111-111111111111"
],
"send_invitation_email": false
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"email": "staff@example.test",
"display_name": "Store team",
"onboarding": "invitation",
"access_level": "projects",
"project_ids": [
"11111111-1111-4111-8111-111111111111"
],
"send_invitation_email": false
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"email": "staff@example.test",
"display_name": "Store team",
"onboarding": "invitation",
"access_level": "projects",
"project_ids": [
"11111111-1111-4111-8111-111111111111"
],
"send_invitation_email": false
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"email": "staff@example.test",
"display_name": "Store team",
"onboarding": "invitation",
"access_level": "projects",
"project_ids": [
"11111111-1111-4111-8111-111111111111"
],
"send_invitation_email": false
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
require_password_changesame as merchant creationInvitation creation additionally requires invitations.write.
send_invitation_emailsame as merchant creationInvitation creation additionally requires invitations.write.
access_leveladmin | projectsoptionaladmin is only this merchant's administrator, never the installation/operator administrator.
project_ids[]UUID[]Only projects owned by the merchant. Required selections for project-restricted access; never cross-tenant.
Response 201 or 200 application/json
Example response
{
"user": {
"id": "22222222-2222-4222-8222-222222222222",
"merchant_id": "11111111-1111-4111-8111-111111111111",
"email": "admin@example.test",
"display_name": "Shop administrator",
"access_level": "admin",
"project_ids": [],
"enabled": true,
"password_setup_required": false,
"custody_acceptance_required": true,
"password_change_required": true
},
"user_id": "22222222-2222-4222-8222-222222222222",
"access_link": null,
"email_delivery": {
"status": "not_requested"
},
"merchant_id": "11111111-1111-4111-8111-111111111111"
}Response fields
userobject
user_idstringaccess_linkJSON | nullmerchant_idstringUsage notes
- Requires users.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETGet User/v1/operator/merchants/{merchant_id}/users/{user_id}Read only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"id": "22222222-2222-4222-8222-222222222222",
"merchant_id": "11111111-1111-4111-8111-111111111111",
"email": "admin@example.test",
"display_name": "Shop administrator",
"access_level": "admin",
"project_ids": [],
"enabled": true,
"password_setup_required": false,
"custody_acceptance_required": true,
"password_change_required": true
}Response fields
Usage notes
- Requires users.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTUpdate User/v1/operator/merchants/{merchant_id}/users/{user_id}Read + write
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"display_name": "Store manager"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"display_name": "Store manager"
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"display_name": "Store manager"
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"display_name": "Store manager"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
emailfieldsoptionalUpdates supplied fields; last-admin protection remains. Passwords have a separate users.security operation.
display_namefieldsoptionalUpdates supplied fields; last-admin protection remains. Passwords have a separate users.security operation.
enabledfieldsoptionalUpdates supplied fields; last-admin protection remains. Passwords have a separate users.security operation.
access_levelfieldsoptionalUpdates supplied fields; last-admin protection remains. Passwords have a separate users.security operation.
project_idsfieldsoptionalUpdates supplied fields; last-admin protection remains. Passwords have a separate users.security operation.
default_timezonefieldsoptionalUpdates supplied fields; last-admin protection remains. Passwords have a separate users.security operation.
Response 200 application/json
Example response
{
"id": "22222222-2222-4222-8222-222222222222",
"merchant_id": "11111111-1111-4111-8111-111111111111",
"email": "admin@example.test",
"display_name": "Shop administrator",
"access_level": "admin",
"project_ids": [],
"enabled": true,
"password_setup_required": false,
"custody_acceptance_required": true,
"password_change_required": true
}Response fields
Usage notes
- Requires users.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTSet User Password/v1/operator/merchants/{merchant_id}/users/{user_id}/passwordRead + write
Set a hosted account password and revoke sessions. Existing TOTP is retained.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID/password" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"password": "REPLACE_WITH_A_NEW_UNIQUE_PASSWORD",
"require_password_change": true
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"password": "REPLACE_WITH_A_NEW_UNIQUE_PASSWORD",
"require_password_change": true
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID/password", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"password": "REPLACE_WITH_A_NEW_UNIQUE_PASSWORD",
"require_password_change": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID/password");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"password": "REPLACE_WITH_A_NEW_UNIQUE_PASSWORD",
"require_password_change": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID/password",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"user_id": "11111111-1111-4111-8111-111111111111",
"sessions_revoked": true,
"totp_preserved": true
}Response fields
Usage notes
- Requires users.security; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTRevoke User Sessions/v1/operator/merchants/{merchant_id}/users/{user_id}/revoke-sessionsRead + write
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID/revoke-sessions" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID/revoke-sessions", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID/revoke-sessions");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID/revoke-sessions",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"user_id": "11111111-1111-4111-8111-111111111111",
"sessions_revoked": true,
"totp_preserved": true
}Response fields
Usage notes
- Requires users.security; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETList Invitations/v1/operator/merchants/{merchant_id}/invitationsRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Query parameters
pageoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
searchoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/invitations" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/invitations", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/invitations");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/invitations",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}Response fields
Usage notes
- Requires invitations.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTCreate Invitation/v1/operator/merchants/{merchant_id}/invitationsRead + write
Create or replace a one-time invitation or password-reset link.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/invitations" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"user_id": "11111111-1111-4111-8111-111111111111",
"send_email": false
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"user_id": "11111111-1111-4111-8111-111111111111",
"send_email": false
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/invitations", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"user_id": "11111111-1111-4111-8111-111111111111",
"send_email": false
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/invitations");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"user_id": "11111111-1111-4111-8111-111111111111",
"send_email": false
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/invitations",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
user_idUUID, booleanIssue/replace a one-time link for an existing account. Activated users receive a one-hour reset link and require users.security.
send_emailUUID, booleanIssue/replace a one-time link for an existing account. Activated users receive a one-hour reset link and require users.security.
new user fieldsalternative to user_idUse email, display_name, access_level and project_ids to create an invited user; requires users.write.
Response 201 or 200 application/json
Example response
{
"user_id": "22222222-2222-4222-8222-222222222222",
"email": "admin@example.test",
"kind": "invitation",
"expires_at": "2026-10-03T12:00:00Z",
"url": "https://merchant.example.com/account-access.html#token=YOUR_PRIVATE_INVITATION_TOKEN"
}Response fields
Usage notes
- Requires invitations.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETGet Invitation/v1/operator/invitations/{invitation_id}Read only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
invitation_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"id": "44444444-4444-4444-8444-444444444444",
"user_id": "22222222-2222-4222-8222-222222222222",
"merchant_id": "11111111-1111-4111-8111-111111111111",
"kind": "invitation",
"status": "pending",
"created_at": "2026-10-01T12:00:00Z",
"expires_at": "2026-10-03T12:00:00Z"
}Response fields
Usage notes
- Requires invitations.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTResend Invitation/v1/operator/invitations/{invitation_id}/resendRead + write
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
invitation_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID/resend" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"send_email": false
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"send_email": false
}`;
const response = await fetch("https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID/resend", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"send_email": false
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID/resend");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"send_email": false
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID/resend",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
send_emailbooleanoptionalReplaces the prior token, never adds credit. Returns a newly generated link once. An already activated account needs users.security.
Response 200 application/json
Example response
{
"user_id": "22222222-2222-4222-8222-222222222222",
"email": "admin@example.test",
"kind": "invitation",
"expires_at": "2026-10-03T12:00:00Z",
"url": "https://merchant.example.com/account-access.html#token=YOUR_PRIVATE_INVITATION_TOKEN"
}Response fields
Usage notes
- Requires invitations.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTRevoke Invitation/v1/operator/invitations/{invitation_id}/revokeRead + write
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
invitation_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID/revoke" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{}`;
const response = await fetch("https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID/revoke", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID/revoke");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID/revoke",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"revoked": true,
"invitation_id": "44444444-4444-4444-8444-444444444444"
}Response fields
Usage notes
- Requires invitations.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETGet Credits/v1/operator/merchants/{merchant_id}/creditsRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"id": "11111111-1111-4111-8111-111111111111",
"name": "Example shop",
"currency": "EUR",
"balance": "10",
"fee_bps": 300,
"enabled": true,
"payments_paused": false,
"external_id": "customer-1042"
}Response fields
Usage notes
- Requires credits.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETList Credit Ledger/v1/operator/merchants/{merchant_id}/credits/ledgerRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Query parameters
pageoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
qoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits/ledger" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits/ledger", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits/ledger");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits/ledger",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}Response fields
Usage notes
- Requires credits.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTAdjust Credits/v1/operator/merchants/{merchant_id}/credits/adjustmentsRead + write
Append a reasoned grant or correction to this merchant's prepaid ledger.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits/adjustments" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"amount": "10.00",
"note": "Promotional credit",
"request_id": "11111111-1111-4111-8111-111111111111"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"amount": "10.00",
"note": "Promotional credit",
"request_id": "11111111-1111-4111-8111-111111111111"
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits/adjustments", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"amount": "10.00",
"note": "Promotional credit",
"request_id": "11111111-1111-4111-8111-111111111111"
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits/adjustments");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"amount": "10.00",
"note": "Promotional credit",
"request_id": "11111111-1111-4111-8111-111111111111"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits/adjustments",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
amountsigned decimal stringrequiredPositive grant or negative correction, up to six decimals in merchant credit currency. Not an on-chain transfer.
Response 200 application/json
Example response
{
"balance": "25"
}Response fields
balancestringUsage notes
- Requires credits.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETList Topups/v1/operator/merchants/{merchant_id}/topupsRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Query parameters
pageoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
searchoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}Response fields
Usage notes
- Requires topups.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTCreate Topup/v1/operator/merchants/{merchant_id}/topupsRead + write
Create a checkout for prepaid credit; never manually mark it as paid.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"amount": "25.00",
"request_id": "11111111-1111-4111-8111-111111111111"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"amount": "25.00",
"request_id": "11111111-1111-4111-8111-111111111111"
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"amount": "25.00",
"request_id": "11111111-1111-4111-8111-111111111111"
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"amount": "25.00",
"request_id": "11111111-1111-4111-8111-111111111111"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 201 or 200 application/json
Example response
{
"id": "44444444-4444-4444-8444-444444444444",
"invoice_id": "33333333-3333-4333-8333-333333333333",
"checkout_url": "https://pay.example.com/invoice/33333333-3333-4333-8333-333333333333"
}Response fields
Usage notes
- Requires topups.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETGet Topup/v1/operator/merchants/{merchant_id}/topups/{topup_id}Read only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups/YOUR_TOPUP_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups/YOUR_TOPUP_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups/YOUR_TOPUP_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups/YOUR_TOPUP_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"id": "44444444-4444-4444-8444-444444444444",
"merchant_id": "11111111-1111-4111-8111-111111111111",
"invoice_id": "33333333-3333-4333-8333-333333333333",
"amount": "25",
"currency": "EUR",
"status": "pending",
"invoice_status": "new",
"checkout_url": "https://pay.example.com/invoice/33333333-3333-4333-8333-333333333333"
}Response fields
Usage notes
- Requires topups.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETReports/v1/operator/reportsRead only
Read the Operator financial overview. Requires access to all hosted merchants.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Query parameters
periodoptionalFinance filters. period defaults last30; use custom with YYYY-MM-DD start/end. All-merchant credentials only.
startoptionalFinance filters. period defaults last30; use custom with YYYY-MM-DD start/end. All-merchant credentials only.
endoptionalFinance filters. period defaults last30; use custom with YYYY-MM-DD start/end. All-merchant credentials only.
currencyoptionalFinance filters. period defaults last30; use custom with YYYY-MM-DD start/end. All-merchant credentials only.
merchant_idoptionalFinance filters. period defaults last30; use custom with YYYY-MM-DD start/end. All-merchant credentials only.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/reports" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/reports", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/reports");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/reports",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"summary": {
"fees": "10",
"costs": "3",
"margin": "7",
"credits": "25",
"pending": 0,
"missing_rates": 0
},
"merchants": [],
"filters": {
"period": "last30",
"currency": "EUR",
"timezone": "UTC"
},
"basis": "first_settlement_latest_net_fees"
}Response fields
Usage notes
- Requires reports.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Own-business rows, costs and counts are excluded before totals are calculated. An own-business merchant_id filter returns 404. Reporting uses the installation timezone.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETList Audit/v1/operator/auditRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Query parameters
merchant_idoptionalFilter permitted merchant, exact event type (events) or action text (audit). Retained events: 30 days.
searchoptionalFilter permitted merchant, exact event type (events) or action text (audit). Retained events: 30 days.
pageoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/audit" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/audit", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/audit");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/audit",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}Response fields
Usage notes
- Requires audit.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETList Events/v1/operator/eventsRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Query parameters
merchant_idoptionalFilter permitted merchant, exact event type (events) or action text (audit). Retained events: 30 days.
event_typeoptionalFilter permitted merchant, exact event type (events) or action text (audit). Retained events: 30 days.
pageoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/events" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/events", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/events");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/events",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}Response fields
Usage notes
- Requires events.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETList Webhooks/v1/operator/webhooksRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Query parameters
pageoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
searchoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/webhooks" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/webhooks", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/webhooks");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/webhooks",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}Response fields
Usage notes
- Requires events.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTCreate Webhook/v1/operator/webhooksRead + write
Subscribe to future Operator lifecycle events. Not a store payment webhook.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/webhooks" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"url": "https://shop.example.test/operator-events",
"events": [
"merchant.created",
"topup.settled"
],
"merchant_ids": [],
"enabled": true
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"url": "https://shop.example.test/operator-events",
"events": [
"merchant.created",
"topup.settled"
],
"merchant_ids": [],
"enabled": true
}`;
const response = await fetch("https://api.example.com/v1/operator/webhooks", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"url": "https://shop.example.test/operator-events",
"events": [
"merchant.created",
"topup.settled"
],
"merchant_ids": [],
"enabled": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/webhooks");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"url": "https://shop.example.test/operator-events",
"events": [
"merchant.created",
"topup.settled"
],
"merchant_ids": [],
"enabled": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/webhooks",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
urlpublic HTTPS URLrequiredNo credentials, private IPs or redirects. DNS/IP checked again when delivering.
merchant_ids[]UUID[]optionalEmpty means all merchants permitted by this credential. Live scope restrictions are rechecked.
enabledbooleanoptionalPaused endpoints retain queued deliveries; re-enabling resumes valid retained work.
Response 201 or 200 application/json
Example response
{
"id": "11111111-1111-4111-8111-111111111111",
"signing_secret": "wco_whsec_EXAMPLE_ONLY_SAVE_ONCE"
}Response fields
Usage notes
- Requires webhooks.write + events.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTUpdate Webhook/v1/operator/webhooks/{webhook_id}Read + write
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
webhook_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"url": "https://shop.example.test/operator-events",
"events": [
"topup.settled"
],
"merchant_ids": [],
"enabled": false
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"url": "https://shop.example.test/operator-events",
"events": [
"topup.settled"
],
"merchant_ids": [],
"enabled": false
}`;
const response = await fetch("https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"url": "https://shop.example.test/operator-events",
"events": [
"topup.settled"
],
"merchant_ids": [],
"enabled": false
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"url": "https://shop.example.test/operator-events",
"events": [
"topup.settled"
],
"merchant_ids": [],
"enabled": false
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
urlpublic HTTPS URLrequiredNo credentials, private IPs or redirects. DNS/IP checked again when delivering.
merchant_ids[]UUID[]optionalEmpty means all merchants permitted by this credential. Live scope restrictions are rechecked.
enabledbooleanoptionalPaused endpoints retain queued deliveries; re-enabling resumes valid retained work.
Response 200 application/json
Example response
{
"id": "44444444-4444-4444-8444-444444444444",
"signing_secret": null
}Response fields
Usage notes
- Requires webhooks.write + events.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTRotate Webhook Secret/v1/operator/webhooks/{webhook_id}/rotateRead + write
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
webhook_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID/rotate" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{}`;
const response = await fetch("https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID/rotate", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID/rotate");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID/rotate",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"id": "11111111-1111-4111-8111-111111111111",
"signing_secret": "wco_whsec_EXAMPLE_ONLY_SAVE_ONCE"
}Response fields
Usage notes
- Requires webhooks.write + events.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETList Webhook Deliveries/v1/operator/webhooks/{webhook_id}/deliveriesRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
webhook_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Query parameters
pageoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID/deliveries" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID/deliveries", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID/deliveries");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID/deliveries",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}Response fields
Usage notes
- Requires events.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETList Projects/v1/operator/merchants/{merchant_id}/projectsRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Query parameters
pageoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
searchoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}Response fields
Usage notes
- Requires projects.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTCreate Project/v1/operator/merchants/{merchant_id}/projectsRead + write
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Online shop",
"slug": "online-shop",
"enabled": false
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"name": "Online shop",
"slug": "online-shop",
"enabled": false
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"name": "Online shop",
"slug": "online-shop",
"enabled": false
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"name": "Online shop",
"slug": "online-shop",
"enabled": false
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
namestringsrequiredName and unique stable project identifier. Creates local wallets using the existing project bootstrap, never transfers funds.
slugstringsrequiredName and unique stable project identifier. Creates local wallets using the existing project bootstrap, never transfers funds.
enabledoptionalenabled defaults true; creating paused and configuring a store first is recommended.
reporting_timezoneoptionalenabled defaults true; creating paused and configuring a store first is recommended.
checkout_titleoptionalenabled defaults true; creating paused and configuring a store first is recommended.
checkout_descriptionoptionalenabled defaults true; creating paused and configuring a store first is recommended.
checkout_themeoptionalenabled defaults true; creating paused and configuring a store first is recommended.
checkout_accent_coloroptionalenabled defaults true; creating paused and configuring a store first is recommended.
Response 201 or 200 application/json
Example response
{
"id": "33333333-3333-4333-8333-333333333333",
"slug": "example-shop",
"name": "Example shop",
"enabled": false,
"reporting_currency": "EUR",
"reporting_timezone": "Europe/Berlin",
"stores": []
}Response fields
Usage notes
- Requires projects.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETGet Project/v1/operator/merchants/{merchant_id}/projects/{project_id}Read only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"id": "33333333-3333-4333-8333-333333333333",
"slug": "example-shop",
"name": "Example shop",
"enabled": false,
"reporting_currency": "EUR",
"reporting_timezone": "Europe/Berlin",
"stores": []
}Response fields
Usage notes
- Requires projects.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTUpdate Project/v1/operator/merchants/{merchant_id}/projects/{project_id}Read + write
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"enabled": true
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"enabled": true
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"enabled": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"enabled": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"id": "33333333-3333-4333-8333-333333333333",
"slug": "example-shop",
"name": "Example shop",
"enabled": false,
"reporting_currency": "EUR",
"reporting_timezone": "Europe/Berlin",
"stores": []
}Response fields
Usage notes
- Requires projects.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETList Stores/v1/operator/merchants/{merchant_id}/projects/{project_id}/storesRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
Query parameters
pageoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
searchoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}Response fields
Usage notes
- Requires projects.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTCreate Store/v1/operator/merchants/{merchant_id}/projects/{project_id}/storesRead + write
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Web checkout",
"slug": "web-checkout",
"default_currency": "EUR",
"enabled": false
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"name": "Web checkout",
"slug": "web-checkout",
"default_currency": "EUR",
"enabled": false
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"name": "Web checkout",
"slug": "web-checkout",
"default_currency": "EUR",
"enabled": false
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"name": "Web checkout",
"slug": "web-checkout",
"default_currency": "EUR",
"enabled": false
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
default_currencyoptionalUse decimal strings for percentages. New stores inherit the project's default-store appearance.
invoice_expiry_minutesoptionalUse decimal strings for percentages. New stores inherit the project's default-store appearance.
exchange_rate_spread_percentoptionalUse decimal strings for percentages. New stores inherit the project's default-store appearance.
underpayment_tolerance_percentoptionalUse decimal strings for percentages. New stores inherit the project's default-store appearance.
allow_zero_amount_invoicesoptionalConfigure accepted assets with payment-assets; zero-amount invoices default off.
allow_underpaymentsoptionalConfigure accepted assets with payment-assets; zero-amount invoices default off.
allow_overpaymentsoptionalConfigure accepted assets with payment-assets; zero-amount invoices default off.
allowed_chain_slugsoptionalConfigure accepted assets with payment-assets; zero-amount invoices default off.
ipn_enabledoptionalIPN and return URLs follow existing URL validation. No arbitrary HTML/JavaScript.
default_ipn_urloptionalIPN and return URLs follow existing URL validation. No arbitrary HTML/JavaScript.
default_redirect_urloptionalIPN and return URLs follow existing URL validation. No arbitrary HTML/JavaScript.
default_cancel_urloptionalIPN and return URLs follow existing URL validation. No arbitrary HTML/JavaScript.
redirect_automaticallyoptionalIPN and return URLs follow existing URL validation. No arbitrary HTML/JavaScript.
checkout_languageoptionalUse supported language and active role domains; configure embedding origins explicitly.
embed_enabledoptionalUse supported language and active role domains; configure embedding origins explicitly.
allowed_embed_originsoptionalUse supported language and active role domains; configure embedding origins explicitly.
domainsoptionalUse supported language and active role domains; configure embedding origins explicitly.
Response 201 or 200 application/json
Example response
{
"id": "44444444-4444-4444-8444-444444444444",
"project_id": "33333333-3333-4333-8333-333333333333",
"slug": "online",
"name": "Online shop",
"enabled": false,
"default_currency": "EUR",
"invoice_expiry_minutes": 60
}Response fields
Usage notes
- Requires projects.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETGet Store/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}Read only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
project_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
store_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"id": "44444444-4444-4444-8444-444444444444",
"project_id": "33333333-3333-4333-8333-333333333333",
"slug": "online",
"name": "Online shop",
"enabled": false,
"default_currency": "EUR",
"invoice_expiry_minutes": 60
}Response fields
Usage notes
- Requires projects.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTUpdate Store/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}Read + write
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
project_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
store_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"enabled": true
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"enabled": true
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"enabled": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"enabled": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"id": "44444444-4444-4444-8444-444444444444",
"project_id": "33333333-3333-4333-8333-333333333333",
"slug": "online",
"name": "Online shop",
"enabled": false,
"default_currency": "EUR",
"invoice_expiry_minutes": 60
}Response fields
Usage notes
- Requires projects.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETGet Store Appearance/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/checkout-appearanceRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
project_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
store_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/checkout-appearance" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/checkout-appearance", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/checkout-appearance");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/checkout-appearance",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"revision": 1,
"settings": {
"inherit_default_store": true
},
"effective": {
"title": "Pay securely"
}
}Response fields
Usage notes
- Requires projects.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTUpdate Store Appearance/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/checkout-appearanceRead + write
Save a validated, revision-protected store design.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
project_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
store_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/checkout-appearance" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"revision": 1,
"settings": {
"inherit_default_store": false,
"title": "Pay securely",
"theme": "light"
}
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"revision": 1,
"settings": {
"inherit_default_store": false,
"title": "Pay securely",
"theme": "light"
}
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/checkout-appearance", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"revision": 1,
"settings": {
"inherit_default_store": false,
"title": "Pay securely",
"theme": "light"
}
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/checkout-appearance");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"revision": 1,
"settings": {
"inherit_default_store": false,
"title": "Pay securely",
"theme": "light"
}
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/checkout-appearance",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
revisionintegerrequiredRead the current revision with GET first. A stale revision fails without overwriting another editor.
settingsobjectrequired
Validated checkout appearance, including inherit_default_store, branding, intro/outro, font sizes and visibility. No arbitrary HTML/JavaScript. Uploading image bytes is console-only.Details
Validated checkout appearance, including inherit_default_store, branding, intro/outro, font sizes and visibility. No arbitrary HTML/JavaScript. Uploading image bytes is console-only.
Response 200 application/json
Example response
{
"settings": {
"inherit_default_store": false,
"title": "Pay securely",
"theme": "light"
},
"revision": 2
}Response fields
Usage notes
- Requires projects.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETList Store Payment Assets/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/payment-assetsRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
project_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
store_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": []
}Response fields
data[]arrayUsage notes
- Requires projects.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTUpdate Store Payment Assets/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/payment-assetsRead + write
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
project_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
store_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"assets": [
{
"asset_id": "11111111-1111-4111-8111-111111111111",
"display_order": 0
}
]
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"assets": [
{
"asset_id": "11111111-1111-4111-8111-111111111111",
"display_order": 0
}
]
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"assets": [
{
"asset_id": "11111111-1111-4111-8111-111111111111",
"display_order": 0
}
]
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"assets": [
{
"asset_id": "11111111-1111-4111-8111-111111111111",
"display_order": 0
}
]
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
assets[]object[]required
Full replacement of on-chain methods: asset_id UUID and display_order. [] clears accepted on-chain assets. Only verified project assets; does not configure Lightning.Details
Full replacement of on-chain methods: asset_id UUID and display_order. [] clears accepted on-chain assets. Only verified project assets; does not configure Lightning.
Response 200 application/json
Example response
{
"data": []
}Response fields
data[]arrayUsage notes
- Requires projects.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETList Store Webhooks/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/webhooksRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
project_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
store_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Query parameters
pageoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
searchoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}Response fields
Usage notes
- Requires projects.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTCreate Store Webhook/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/webhooksRead + write
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
project_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
store_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Orders",
"url": "https://shop.example.test/payments",
"event_types": [
"invoice.settled"
],
"enabled": true,
"automatic_redelivery": true
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"name": "Orders",
"url": "https://shop.example.test/payments",
"event_types": [
"invoice.settled"
],
"enabled": true,
"automatic_redelivery": true
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"name": "Orders",
"url": "https://shop.example.test/payments",
"event_types": [
"invoice.settled"
],
"enabled": true,
"automatic_redelivery": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"name": "Orders",
"url": "https://shop.example.test/payments",
"event_types": [
"invoice.settled"
],
"enabled": true,
"automatic_redelivery": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
namestrings / arrayrequiredPublic HTTPS receiver and invoice event names from IPN & webhook documentation.
urlstrings / arrayrequiredPublic HTTPS receiver and invoice event names from IPN & webhook documentation.
event_types[]strings / arrayrequiredPublic HTTPS receiver and invoice event names from IPN & webhook documentation.
enabledbooleansoptionalCreation returns the signing secret once. These are store payment callbacks, not Operator lifecycle events.
automatic_redeliverybooleansoptionalCreation returns the signing secret once. These are store payment callbacks, not Operator lifecycle events.
Response 201 or 200 application/json
Example response
{
"signing_secret": "EXAMPLE_ONLY_SAVE_ONCE",
"endpoint": {
"id": "44444444-4444-4444-8444-444444444444",
"name": "Orders",
"enabled": true
},
"secret_visible_once": true
}Response fields
Usage notes
- Requires projects.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTUpdate Store Webhook/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/webhooks/{webhook_id}Read + write
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
project_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
store_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
webhook_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks/YOUR_WEBHOOK_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Orders",
"url": "https://shop.example.test/payments",
"event_types": [
"invoice.settled"
],
"enabled": false,
"automatic_redelivery": true
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"name": "Orders",
"url": "https://shop.example.test/payments",
"event_types": [
"invoice.settled"
],
"enabled": false,
"automatic_redelivery": true
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks/YOUR_WEBHOOK_ID", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"name": "Orders",
"url": "https://shop.example.test/payments",
"event_types": [
"invoice.settled"
],
"enabled": false,
"automatic_redelivery": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks/YOUR_WEBHOOK_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"name": "Orders",
"url": "https://shop.example.test/payments",
"event_types": [
"invoice.settled"
],
"enabled": false,
"automatic_redelivery": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks/YOUR_WEBHOOK_ID",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
namestrings / arrayrequiredPublic HTTPS receiver and invoice event names from IPN & webhook documentation.
urlstrings / arrayrequiredPublic HTTPS receiver and invoice event names from IPN & webhook documentation.
event_types[]strings / arrayrequiredPublic HTTPS receiver and invoice event names from IPN & webhook documentation.
enabledbooleansoptionalCreation returns the signing secret once. These are store payment callbacks, not Operator lifecycle events.
automatic_redeliverybooleansoptionalCreation returns the signing secret once. These are store payment callbacks, not Operator lifecycle events.
Response 200 application/json
Example response
{
"id": "44444444-4444-4444-8444-444444444444",
"name": "Orders",
"enabled": true
}Response fields
Usage notes
- Requires projects.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETList Invoices/v1/operator/merchants/{merchant_id}/projects/{project_id}/invoicesRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
Query parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/invoices" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/invoices", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/invoices");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/invoices",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"pagination": {
"limit": 25,
"offset": 0,
"total": 0,
"has_more": false
}
}Response fields
Usage notes
- Requires reports.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETGet Invoice/v1/operator/merchants/{merchant_id}/projects/{project_id}/invoices/{invoice_id}Read only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
project_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
invoice_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/invoices/YOUR_INVOICE_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/invoices/YOUR_INVOICE_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/invoices/YOUR_INVOICE_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/invoices/YOUR_INVOICE_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"invoice_id": "44444444-4444-4444-8444-444444444444",
"project_id": "33333333-3333-4333-8333-333333333333",
"amount": "25",
"currency": "EUR",
"status": "new",
"metadata": {},
"payment_intents": []
}Response fields
Usage notes
- Requires reports.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
- invoice_id is the public invoice ID returned at creation and in callbacks, not the internal id.
Errors
GETList Wallets/v1/operator/merchants/{merchant_id}/projects/{project_id}/walletsRead only
Read cached public wallet balances, never private keys or recovery phrases.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
Query parameters
pageoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
searchoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/wallets" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/wallets", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/wallets");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/wallets",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}Response fields
Usage notes
- Requires reports.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Balances are cached observations with freshness fields, not a spendable-balance guarantee. Sending and key export are not available through this API.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETList Wallet Addresses/v1/operator/merchants/{merchant_id}/projects/{project_id}/wallets/{wallet_id}/addressesRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
project_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
wallet_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Query parameters
limitoptionalLimit1–50, default25. Pass next_cursor as before for the next page. Omit before for page1. has_balance=false and hide_small_balances=false include empty/small balances.Details
Limit1–50, default25. Pass next_cursor as before for the next page. Omit before for page1. has_balance=false and hide_small_balances=false include empty/small balances.
beforeoptionalLimit1–50, default25. Pass next_cursor as before for the next page. Omit before for page1. has_balance=false and hide_small_balances=false include empty/small balances.Details
Limit1–50, default25. Pass next_cursor as before for the next page. Omit before for page1. has_balance=false and hide_small_balances=false include empty/small balances.
searchoptionalLimit1–50, default25. Pass next_cursor as before for the next page. Omit before for page1. has_balance=false and hide_small_balances=false include empty/small balances.Details
Limit1–50, default25. Pass next_cursor as before for the next page. Omit before for page1. has_balance=false and hide_small_balances=false include empty/small balances.
has_balanceoptionalLimit1–50, default25. Pass next_cursor as before for the next page. Omit before for page1. has_balance=false and hide_small_balances=false include empty/small balances.Details
Limit1–50, default25. Pass next_cursor as before for the next page. Omit before for page1. has_balance=false and hide_small_balances=false include empty/small balances.
hide_small_balancesoptionalLimit1–50, default25. Pass next_cursor as before for the next page. Omit before for page1. has_balance=false and hide_small_balances=false include empty/small balances.Details
Limit1–50, default25. Pass next_cursor as before for the next page. Omit before for page1. has_balance=false and hide_small_balances=false include empty/small balances.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/wallets/YOUR_WALLET_ID/addresses" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/wallets/YOUR_WALLET_ID/addresses", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/wallets/YOUR_WALLET_ID/addresses");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/wallets/YOUR_WALLET_ID/addresses",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"wallet": {
"id": "22222222-2222-4222-8222-222222222222",
"chain_slug": "ethereum"
},
"items": [],
"total": 0,
"total_pages": 1,
"next_cursor": null,
"reporting_currency": "EUR",
"has_balance": true,
"hide_small_balances": true,
"small_balance_threshold": {
"amount": "0.20",
"currency": "EUR"
}
}Response fields
Usage notes
- Requires reports.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETList Merchant Credentials/v1/operator/merchants/{merchant_id}/api-credentialsRead only
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Query parameters
pageoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
searchoptional1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.Details
1-based page, 25 items per page. Search is supported on merchants, users, projects, stores, wallets, credentials and webhooks; native event lists use their specific filters.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}Response fields
Usage notes
- Requires merchant_credentials.read; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTCreate Merchant Credential/v1/operator/merchants/{merchant_id}/api-credentialsRead + write
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
merchant_idUUIDrequiredCanonical lowercase resource UUID; must belong to the credential's merchant scope.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Store integration",
"access_level": "read_only",
"enabled": true,
"ip_restriction_enabled": false,
"allowed_ips": [],
"project_ids": [
"11111111-1111-4111-8111-111111111111"
]
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"name": "Store integration",
"access_level": "read_only",
"enabled": true,
"ip_restriction_enabled": false,
"allowed_ips": [],
"project_ids": [
"11111111-1111-4111-8111-111111111111"
]
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"name": "Store integration",
"access_level": "read_only",
"enabled": true,
"ip_restriction_enabled": false,
"allowed_ips": [],
"project_ids": [
"11111111-1111-4111-8111-111111111111"
]
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"name": "Store integration",
"access_level": "read_only",
"enabled": true,
"ip_restriction_enabled": false,
"allowed_ips": [],
"project_ids": [
"11111111-1111-4111-8111-111111111111"
]
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
project_ids[]UUID[]Projects owned by the selected merchant only; an empty list follows the existing all-merchant-projects policy.
enabledoptionalExisting merchant key controls. Secret returned only once; requires merchant_credentials.write.
ip_restriction_enabledoptionalExisting merchant key controls. Secret returned only once; requires merchant_credentials.write.
allowed_ips[]optionalExisting merchant key controls. Secret returned only once; requires merchant_credentials.write.
requests_per_minuteoptionalExisting merchant key controls. Secret returned only once; requires merchant_credentials.write.
Response 201 or 200 application/json
Example response
{
"credential": {
"id": "22222222-2222-4222-8222-222222222222",
"name": "Checkout",
"access_level": "read_only",
"project_ids": [
"33333333-3333-4333-8333-333333333333"
],
"enabled": true
},
"token": "wc_live_EXAMPLE_ONLY_SAVE_ONCE"
}Response fields
Usage notes
- Requires merchant_credentials.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTUpdate Merchant Credential/v1/operator/merchants/{merchant_id}/api-credentials/{credential_id}Read + write
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Checkout",
"access_level": "read_only",
"enabled": true,
"ip_restriction_enabled": false,
"allowed_ips": [],
"project_ids": [
"33333333-3333-4333-8333-333333333333"
]
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"name": "Checkout",
"access_level": "read_only",
"enabled": true,
"ip_restriction_enabled": false,
"allowed_ips": [],
"project_ids": [
"33333333-3333-4333-8333-333333333333"
]
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"name": "Checkout",
"access_level": "read_only",
"enabled": true,
"ip_restriction_enabled": false,
"allowed_ips": [],
"project_ids": [
"33333333-3333-4333-8333-333333333333"
]
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"name": "Checkout",
"access_level": "read_only",
"enabled": true,
"ip_restriction_enabled": false,
"allowed_ips": [],
"project_ids": [
"33333333-3333-4333-8333-333333333333"
]
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
namefieldsrequiredSend the complete current credential configuration with changes. project_ids defaults []; requests_per_minute defaults to the merchant API quota.
access_levelfieldsrequiredSend the complete current credential configuration with changes. project_ids defaults []; requests_per_minute defaults to the merchant API quota.
enabledfieldsrequiredSend the complete current credential configuration with changes. project_ids defaults []; requests_per_minute defaults to the merchant API quota.
ip_restriction_enabledfieldsrequiredSend the complete current credential configuration with changes. project_ids defaults []; requests_per_minute defaults to the merchant API quota.
allowed_ips[]fieldsrequiredSend the complete current credential configuration with changes. project_ids defaults []; requests_per_minute defaults to the merchant API quota.
project_ids[]arrayResponse 200 application/json
Example response
{
"id": "22222222-2222-4222-8222-222222222222",
"name": "Checkout",
"access_level": "read_only",
"project_ids": [
"33333333-3333-4333-8333-333333333333"
],
"enabled": true
}Response fields
Usage notes
- Requires merchant_credentials.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTRotate Merchant Credential/v1/operator/merchants/{merchant_id}/api-credentials/{credential_id}/rotateRead + write
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID/rotate" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID/rotate", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID/rotate");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID/rotate",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"credential": {
"id": "22222222-2222-4222-8222-222222222222",
"name": "Checkout",
"access_level": "read_only",
"project_ids": [
"33333333-3333-4333-8333-333333333333"
],
"enabled": true
},
"token": "wc_live_EXAMPLE_ONLY_SAVE_ONCE"
}Response fields
Usage notes
- Requires merchant_credentials.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTRevoke Merchant Credential/v1/operator/merchants/{merchant_id}/api-credentials/{credential_id}/revokeRead + write
Manage or inspect the named hosted merchant resource using a separate Operator credential.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persisted for this operation |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID/revoke" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID/revoke", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID/revoke");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID/revoke",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"revoked": true
}Response fields
revokedbooleanUsage notes
- Requires merchant_credentials.write; only permitted hosted merchants. Operator keys do not access the owner's own-business workspace.
- Persist a unique Idempotency-Key and the exact body before sending. Retries never repeat a committed action. Secret fields are omitted on replay; if a secret response was lost, inspect the created resource and explicitly rotate/reissue it. A 409 operator_request_in_progress can mean an interrupted request with unknown outcome: inspect the resource/audit; do not retry with a new key blindly.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTCheck invitation token/v1/onboarding/invitations/checkPublic
Token-only onboarding. Does not accept an Operator key or automatically sign in. Console login still requires the site's Basic Auth and existing TOTP.
Authentication & headers
Public
Request
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/onboarding/invitations/check" \
--header 'Content-Type: application/json' \
--data-raw '{
"token": "YOUR_PRIVATE_INVITATION_TOKEN"
}'// Node.js 18+ · run on your server, never in browser code.
const body = `{
"token": "YOUR_PRIVATE_INVITATION_TOKEN"
}`;
const response = await fetch("https://api.example.com/v1/onboarding/invitations/check", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$body = <<<'JSON'
{
"token": "YOUR_PRIVATE_INVITATION_TOKEN"
}
JSON;
$ch = curl_init("https://api.example.com/v1/onboarding/invitations/check");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Content-Type": "application/json"
}
body = """{
"token": "YOUR_PRIVATE_INVITATION_TOKEN"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/onboarding/invitations/check",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"kind": "invitation",
"email": "admin@example.test",
"merchant_name": "Example shop"
}Response fields
Usage notes
- 48-hour invitation; one-hour password-reset link. Single-use hashed tokens. Reissuing revokes the earlier link. Acceptance preserves TOTP and revokes old sessions.
- No automatic retries. If acceptance times out, check the link status and try signing in; do not assume it failed. Rate limited by observed source IP. The recipient must give their own custody acknowledgement.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
POSTAccept invitation or password reset/v1/onboarding/invitations/acceptPublic
Token-only onboarding. Does not accept an Operator key or automatically sign in. Console login still requires the site's Basic Auth and existing TOTP.
Authentication & headers
Public
Request
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/onboarding/invitations/accept" \
--header 'Content-Type: application/json' \
--data-raw '{
"token": "YOUR_PRIVATE_INVITATION_TOKEN",
"password": "REPLACE_WITH_YOUR_OWN_UNIQUE_PASSWORD",
"custody_acknowledged": true
}'// Node.js 18+ · run on your server, never in browser code.
const body = `{
"token": "YOUR_PRIVATE_INVITATION_TOKEN",
"password": "REPLACE_WITH_YOUR_OWN_UNIQUE_PASSWORD",
"custody_acknowledged": true
}`;
const response = await fetch("https://api.example.com/v1/onboarding/invitations/accept", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$body = <<<'JSON'
{
"token": "YOUR_PRIVATE_INVITATION_TOKEN",
"password": "REPLACE_WITH_YOUR_OWN_UNIQUE_PASSWORD",
"custody_acknowledged": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/onboarding/invitations/accept");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Content-Type": "application/json"
}
body = """{
"token": "YOUR_PRIVATE_INVITATION_TOKEN",
"password": "REPLACE_WITH_YOUR_OWN_UNIQUE_PASSWORD",
"custody_acknowledged": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/onboarding/invitations/accept",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"password_set": true
}Response fields
password_setbooleanUsage notes
- 48-hour invitation; one-hour password-reset link. Single-use hashed tokens. Reissuing revokes the earlier link. Acceptance preserves TOTP and revokes old sessions.
- No automatic retries. If acceptance times out, check the link status and try signing in; do not assume it failed. Rate limited by observed source IP. The recipient must give their own custody acknowledgement.
- Response examples show selected fields. Treat additional response fields as additive.
Errors
GETCapabilities/v1/marketplace/capabilitiesRead only
Participating stores, configured assets, supported payout rails, native fee asset and readiness reasons. Temporary scanner availability does not widen the allowed asset set.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/capabilities?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/capabilities?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/capabilities?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/capabilities?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETMarketplace report/v1/marketplace/overviewRead only
Filter store_id, asset_id, from and to (inclusive YYYY-MM-DD, UTC; default last 30 days; up to ten years). Exact account balances as of the end date and changes in the period. No estimated fiat earnings. A store filter excludes project-wide fees without an invoice.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/overview?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/overview?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/overview?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/overview?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETProject settings/v1/marketplace/settingsRead only
Returns enabled, payouts_paused, commission_percent, revision and participating stores.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/settings?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/settings?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/settings?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/settings?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTSave project settings/v1/marketplace/settingsRead + write
Replace the participating-store selection. revision 0 initializes settings; later edits require the returned revision. Commission is a decimal percentage, 0–100 with at most two decimal places.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | policies.write |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/settings" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"revision": 0,
"enabled": true,
"payouts_paused": true,
"commission_percent": "4",
"stores": [
{
"store_id": "YOUR_STORE_ID",
"enabled": true,
"commission_percent": null
}
]
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"revision": 0,
"enabled": true,
"payouts_paused": true,
"commission_percent": "4",
"stores": [
{
"store_id": "YOUR_STORE_ID",
"enabled": true,
"commission_percent": null
}
]
}`;
const response = await fetch("https://api.example.com/v1/marketplace/settings", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"revision": 0,
"enabled": true,
"payouts_paused": true,
"commission_percent": "4",
"stores": [
{
"store_id": "YOUR_STORE_ID",
"enabled": true,
"commission_percent": null
}
]
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/settings");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"revision": 0,
"enabled": true,
"payouts_paused": true,
"commission_percent": "4",
"stores": [
{
"store_id": "YOUR_STORE_ID",
"enabled": true,
"commission_percent": null
}
]
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/settings",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires policies.write. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETList vendors/v1/marketplace/vendorsRead only
Paged vendors. Search q by name, external ID or email; status active, suspended or archived.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/vendors?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/vendors?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/vendors?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/vendors?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"pagination": {
"page": 1,
"per_page": 25,
"total": 0,
"pages": 1
}
}Response fields
Usage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTCreate vendor/v1/marketplace/vendorsRead + write
Name required (160 characters); external_id optional and project-unique (128); email optional (254); description up to 2000. commission_percent null inherits the store. A non-null override also requires commissions.override. Vendors are not hosted merchant accounts.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | vendors.write |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/vendors" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"name": "Example vendor",
"external_id": "vendor-1042",
"email": "vendor@example.com",
"description": "",
"status": "active",
"commission_percent": null
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"name": "Example vendor",
"external_id": "vendor-1042",
"email": "vendor@example.com",
"description": "",
"status": "active",
"commission_percent": null
}`;
const response = await fetch("https://api.example.com/v1/marketplace/vendors", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"name": "Example vendor",
"external_id": "vendor-1042",
"email": "vendor@example.com",
"description": "",
"status": "active",
"commission_percent": null
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/vendors");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"name": "Example vendor",
"external_id": "vendor-1042",
"email": "vendor@example.com",
"description": "",
"status": "active",
"commission_percent": null
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/vendors",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires vendors.write. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETGet vendor/v1/marketplace/vendors/{vendor_id}Read only
Return one vendor, revision and commission override.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTUpdate vendor/v1/marketplace/vendors/{vendor_id}/updateRead + write
Update vendor details; status active or suspended. Changing or removing commission_percent also requires commissions.override; unchanged values need only vendors.write. Restoring an archived vendor also requires reconciliation.write. Console financial edits require account reauthentication. Existing allocations retain their original commission and destinations.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | vendors.write |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/update" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"name": "Example vendor",
"external_id": "vendor-1042",
"email": "vendor@example.com",
"description": "",
"status": "active",
"commission_percent": null,
"revision": 1
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"name": "Example vendor",
"external_id": "vendor-1042",
"email": "vendor@example.com",
"description": "",
"status": "active",
"commission_percent": null,
"revision": 1
}`;
const response = await fetch("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/update", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"name": "Example vendor",
"external_id": "vendor-1042",
"email": "vendor@example.com",
"description": "",
"status": "active",
"commission_percent": null,
"revision": 1
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/update");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"name": "Example vendor",
"external_id": "vendor-1042",
"email": "vendor@example.com",
"description": "",
"status": "active",
"commission_percent": null,
"revision": 1
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/update",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires vendors.write. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTArchive vendor/v1/marketplace/vendors/{vendor_id}/archiveRead + write
Archive only after unpaid and open invoice obligations are resolved; otherwise returns 409 vendor_has_obligations. History is retained. Temporarily use suspended status, or restore an old archived vendor with the update route and reconciliation.write.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | vendors.write |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/archive" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}`;
const response = await fetch("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/archive", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/archive");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/archive",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires vendors.write. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETVendor destinations/v1/marketplace/vendors/{vendor_id}/destinationsRead only
Versioned destinations and pending/approved/disabled states. Approval does not prove ownership.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/destinations?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/destinations?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/destinations?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/destinations?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"pagination": {
"page": 1,
"per_page": 25,
"total": 0,
"pages": 1
}
}Response fields
Usage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTAdd payout destination/v1/marketplace/vendors/{vendor_id}/destinationsRead + write
Register a valid mainnet BTC or supported EVM address, pending approval. Project-owned addresses, wrong network and zero EVM addresses are rejected.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | destinations.write |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/destinations" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"chain_slug": "bitcoin",
"network": "mainnet",
"address": "VERIFIED_VENDOR_BTC_ADDRESS",
"label": "Vendor BTC"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"chain_slug": "bitcoin",
"network": "mainnet",
"address": "VERIFIED_VENDOR_BTC_ADDRESS",
"label": "Vendor BTC"
}`;
const response = await fetch("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/destinations", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"chain_slug": "bitcoin",
"network": "mainnet",
"address": "VERIFIED_VENDOR_BTC_ADDRESS",
"label": "Vendor BTC"
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/destinations");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"chain_slug": "bitcoin",
"network": "mainnet",
"address": "VERIFIED_VENDOR_BTC_ADDRESS",
"label": "Vendor BTC"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/destinations",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires destinations.write. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTApprove destination/v1/marketplace/vendors/{vendor_id}/destinations/{destination_id}/approveRead + write
Verify ownership independently first. Disable an existing approved destination on this vendor/network first; old snapshots are not redirected.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | destinations.approve |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/destinations/YOUR_DESTINATION_ID/approve" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}`;
const response = await fetch("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/destinations/YOUR_DESTINATION_ID/approve", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/destinations/YOUR_DESTINATION_ID/approve");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/destinations/YOUR_DESTINATION_ID/approve",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires destinations.approve. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTDisable destination/v1/marketplace/vendors/{vendor_id}/destinations/{destination_id}/disableRead + write
Stop new/unsent work using this version. A signed transaction cannot be revoked.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | destinations.approve |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/destinations/YOUR_DESTINATION_ID/disable" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}`;
const response = await fetch("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/destinations/YOUR_DESTINATION_ID/disable", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/destinations/YOUR_DESTINATION_ID/disable");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/destinations/YOUR_DESTINATION_ID/disable",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires destinations.approve. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETVendor balances/v1/marketplace/vendors/{vendor_id}/balancesRead only
Exact retained ledger amounts grouped by immutable asset identity and account type; not live spendable wallet balances.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/balances?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/balances?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/balances?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/vendors/YOUR_VENDOR_ID/balances?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETMarketplace invoices/v1/marketplace/invoicesRead only
Paged invoices; q searches order, email, description, metadata and invoice UUID. Filter store_id, vendor_id and Marketplace status. Ordinary invoices are excluded.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/invoices?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/invoices?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/invoices?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/invoices?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"pagination": {
"page": 1,
"per_page": 25,
"total": 0,
"pages": 1
}
}Response fields
Usage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTCreate multi-vendor invoice/v1/marketplace/invoicesRead + write
Reuse normal InvoiceCreateInput fields, plus store_id and allocations. Each vendor occurs once, 1–100 positive shares, sum exactly equal to amount. Optional invoice/per-allocation commission_percent requires commissions.override and console reauthentication for session requests. Returns data.invoice_id and links.checkout. No compatible common method returns a detailed error; no fallback to an incompatible asset.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | invoices.write |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/invoices" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"store_id": "YOUR_STORE_ID",
"amount": "300.00",
"currency": "USD",
"order_id": "cart-1042",
"allocations": [
{
"vendor_id": "VENDOR_A_UUID",
"gross_amount": "100.00"
},
{
"vendor_id": "VENDOR_B_UUID",
"gross_amount": "100.00"
},
{
"vendor_id": "VENDOR_C_UUID",
"gross_amount": "100.00"
}
]
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"store_id": "YOUR_STORE_ID",
"amount": "300.00",
"currency": "USD",
"order_id": "cart-1042",
"allocations": [
{
"vendor_id": "VENDOR_A_UUID",
"gross_amount": "100.00"
},
{
"vendor_id": "VENDOR_B_UUID",
"gross_amount": "100.00"
},
{
"vendor_id": "VENDOR_C_UUID",
"gross_amount": "100.00"
}
]
}`;
const response = await fetch("https://api.example.com/v1/marketplace/invoices", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"store_id": "YOUR_STORE_ID",
"amount": "300.00",
"currency": "USD",
"order_id": "cart-1042",
"allocations": [
{
"vendor_id": "VENDOR_A_UUID",
"gross_amount": "100.00"
},
{
"vendor_id": "VENDOR_B_UUID",
"gross_amount": "100.00"
},
{
"vendor_id": "VENDOR_C_UUID",
"gross_amount": "100.00"
}
]
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/invoices");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"store_id": "YOUR_STORE_ID",
"amount": "300.00",
"currency": "USD",
"order_id": "cart-1042",
"allocations": [
{
"vendor_id": "VENDOR_A_UUID",
"gross_amount": "100.00"
},
{
"vendor_id": "VENDOR_B_UUID",
"gross_amount": "100.00"
},
{
"vendor_id": "VENDOR_C_UUID",
"gross_amount": "100.00"
}
]
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/invoices",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
amountstringrequiredUnsigned plain decimal string; no sign or exponent, up to 48 integer digits and 30 decimal places. Must be positive by default. A store can allow zero-amount invoices in Stores → Invoice; zero totals settle without receiving funds, allocating addresses or processing fees.Details
Unsigned plain decimal string; no sign or exponent, up to 48 integer digits and 30 decimal places. Must be positive by default. A store can allow zero-amount invoices in Stores → Invoice; zero totals settle without receiving funds, allocating addresses or processing fees.
currencystring | nulloptionalSupported three-letter fiat currency, normalized to uppercase. Omitted or null inherits the store invoice currency. Creation also requires an independently available billing conversion rate.Details
Supported three-letter fiat currency, normalized to uppercase. Omitted or null inherits the store invoice currency. Creation also requires an independently available billing conversion rate.
payment_methods[]object[] | nulloptional
Select store-enabled methods for this invoice. Merchant 5.4.0+: ignore unknown/inactive/unaccepted choices; if none match, use store defaults. Omitted/null also uses store defaults; [] is invalid. Never enables a method or changes store settings. See selection schema below.Details
Select store-enabled methods for this invoice. Merchant 5.4.0+: ignore unknown/inactive/unaccepted choices; if none match, use store defaults. Omitted/null also uses store defaults; [] is invalid. Never enables a method or changes store settings. See selection schema below.
chain_slugstringrequiredCopy chain_slug in Project → Stores → Payment methods, or read it from GET /v1/projects/{project_id}/stores/{store_id}/payment-assets, such as ethereum, base or bitcoin. A chain/rail pair may occur only once.Details
Copy chain_slug in Project → Stores → Payment methods, or read it from GET /v1/projects/{project_id}/stores/{store_id}/payment-assets, such as ethereum, base or bitcoin. A chain/rail pair may occur only once.
asset_ids[]UUID[] | nulloptionalOn-chain asset.id UUIDs, not contract addresses or invoice payment-method IDs. Use this OR asset_tickers. Omit both selectors for all active accepted assets on this chain. [] and duplicate/nil IDs are invalid. On 5.4.0+, ignore IDs not active/accepted on this chain in this store; an entirely unmatched selection uses store defaults.Details
On-chain asset.id UUIDs, not contract addresses or invoice payment-method IDs. Use this OR asset_tickers. Omit both selectors for all active accepted assets on this chain. [] and duplicate/nil IDs are invalid. On 5.4.0+, ignore IDs not active/accepted on this chain in this store; an entirely unmatched selection uses store defaults.
asset_tickers[]string[] | nulloptionalMerchant 5.3.0+. Symbols such as BTC, USDC or PEPE, scoped to chain_slug and this store. 1–64 unique tickers; trim/case-insensitive, 1–40 ASCII letters/digits/dot/underscore/hyphen. Use this OR asset_ids. On 5.4.0+, ignore unknown/inactive/unaccepted tickers. Ambiguous accepted symbols still fail: use asset_ids. Active selected methods need valid wallets and pricing; temporary on-chain scanner outages do not block creation from 6.0.6. Lightning optionally accepts only BTC.Details
Merchant 5.3.0+. Symbols such as BTC, USDC or PEPE, scoped to chain_slug and this store. 1–64 unique tickers; trim/case-insensitive, 1–40 ASCII letters/digits/dot/underscore/hyphen. Use this OR asset_ids. On 5.4.0+, ignore unknown/inactive/unaccepted tickers. Ambiguous accepted symbols still fail: use asset_ids. Active selected methods need valid wallets and pricing; temporary on-chain scanner outages do not block creation from 6.0.6. Lightning optionally accepts only BTC.
payment_railonchain | lightningoptionalDefaults to onchain. To choose Bitcoin Lightning, use {chain_slug: bitcoin, payment_rail: lightning} with no asset_ids; asset_tickers may optionally be [BTC]. Bitcoin on-chain does not include Lightning. The store's Lightning connection must already be enabled and ready.Details
Defaults to onchain. To choose Bitcoin Lightning, use {chain_slug: bitcoin, payment_rail: lightning} with no asset_ids; asset_tickers may optionally be [BTC]. Bitcoin on-chain does not include Lightning. The store's Lightning connection must already be enabled and ready.
order_idstring | nulloptionalMerchant order reference, 1–128 characters after trimming; control characters are rejected.
emailstring | nulloptionalMerchant-only customer email, normalized to a practical ASCII address with at most 254 characters. Omitted or null stores no email.
descriptionstring | nulloptionalCustomer-facing description, 1–500 characters; line breaks and tabs are allowed.
expires_in_secondsinteger | nulloptionalInvoice quote lifetime from 300 through 86,400 seconds; omitted or null inherits store policy.
exchange_rate_spread_percentdecimal string | nulloptionalQuote markup from 0 through 100, with at most two decimal places. Omitted or null inherits the store default; "0" disables it for this invoice. Applied before upward rounding, then locked. Does not change the invoice fiat amount or the processing-fee basis.Details
Quote markup from 0 through 100, with at most two decimal places. Omitted or null inherits the store default; "0" disables it for this invoice. Applied before upward rounding, then locked. Does not change the invoice fiat amount or the processing-fee basis.
underpayment_tolerance_percentdecimal string | nulloptionalAccepted shortfall from 0 through 99.99 with at most two decimal places. Omitted or null inherits the store default.
ipn_urlstring | nulloptionalPublic HTTPS callback, at most 2,048 bytes and without credentials or fragment. Overrides the store default; null/omitted inherits it.
redirect_urlstring | nulloptionalHTTPS success URL used after settlement, at most 2,048 bytes and without embedded credentials. Omitted or null inherits the store default and cannot clear it.
cancel_urlstring | nulloptionalHTTPS return URL used when checkout ends without successful payment. Omitted or null inherits the store default and cannot clear it.
redirect_automaticallyboolean | nulloptionalOmitted or null inherits store policy. true requires an effective redirect_url.
languagestring | nulloptionalCustomer language: en, de, es, pt-BR, it, ru, zh-CN, fr, ko or ja. pt means Brazilian Portuguese and zh means Simplified Chinese. Supported regional tags such as de-DE normalize to the listed language. Omitted or null inherits the store language. Console language is separate.Details
Customer language: en, de, es, pt-BR, it, ru, zh-CN, fr, ko or ja. pt means Brazilian Portuguese and zh means Simplified Chinese. Supported regional tags such as de-DE normalize to the listed language. Omitted or null inherits the store language. Console language is separate.
checkout_appearanceobject | nulloptional
Partial presentation settings for this invoice. Omitted/null follows the store's current design. An object, including {}, freezes the resolved design and images at creation. See the override schema below; no financial settings, HTML, CSS, JavaScript or remote image URLs.Details
Partial presentation settings for this invoice. Omitted/null follows the store's current design. An object, including {}, freezes the resolved design and images at creation. See the override schema below; no financial settings, HTML, CSS, JavaScript or remote image URLs.
inherit_default_storebooleanoptionaltrue selects the project's default-store design as the base; otherwise use the target store's effective design. Overrides are then applied and saved independently; the resolved invoice flag is false.Details
true selects the project's default-store design as the base; otherwise use the target store's effective design. Overrides are then applied and saved independently; the resolved invoice flag is false.
introstringoptionalPlain text, up to 2,000 characters each. Intro appears at the top, Outro at the bottom in every state. Newlines are preserved; safe text URLs become links. Empty string clears. Legacy customer_message is accepted as an alias for intro; do not send both.Details
Plain text, up to 2,000 characters each. Intro appears at the top, Outro at the bottom in every state. Newlines are preserved; safe text URLs become links. Empty string clears. Legacy customer_message is accepted as an alias for intro; do not send both.
outrostringoptionalPlain text, up to 2,000 characters each. Intro appears at the top, Outro at the bottom in every state. Newlines are preserved; safe text URLs become links. Empty string clears. Legacy customer_message is accepted as an alias for intro; do not send both.Details
Plain text, up to 2,000 characters each. Intro appears at the top, Outro at the bottom in every state. Newlines are preserved; safe text URLs become links. Empty string clears. Legacy customer_message is accepted as an alias for intro; do not send both.
intro_font_sizeintegeroptionalPixels: 12, 14, 16, 18, 20 or 24. Default 16 unless inherited differently.
outro_font_sizeintegeroptionalPixels: 12, 14, 16, 18, 20 or 24. Default 16 unless inherited differently.
accent_colorstringoptional#RRGGBB. Background, card and button may be empty for automatic colors. Text contrast is automatic.
background_colorstringoptional#RRGGBB. Background, card and button may be empty for automatic colors. Text contrast is automatic.
card_colorstringoptional#RRGGBB. Background, card and button may be empty for automatic colors. Text contrast is automatic.
button_colorstringoptional#RRGGBB. Background, card and button may be empty for automatic colors. Text contrast is automatic.
imagesobjectoptional
Keys logo_light, logo_dark, favicon. Omitted key keeps the base image; null removes it. An object {store_id: UUID, kind?: logo_light|logo_dark|favicon} reuses that store's effective uploaded image in the SAME project. kind defaults to the target key. Upload in Store → Checkout first; copy the Store API ID from Basic → API IDs. Missing images or cross-project IDs return 400. No external URLs or image data accepted.Details
Keys logo_light, logo_dark, favicon. Omitted key keeps the base image; null removes it. An object {store_id: UUID, kind?: logo_light|logo_dark|favicon} reuses that store's effective uploaded image in the SAME project. kind defaults to the target key. Upload in Store → Checkout first; copy the Store API ID from Basic → API IDs. Missing images or cross-project IDs return 400. No external URLs or image data accepted.
show_order_idbooleanoptionalShow order ID details and a plain-text description below the title. details_expanded opens order ID details initially. Display only, not data redaction.
show_descriptionbooleanoptionalShow order ID details and a plain-text description below the title. details_expanded opens order ID details initially. Display only, not data redaction.
details_expandedbooleanoptionalShow order ID details and a plain-text description below the title. details_expanded opens order ID details initially. Display only, not data redaction.
show_project_namebooleanoptionalMerchant 5.6.0+: show or hide each name in the customer checkout header. Both default to true. Also available in Store → Checkout; inherited and invoice-snapshotted like other appearance settings. Display only, not data redaction.Details
Merchant 5.6.0+: show or hide each name in the customer checkout header. Both default to true. Also available in Store → Checkout; inherited and invoice-snapshotted like other appearance settings. Display only, not data redaction.
show_store_namebooleanoptionalMerchant 5.6.0+: show or hide each name in the customer checkout header. Both default to true. Also available in Store → Checkout; inherited and invoice-snapshotted like other appearance settings. Display only, not data redaction.Details
Merchant 5.6.0+: show or hide each name in the customer checkout header. Both default to true. Also available in Store → Checkout; inherited and invoice-snapshotted like other appearance settings. Display only, not data redaction.
featured_chains[]string[]optionalOrdered chain slugs, at most 60 unique values (lowercase letters, digits, hyphens; up to 64 characters). [] clears. Only available invoice methods are reordered.Details
Ordered chain slugs, at most 60 unique values (lowercase letters, digits, hyphens; up to 64 characters). [] clears. Only available invoice methods are reordered.
featured_asset_ids[]UUID[]optionalUp to 100 unique ordered asset IDs; [] clears. Default asset may be null. IDs come from payment-assets, not payment-intent IDs. These never enable methods; received payments and valid customer preferences take priority.Details
Up to 100 unique ordered asset IDs; [] clears. Default asset may be null. IDs come from payment-assets, not payment-intent IDs. These never enable methods; received payments and valid customer preferences take priority.
default_asset_idUUID|nulloptionalUp to 100 unique ordered asset IDs; [] clears. Default asset may be null. IDs come from payment-assets, not payment-intent IDs. These never enable methods; received payments and valid customer preferences take priority.Details
Up to 100 unique ordered asset IDs; [] clears. Default asset may be null. IDs come from payment-assets, not payment-intent IDs. These never enable methods; received payments and valid customer preferences take priority.
messagesobjectoptional
Language objects keyed by en, de, es, pt-BR, it, ru, zh-CN, fr, ko or ja, with waiting, confirming, paid, underpaid, expired plain strings (500 characters each). Only supplied languages/states change; {} clears all messages, {en:{}} clears English, and an empty state string clears that state. English is the fallback. Does not replace actual status.Details
Language objects keyed by en, de, es, pt-BR, it, ru, zh-CN, fr, ko or ja, with waiting, confirming, paid, underpaid, expired plain strings (500 characters each). Only supplied languages/states change; {} clears all messages, {en:{}} clears English, and an empty state string clears that state. English is the fallback. Does not replace actual status.
enobjectoptional
deobjectoptional
esobjectoptional
pt-BRobjectoptional
itobjectoptional
ruobjectoptional
zh-CNobjectoptional
frobjectoptional
koobjectoptional
support_urlstringoptionalHTTPS URLs up to 2,048 characters, without credentials. Empty clears. Links open in a new window.
terms_urlstringoptionalHTTPS URLs up to 2,048 characters, without credentials. Empty clears. Links open in a new window.
privacy_urlstringoptionalHTTPS URLs up to 2,048 characters, without credentials. Empty clears. Links open in a new window.
return_button_textstringoptionalLabel up to 60 characters. Use top-level redirect_url/cancel_url/redirect_automatically/language for invoice behavior.
show_return_buttonbooleanoptionalShow the manual Return to merchant/cancel button in checkout, including terminal states. Omit to inherit the store appearance (default true); false hides it. Does not cancel an invoice, hide URLs in JSON or disable automatic success redirects. Set top-level redirect_automatically to false separately if needed. Requires merchant 8.1.4 or newer.Details
Show the manual Return to merchant/cancel button in checkout, including terminal states. Omit to inherit the store appearance (default true); false hides it. Does not cancel an invoice, hide URLs in JSON or disable automatic success redirects. Set top-level redirect_automatically to false separately if needed. Requires merchant 8.1.4 or newer.
metadataobject | nulloptional
Merchant-only JSON object; omitted or null becomes {}, maximum 4,096 encoded bytes and five nested levels. firstname, lastname, street, street2, zip, city, country, countryiso2, company, and vatid are validated, normalized, and projected into customer summary fields.Details
Merchant-only JSON object; omitted or null becomes {}, maximum 4,096 encoded bytes and five nested levels. firstname, lastname, street, street2, zip, city, country, countryiso2, company, and vatid are validated, normalized, and projected into customer summary fields.
allocations[]object[]
[{"vendor_id":"VENDOR_A_UUID","gross_amount":"100.00"},{"vendor_id":"VENDOR_B_UUID","gross_amount":"100.00"},{"vendor_id":"VENDOR_C_UUID","gross_amount":"100.00"}]Details
[{"vendor_id":"VENDOR_A_UUID","gross_amount":"100.00"},{"vendor_id":"VENDOR_B_UUID","gross_amount":"100.00"},{"vendor_id":"VENDOR_C_UUID","gross_amount":"100.00"}]
Response 200 application/json
Example response
{
"data": {
"invoice_id": "YOUR_INVOICE_ID",
"status": "new",
"marketplace": {
"state": "pending",
"allocations": []
}
},
"links": {
"checkout": "https://pay.example.com/invoice/YOUR_INVOICE_ID"
}
}Response fields
Usage notes
- Requires invoices.write. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETInvoice and allocations/v1/marketplace/invoices/{invoice_id}Read only
Normal invoice details plus marketplace state, revision, hold_reason, frozen allocations, policy snapshot, latest 200 payments and latest 100 related payouts with truncation flags. Public checkout does not expose vendor data.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETInvoice payment evidence/v1/marketplace/invoices/{invoice_id}/paymentsRead only
Normal payment evidence with page/per_page. Observations are not vendor payouts.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/payments?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/payments?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/payments?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/payments?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETInvoice allocations/v1/marketplace/invoices/{invoice_id}/allocationsRead only
Immutable share/commission/destination snapshots and exact gross, commission and net atomic values after qualified settlement.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/allocations?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/allocations?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/allocations?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/allocations?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTCancel unpaid invoice/v1/marketplace/invoices/{invoice_id}/cancelRead + write
Only an unpaid new or expired invoice. Late receipts are held for review. Revision is the Marketplace invoice revision.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | invoices.write |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/cancel" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}`;
const response = await fetch("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/cancel", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/cancel");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/cancel",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires invoices.write. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTHold allocations/v1/marketplace/invoices/{invoice_id}/allocations/holdRead + write
Hold unsigned allocations without erasing obligations. Signed or uncertain payouts remain tracked.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | reconciliation.write |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/allocations/hold" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"reason": "Verify customer request"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"reason": "Verify customer request"
}`;
const response = await fetch("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/allocations/hold", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"reason": "Verify customer request"
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/allocations/hold");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"reason": "Verify customer request"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/allocations/hold",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires reconciliation.write. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTReview and release allocations/v1/marketplace/invoices/{invoice_id}/allocations/releaseRead + write
Recheck backing/finality. Optional approved_amount_atomic explicitly accepts a reduced fully backed split before allocation freeze; never fabricates receipt funds.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | reconciliation.write |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/allocations/release" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"reason": "Verified payment evidence"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"reason": "Verified payment evidence"
}`;
const response = await fetch("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/allocations/release", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"reason": "Verified payment evidence"
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/allocations/release");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"reason": "Verified payment evidence"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/allocations/release",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires reconciliation.write. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTRebind unpaid destination/v1/marketplace/invoices/{invoice_id}/allocations/{allocation_id}/destinationRead + write
Allocation revision required. Only unreserved/unsigned work and a separately approved version for the same vendor on the original network. Audited; signed work cannot change.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | reconciliation.write |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/allocations/YOUR_ALLOCATION_ID/destination" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"destination_id": "YOUR_DESTINATION_ID",
"reason": "Vendor verified replacement address"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"destination_id": "YOUR_DESTINATION_ID",
"reason": "Vendor verified replacement address"
}`;
const response = await fetch("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/allocations/YOUR_ALLOCATION_ID/destination", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"destination_id": "YOUR_DESTINATION_ID",
"reason": "Vendor verified replacement address"
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/allocations/YOUR_ALLOCATION_ID/destination");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"destination_id": "YOUR_DESTINATION_ID",
"reason": "Vendor verified replacement address"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/allocations/YOUR_ALLOCATION_ID/destination",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires reconciliation.write. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTPrepare full refund/v1/marketplace/invoices/{invoice_id}/refundRead + write
Prepare a separately approved same-asset full received-amount refund. Before vendor payout reserve unpaid funds and reverse commission on confirmation. After all vendor payouts use separate free primary-address funds. Never infer the customer refund address from the sender.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | reconciliation.write |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/refund" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"payment_method_id": "YOUR_PAYMENT_METHOD_ID",
"destination_address": "VERIFIED_CUSTOMER_REFUND_ADDRESS",
"reason": "Customer cancellation",
"maximum_network_fee_atomic": "10000",
"maximum_gas_funding_atomic": "0",
"fee_tier": "medium"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"payment_method_id": "YOUR_PAYMENT_METHOD_ID",
"destination_address": "VERIFIED_CUSTOMER_REFUND_ADDRESS",
"reason": "Customer cancellation",
"maximum_network_fee_atomic": "10000",
"maximum_gas_funding_atomic": "0",
"fee_tier": "medium"
}`;
const response = await fetch("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/refund", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"payment_method_id": "YOUR_PAYMENT_METHOD_ID",
"destination_address": "VERIFIED_CUSTOMER_REFUND_ADDRESS",
"reason": "Customer cancellation",
"maximum_network_fee_atomic": "10000",
"maximum_gas_funding_atomic": "0",
"fee_tier": "medium"
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/refund");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"payment_method_id": "YOUR_PAYMENT_METHOD_ID",
"destination_address": "VERIFIED_CUSTOMER_REFUND_ADDRESS",
"reason": "Customer cancellation",
"maximum_network_fee_atomic": "10000",
"maximum_gas_funding_atomic": "0",
"fee_tier": "medium"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/invoices/YOUR_INVOICE_ID/refund",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires reconciliation.write. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETFind payout allocations/v1/marketplace/allocationsRead only
Paged candidates; filter status (available etc.), asset_id, vendor_id, store_id and q (vendor/order/invoice).
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/allocations?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/allocations?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/allocations?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/allocations?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"pagination": {
"page": 1,
"per_page": 25,
"total": 0,
"pages": 1
}
}Response fields
Usage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETProject ledger balances/v1/marketplace/balancesRead only
Exact grouped asset/account totals including held, available, reserved, paid, commission, refunds and network fees. External balancing account is excluded.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/balances?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/balances?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/balances?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/balances?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETImmutable journal/v1/marketplace/ledgerRead only
Paged append-only movements; q reference/account, asset_id and vendor_id filters. Amounts use atomic strings; no entry can be edited or marked paid manually.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/ledger?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/ledger?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/ledger?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/ledger?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"pagination": {
"page": 1,
"per_page": 25,
"total": 0,
"pages": 1
}
}Response fields
Usage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETPayout history/v1/marketplace/payoutsRead only
Paged parents; filter state, asset_id, vendor_id or q ID/symbol. Paid means independent confirmation, not just broadcast.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/payouts?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/payouts?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/payouts?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/payouts?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"pagination": {
"page": 1,
"per_page": 25,
"total": 0,
"pages": 1
}
}Response fields
Usage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTPreview allocation plan/v1/marketplace/payouts/previewRead + write
Read-only accounting plan, no reservation or spend authority. Maximum fee is a user cap, not an estimated actual fee. Preparation performs live checks later.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | payouts.write |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/payouts/preview" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"allocation_ids": [
"YOUR_ALLOCATION_ID"
],
"maximum_network_fee_atomic": "10000",
"maximum_gas_funding_atomic": "0",
"fee_tier": "medium"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"allocation_ids": [
"YOUR_ALLOCATION_ID"
],
"maximum_network_fee_atomic": "10000",
"maximum_gas_funding_atomic": "0",
"fee_tier": "medium"
}`;
const response = await fetch("https://api.example.com/v1/marketplace/payouts/preview", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"allocation_ids": [
"YOUR_ALLOCATION_ID"
],
"maximum_network_fee_atomic": "10000",
"maximum_gas_funding_atomic": "0",
"fee_tier": "medium"
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/payouts/preview");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"allocation_ids": [
"YOUR_ALLOCATION_ID"
],
"maximum_network_fee_atomic": "10000",
"maximum_gas_funding_atomic": "0",
"fee_tier": "medium"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/payouts/preview",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires payouts.write. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTPrepare payout/v1/marketplace/payoutsRead + write
Reserve 1–100 available allocation IDs in one asset/wallet. BTC requires all unpaid shares for each selected invoice in one multi-output batch. Budgets are atomic strings in the native fee asset. Creates an awaiting_approval plan, does not broadcast.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | payouts.write |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/payouts" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"allocation_ids": [
"YOUR_ALLOCATION_ID"
],
"maximum_network_fee_atomic": "10000",
"maximum_gas_funding_atomic": "0",
"fee_tier": "medium"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"allocation_ids": [
"YOUR_ALLOCATION_ID"
],
"maximum_network_fee_atomic": "10000",
"maximum_gas_funding_atomic": "0",
"fee_tier": "medium"
}`;
const response = await fetch("https://api.example.com/v1/marketplace/payouts", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"allocation_ids": [
"YOUR_ALLOCATION_ID"
],
"maximum_network_fee_atomic": "10000",
"maximum_gas_funding_atomic": "0",
"fee_tier": "medium"
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/payouts");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"allocation_ids": [
"YOUR_ALLOCATION_ID"
],
"maximum_network_fee_atomic": "10000",
"maximum_gas_funding_atomic": "0",
"fee_tier": "medium"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/payouts",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {
"id": "YOUR_PAYOUT_ID",
"state": "awaiting_approval",
"revision": 1,
"plan_hash": "RETAINED_PLAN_HASH",
"plan": {
"amount_atomic": "28800000",
"maximum_network_fee_atomic": "10000",
"maximum_gas_funding_atomic": "0",
"items": []
},
"transactions": []
}
}Response fields
Usage notes
- Requires payouts.write. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETPayout details/v1/marketplace/payouts/{payout_id}Read only
Immutable plan/hash, revision, expiry, recipient items, preserved child attempts, progress, exact fees and explorer links. Never private keys or signed bytes.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/payouts/YOUR_PAYOUT_ID?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/payouts/YOUR_PAYOUT_ID?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/payouts/YOUR_PAYOUT_ID?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/payouts/YOUR_PAYOUT_ID?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": {
"id": "YOUR_PAYOUT_ID",
"state": "awaiting_approval",
"revision": 1,
"plan_hash": "RETAINED_PLAN_HASH",
"plan": {
"amount_atomic": "28800000",
"maximum_network_fee_atomic": "10000",
"maximum_gas_funding_atomic": "0",
"items": []
},
"transactions": []
}
}Response fields
Usage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTApprove and send/v1/marketplace/payouts/{payout_id}/approveRead + write
Explicitly authorize the exact retained plan within its expiry. Queue bounded gas funding and vendor transactions in the background.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | payouts.approve |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/payouts/YOUR_PAYOUT_ID/approve" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"expected_plan_hash": "COPY_EXACT_PLAN_HASH_FROM_PREPARE",
"confirm": true
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"expected_plan_hash": "COPY_EXACT_PLAN_HASH_FROM_PREPARE",
"confirm": true
}`;
const response = await fetch("https://api.example.com/v1/marketplace/payouts/YOUR_PAYOUT_ID/approve", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"expected_plan_hash": "COPY_EXACT_PLAN_HASH_FROM_PREPARE",
"confirm": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/payouts/YOUR_PAYOUT_ID/approve");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"expected_plan_hash": "COPY_EXACT_PLAN_HASH_FROM_PREPARE",
"confirm": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/payouts/YOUR_PAYOUT_ID/approve",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires payouts.approve. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTReview and resume payout/v1/marketplace/payouts/{payout_id}/resumeRead + write
Only needs_attention. Reuse the retained plan/hash; confirmed recipients are never resent. Two independent providers must prove a canonical sufficiently confirmed EVM revert before a failed child can be replaced. Previous fees still consume the approved budget and all hashes remain in history. Unknown/partial token delivery returns payout_recovery_unverified; reservations remain held.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | payouts.approve |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/payouts/YOUR_PAYOUT_ID/resume" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"expected_plan_hash": "COPY_EXACT_PLAN_HASH_FROM_PREPARE",
"confirm": true
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"expected_plan_hash": "COPY_EXACT_PLAN_HASH_FROM_PREPARE",
"confirm": true
}`;
const response = await fetch("https://api.example.com/v1/marketplace/payouts/YOUR_PAYOUT_ID/resume", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"expected_plan_hash": "COPY_EXACT_PLAN_HASH_FROM_PREPARE",
"confirm": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/payouts/YOUR_PAYOUT_ID/resume");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"expected_plan_hash": "COPY_EXACT_PLAN_HASH_FROM_PREPARE",
"confirm": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/payouts/YOUR_PAYOUT_ID/resume",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires payouts.approve. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTCancel unsigned work/v1/marketplace/payouts/{payout_id}/cancelRead + write
Release only safely unsigned/unsubmitted reservations. Confirmed recipients and audit remain. Signed/uncertain work cannot be cancelled. An explicitly recovered, proven revert no longer blocks cancellation of the remaining unsigned work.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | payouts.write |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/payouts/YOUR_PAYOUT_ID/cancel" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}`;
const response = await fetch("https://api.example.com/v1/marketplace/payouts/YOUR_PAYOUT_ID/cancel", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/payouts/YOUR_PAYOUT_ID/cancel");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/payouts/YOUR_PAYOUT_ID/cancel",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires payouts.write. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETAutomatic payout policies/v1/marketplace/policiesRead only
Read per-asset policies, revision, next_attempt_at, last_checked_at and a safe last_error when preparation or authorization needs attention.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/policies?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/policies?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/policies?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/policies?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": []
}Response fields
data[]arrayUsage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTAuthorize payout policy/v1/marketplace/policiesRead + write
Exact atomic strings; minimum <= transfer <= daily principal, fee/gas caps <= their daily budgets. interval_seconds 60–604800; minimum_confirmations 1–100000 subject to chain floor. Revision 0 creates. Enabled policies may send without further clicks; keep false until reviewed.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | policies.write |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/policies" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"asset_id": "YOUR_ASSET_ID",
"revision": 0,
"enabled": false,
"minimum_atomic": "100000",
"transfer_limit_atomic": "10000000",
"daily_limit_atomic": "30000000",
"fee_limit_atomic": "10000",
"daily_fee_limit_atomic": "30000",
"gas_limit_atomic": "0",
"daily_gas_limit_atomic": "0",
"interval_seconds": 3600,
"minimum_confirmations": 2
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"asset_id": "YOUR_ASSET_ID",
"revision": 0,
"enabled": false,
"minimum_atomic": "100000",
"transfer_limit_atomic": "10000000",
"daily_limit_atomic": "30000000",
"fee_limit_atomic": "10000",
"daily_fee_limit_atomic": "30000",
"gas_limit_atomic": "0",
"daily_gas_limit_atomic": "0",
"interval_seconds": 3600,
"minimum_confirmations": 2
}`;
const response = await fetch("https://api.example.com/v1/marketplace/policies", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"asset_id": "YOUR_ASSET_ID",
"revision": 0,
"enabled": false,
"minimum_atomic": "100000",
"transfer_limit_atomic": "10000000",
"daily_limit_atomic": "30000000",
"fee_limit_atomic": "10000",
"daily_fee_limit_atomic": "30000",
"gas_limit_atomic": "0",
"daily_gas_limit_atomic": "0",
"interval_seconds": 3600,
"minimum_confirmations": 2
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/policies");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"asset_id": "YOUR_ASSET_ID",
"revision": 0,
"enabled": false,
"minimum_atomic": "100000",
"transfer_limit_atomic": "10000000",
"daily_limit_atomic": "30000000",
"fee_limit_atomic": "10000",
"daily_fee_limit_atomic": "30000",
"gas_limit_atomic": "0",
"daily_gas_limit_atomic": "0",
"interval_seconds": 3600,
"minimum_confirmations": 2
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/policies",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires policies.write. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETMarketplace webhooks/v1/marketplace/webhooksRead only
Up to 20 endpoints plus supported event_types. Never returns encrypted or plaintext signing secrets.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/webhooks?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/webhooks?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/webhooks?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/webhooks?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": {
"endpoints": [],
"event_types": [
"marketplace.invoice.created",
"marketplace.allocations.available",
"marketplace.allocations.held",
"marketplace.payout.approved",
"marketplace.payout.broadcast",
"marketplace.payout.confirmed",
"marketplace.payout.partial",
"marketplace.payout.failed",
"marketplace.payout.cancelled",
"marketplace.refund.prepared",
"marketplace.refund.confirmed"
]
}
}Response fields
Usage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTCreate Marketplace webhook/v1/marketplace/webhooksRead + write
Public HTTPS only, no credentials, private destinations or redirects. Returns signing_secret once per retained idempotent create response; store securely. Separate from the store IPN secret.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | webhooks.write |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/webhooks" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"url": "https://shop.example.com/marketplace-events",
"enabled": true,
"events": [
"marketplace.payout.confirmed",
"marketplace.payout.failed"
]
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"url": "https://shop.example.com/marketplace-events",
"enabled": true,
"events": [
"marketplace.payout.confirmed",
"marketplace.payout.failed"
]
}`;
const response = await fetch("https://api.example.com/v1/marketplace/webhooks", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"url": "https://shop.example.com/marketplace-events",
"enabled": true,
"events": [
"marketplace.payout.confirmed",
"marketplace.payout.failed"
]
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/webhooks");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"url": "https://shop.example.com/marketplace-events",
"enabled": true,
"events": [
"marketplace.payout.confirmed",
"marketplace.payout.failed"
]
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/webhooks",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires webhooks.write. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTUpdate Marketplace webhook/v1/marketplace/webhooks/{webhook_id}/updateRead + write
Changes cancel old unsent deliveries. The secret remains unchanged; old immutable payloads are never redirected to a new URL.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | webhooks.write |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/webhooks/YOUR_WEBHOOK_ID/update" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"url": "https://shop.example.com/marketplace-events",
"enabled": true,
"events": [
"marketplace.payout.confirmed"
]
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"url": "https://shop.example.com/marketplace-events",
"enabled": true,
"events": [
"marketplace.payout.confirmed"
]
}`;
const response = await fetch("https://api.example.com/v1/marketplace/webhooks/YOUR_WEBHOOK_ID/update", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"url": "https://shop.example.com/marketplace-events",
"enabled": true,
"events": [
"marketplace.payout.confirmed"
]
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/webhooks/YOUR_WEBHOOK_ID/update");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"revision": 1,
"url": "https://shop.example.com/marketplace-events",
"enabled": true,
"events": [
"marketplace.payout.confirmed"
]
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/webhooks/YOUR_WEBHOOK_ID/update",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires webhooks.write. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTDisable Marketplace webhook/v1/marketplace/webhooks/{webhook_id}/disableRead + write
Stop new deliveries; retained event history remains.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | webhooks.write |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/webhooks/YOUR_WEBHOOK_ID/disable" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}`;
const response = await fetch("https://api.example.com/v1/marketplace/webhooks/YOUR_WEBHOOK_ID/disable", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/webhooks/YOUR_WEBHOOK_ID/disable");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"revision": 1
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/webhooks/YOUR_WEBHOOK_ID/disable",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires webhooks.write. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETMarketplace events/v1/marketplace/eventsRead only
Paged immutable event references. q searches type/reference; status filters event_type.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/events?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/events?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/events?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/events?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"pagination": {
"page": 1,
"per_page": 25,
"total": 0,
"pages": 1
}
}Response fields
Usage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETRead retained event/v1/marketplace/events/{event_id}Read only
Returns the immutable original event payload, not a reconstruction from current balances.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/events/YOUR_EVENT_ID?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/events/YOUR_EVENT_ID?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/events/YOUR_EVENT_ID?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/events/YOUR_EVENT_ID?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": {
"event_id": "11111111-1111-4111-8111-111111111111",
"event_type": "marketplace.payout.confirmed",
"payload_version": 1,
"project_id": "YOUR_PROJECT_ID",
"occurred_at": "2026-10-06T12:00:00Z",
"data": {
"payout_id": "YOUR_PAYOUT_ID",
"asset_id": "YOUR_ASSET_ID",
"amount_atomic": "28800000",
"transactions": [
{
"transaction_id": "EXAMPLE_TRANSACTION_ID",
"explorer_url": "https://mempool.space/tx/EXAMPLE_TRANSACTION_ID"
}
]
}
}
}Response fields
Usage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETMarketplace delivery history/v1/marketplace/webhook-deliveriesRead only
Paged deliveries, event type, attempts and last result. Filter status pending/sending/succeeded/failed/cancelled; q event type.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/webhook-deliveries?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/webhook-deliveries?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/webhook-deliveries?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/webhook-deliveries?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [],
"pagination": {
"page": 1,
"per_page": 25,
"total": 0,
"pages": 1
}
}Response fields
Usage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETDelivery details/v1/marketplace/webhook-deliveries/{delivery_id}Read only
Original URL, retained payload, HTTP result and safe error; no signing secret.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | marketplace.read |
Query parameters
pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
per_pageoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
qoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
statusoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
store_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
vendor_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
asset_idoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
fromoptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
tooptionalUse only filters documented for this resource. Paged lists default to 25 and allow up to 100 rows.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/marketplace/webhook-deliveries/YOUR_DELIVERY_ID?project_id=YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/marketplace/webhook-deliveries/YOUR_DELIVERY_ID?project_id=YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/marketplace/webhook-deliveries/YOUR_DELIVERY_ID?project_id=YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/marketplace/webhook-deliveries/YOUR_DELIVERY_ID?project_id=YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires marketplace.read. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
POSTRedeliver same event/v1/marketplace/webhook-deliveries/{delivery_id}/resendRead + write
Only finished deliveries and the unchanged active endpoint; up to 20 manual retries. Reuses event_id. A failed notification never authorizes a second payment.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MARKETPLACE_API_TOKEN |
| Idempotency-Key | required | 16–128 letters, digits, -, _ or .; persist with this exact request |
| Parameter | Type / location | Rule |
|---|---|---|
| permission | credential scope | webhooks.write |
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/marketplace/webhook-deliveries/YOUR_DELIVERY_ID/resend" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: marketplace-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"project_id": "YOUR_PROJECT_ID",
"confirm": true
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"project_id": "YOUR_PROJECT_ID",
"confirm": true
}`;
const response = await fetch("https://api.example.com/v1/marketplace/webhook-deliveries/YOUR_DELIVERY_ID/resend", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"project_id": "YOUR_PROJECT_ID",
"confirm": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/webhook-deliveries/YOUR_DELIVERY_ID/resend");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: marketplace-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "marketplace-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"project_id": "YOUR_PROJECT_ID",
"confirm": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/webhook-deliveries/YOUR_DELIVERY_ID/resend",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": {}
}Response fields
dataobjectUsage notes
- Requires webhooks.write. Response examples show selected fields; GET the resource for full retained evidence.
- Every POST requires a persisted 16–128 character Idempotency-Key (letters, digits, -, _ or .). Reuse the same key and JSON instructions after a timeout. Different instructions return 409. Completed responses are retained for 30 days; old keys become non-reusable tombstones. Never change a key simply to bypass an uncertain result.
- Keys have project lists, optional invoice-store restrictions, expiry, IP allowlist and a 1–600/minute quota (default 60). HTTP 429 includes Retry-After. Store-limited keys cannot see vendors, project balances, ledger, payouts or settings. Keep spending credentials only on trusted servers, never browsers or shipped mobile apps.
Errors
GETList payment exceptions/v1/projects/{project_id}/reconciliationRead only
One paginated review queue for underpaid, overpaid, late, reorged or ambiguous payments, failed deliveries, and disabled/expired methods. Cases acknowledged by an operator reopen when new evidence arrives.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
Path parameters
Query parameters
reasonstringunderpaid, overpaid, late, reorged, ambiguous, delivery_failed, disabled_method, or expired_method.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/reconciliation?status=open&page=1" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/reconciliation?status=open&page=1", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/reconciliation?status=open&page=1");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/reconciliation?status=open&page=1",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{"data":[],"pagination":{"page":1,"per_page":25,"total":0,"has_more":false},"counts":{"open":0,"resolved":0}}Response fields
data[]object[]always
Newest updated cases first. Use invoice_id, not internal id, in merchant detail URLs.
idUUIDalwaysInternal record ID / customer-facing invoice UUID. invoice_id matches callback payloads.
invoice_idUUIDalwaysInternal record ID / customer-facing invoice UUID. invoice_id matches callback payloads.
paginationobjectalways
page (1–40001), per_page (25), total matching rows, has_more.
Usage notes
- Read-only, project-scoped and covered by the credential quota. Financial decisions and refunds remain console-only.
- Rows contain id (internal UUID), invoice_id (public UUID, same as callbacks), store information, original fiat amount/currency, invoice_status, case status, reasons, revision and updated_at. Use invoice_id in the merchant detail endpoint.
- Automatic detection follows the original invoice monitoring window; Rescan extends observation by one hour without enabling checkout. Post-settlement/cancelled methods continue to be monitored within that window.
Errors
GETRead reconciliation evidence/v1/projects/{project_id}/reconciliation/{invoice_id}Read only
Returns invoice, case, exact method totals and available refund amounts, observed transactions, delivery history, merchant decisions and linked refund transfers. It never exposes signing keys or callback secrets.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
Path parameters
Query parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/reconciliation/YOUR_PUBLIC_INVOICE_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/reconciliation/YOUR_PUBLIC_INVOICE_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/reconciliation/YOUR_PUBLIC_INVOICE_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/reconciliation/YOUR_PUBLIC_INVOICE_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{"invoice":{"invoice_id":"YOUR_PUBLIC_INVOICE_ID","status":"processing"},"case":{"status":"open","reasons":["underpaid"],"revision":1},"methods":[],"history":[],"history_pagination":{"page":1,"per_page":25,"total":0},"refunds":[],"observations":[],"deliveries":[]}Response fields
invoiceobjectalways
Full merchant invoice: summary fields, private metadata and payment_intents. Not wrapped in data.
customer_namestring | nullalwaysDerived display name from private firstname, lastname, and company metadata.
customer_addressstring | nullalwaysDerived one-line merchant address from private company, street, street2, zip, city, country, countryiso2, and vatid metadata.
exchange_rate_spread_percentdecimal stringalwaysLocked quote spread: the creation override, or the store default when omitted. Applied before upward rounding; never changes on this invoice.
underpayment_tolerance_percentdecimal stringalwaysImmutable accepted shortfall percentage snapshotted when the invoice was created.
amount_statusamount statusalwaysnone, partial, paid, or overpaid. An explicitly allowed zero-amount invoice settles with none and no payment methods.
monitoring_expires_atRFC 3339 timestampalwaysLatest configured late-monitoring cutoff across payment methods.
ipn_urlstring | nullalwaysEffective per-invoice IPN target. Merchant response only; omitted from public checkout.
cancel_urlstring | nullalwaysEffective return URL used when checkout ends without successful payment.
metadataobjectalwaysMerchant metadata. Never returned by public checkout. Console payment tools add payment_tool provenance (id, kind, title, pricing, requested_amount, request_id); it does not prove payment. Authenticate callbacks and fulfill invoice.settled once per invoice_id.Details
Merchant metadata. Never returned by public checkout. Console payment tools add payment_tool provenance (id, kind, title, pricing, requested_amount, request_id); it does not prove payment. Authenticate callbacks and fulfill invoice.settled once per invoice_id.
payment_intents[]object[]always
Quoted payment methods and monitoring state.
payment_railonchain | lightningalwaysInvoice transport. Bitcoin on-chain and Lightning can share asset_id; use intent id plus this field, not symbol alone. This differs from the asset catalog's scanner payment_rail.Details
Invoice transport. Bitcoin on-chain and Lightning can share asset_id; use intent id plus this field, not symbol alone. This differs from the asset catalog's scanner payment_rail.
bolt11string | nullalwaysLightning payment request, otherwise null. Pay this request with a Lightning wallet, never send on-chain funds to its payment hash.
asset_decimalsintegeralwaysAtomic-unit precision. Lightning BTC uses 11 (millisatoshis), not on-chain Bitcoin's 8. Quotes are whole satoshis; receipts retain millisatoshi precision.
quote_ratedecimal stringalwaysAsset units per one invoice currency unit, including the locked spread. For example 1.02 USDC per USD. Not the inverse rate.
quote_detailsobject | nullalwaysLocked quote provenance: reference_rate before spread, unrounded_payment_amount, rounding_adjustment, pricing_provider, asset_provider, pricing_fetched_at and asset_fetched_at. Null on older invoices; no historical values are fabricated.Details
Locked quote provenance: reference_rate before spread, unrounded_payment_amount, rounding_adjustment, pricing_provider, asset_provider, pricing_fetched_at and asset_fetched_at. Null on older invoices; no historical values are fabricated.
expected_amountdecimal stringalwaysExact locked asset amount to pay after spread and upward rounding. From 4.1.1, recognized verified fiat stablecoins (such as USDC, USDT, DAI, USDS, EURC) round up to at most two decimals; 1.321 becomes 1.33, never 1.32. This is the expected amount even with zero tolerance. Other assets keep adaptive precision. Existing invoices are never repriced.Details
Exact locked asset amount to pay after spread and upward rounding. From 4.1.1, recognized verified fiat stablecoins (such as USDC, USDT, DAI, USDS, EURC) round up to at most two decimals; 1.321 becomes 1.33, never 1.32. This is the expected amount even with zero tolerance. Other assets keep adaptive precision. Existing invoices are never repriced.
minimum_payment_amountdecimal stringalwaysSmallest amount accepted as paid after applying the invoice tolerance.
minimum_payment_amount_atomicinteger stringalwaysExact accepted threshold in the asset's smallest unit.
destination_addressstringalwaysOn-chain receiving address, or the 64-character payment hash for Lightning. Use bolt11 to pay Lightning; its hash is not a Bitcoin address.
destination_tagstring | nullalwaysRequired public payment reference where the rail uses one: XRP destination tag, Stellar memo ID, or TON invoice comment. Null for unique-address rails.
caseobject | nullalways
Current case with status, reasons, revision and timestamps; null when no exception exists. Internal evidence is excluded.
methods[]object[]alwaysid, wallet_id, asset_id, symbol, chain, decimals, expected_atomic, received_atomic, confirmed_atomic, refundable_atomic, address, tag, monitor_error, last_checked_at, monitoring_expires_at and spending_supported. Atomic amounts are strings.Details
id, wallet_id, asset_id, symbol, chain, decimals, expected_atomic, received_atomic, confirmed_atomic, refundable_atomic, address, tag, monitor_error, last_checked_at, monitoring_expires_at and spending_supported. Atomic amounts are strings.
history[]object[]alwaysNewest 25 decisions for this page: id, action, note, actor, result, created_at.
history_paginationobjectalways
page, per_page (25), total. Only decision history is paginated by page.
refunds[]object[]alwaysNewest 100 refunds: id, payment_intent_id, amount_atomic, destination, status, request, treasury_intent_id, transfer_status, created_at and transactions (id/status). Refund submission is console-only.Details
Newest 100 refunds: id, payment_intent_id, amount_atomic, destination, status, request, treasury_intent_id, transfer_status, created_at and transactions (id/status). Refund submission is console-only.
observations[]object[]alwaysNewest 100: payment_intent_id, transaction_id, event_index, amount, status, confirmations, observed_at, symbol, chain and disabled_at_detection. explorer_name/explorer_url are included when supported.Details
Newest 100: payment_intent_id, transaction_id, event_index, amount, status, confirmations, observed_at, symbol, chain and disabled_at_detection. explorer_name/explorer_url are included when supported.
deliveries[]object[]alwaysNewest 50: id, kind, status, attempts, response_status, error, next_attempt_at, event_type and created_at. No callback secrets.
Usage notes
- case is null when the invoice has not generated an exception. The most recent 100 observations and 50 deliveries are returned; decision history is paginated.
- refundable_atomic requires at least one network confirmation, excludes existing refund reservations, and is not a promise of spendable wallet funds. A live quote additionally validates wallet readiness, source balances and fees.
- A broadcast refund means submitted to a chain endpoint, not independently confirmed receipt by the customer. Fees are additional and fiat processing fees are not automatically credited by issuing a refund.
- Console project menu → Needs attention provides cancel, accept, reject, reopen, review, notes, Rescan, delivery retry and supported-chain refunds. Decisions use CSRF-protected sessions, a unique request_id, current case revision, a required note and explicit confirmation; bearer tokens cannot invoke those mutations.
Errors
GETAPI service discovery/Public
Managed API-host edge response confirming the v1 public API role. This response is produced by the managed proxy, not the merchant Axum router.
Authentication & headers
Public
Request
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/"// Node.js 18+ · run on your server, never in browser code.
const response = await fetch("https://api.example.com/", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://api.example.com/");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://api.example.com/",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"service": "Wholly Crypto API",
"status": "ready",
"version": "v1"
}Response fields
Usage notes
- No bearer token is needed.
- Only the managed API hostname guarantees this exact root response.
Errors
GETService health/healthzPublic
Checks application reachability and a two-second database ping. Use for monitoring, not as a substitute for invoice status.
Authentication & headers
Public
Request
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/healthz"// Node.js 18+ · run on your server, never in browser code.
const response = await fetch("https://api.example.com/healthz", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://api.example.com/healthz");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://api.example.com/healthz",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 healthy; 503 database unavailable
Example response
{
"status": "ok",
"database": "ok",
"version": "0.1.0"
}Response fields
Usage notes
- No bearer token is needed.
- The version value is the running package version, not the API path version.
Errors
GETList project payment assets/v1/projects/{project_id}/payment-assetsRead only
Lists native assets and verified tokens with project policy, chain-wallet readiness, and installed scanner/balance capabilities. scanner_ready is an adapter-build gate, not a live endpoint quorum result. From 6.0.6, creation retains configured methods during scanner downtime. Receiving verification still needs the configured healthy exact-role provider threshold (2 by default, optional 1).
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | recommended | application/json |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-assets" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Accept: application/json'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-assets", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
"Accept": "application/json"
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-assets");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Accept: application/json"],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Accept": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-assets",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [
{
"asset": {
"id": "10000000-0000-4000-8000-000000000003",
"asset_key": "eip155:1/slip44:60",
"chain_slug": "ethereum",
"network": "mainnet",
"caip_network_id": "eip155:1",
"caip_asset_id": "eip155:1/slip44:60",
"asset_kind": "native",
"payment_rail": "evm-native",
"symbol": "ETH",
"name": "Ethereum",
"decimals": 18,
"contract_address": null,
"coingecko_id": "ethereum",
"icon_path": "/assets/coingecko/ethereum.png",
"token_standard": null,
"metadata_verified_at": null,
"payment_supported": true,
"scanner_ready": true,
"balance_ready": true,
"default_finality_mode": "confirmations",
"default_required_confirmations": 12,
"default_monitoring_minutes": 60
},
"policy": {
"enabled": true,
"finality_mode": "confirmations",
"required_confirmations": 12,
"monitoring_minutes": 60,
"late_monitoring_days": 30
},
"wallet": null,
"wallet_readiness": "wallet_missing",
"receive_readiness": { "ready": false, "invoice_creatable": false, "checked_at": "2026-09-16T09:00:00Z", "issues": [{ "chain_slug": "ethereum", "asset_id": "10000000-0000-4000-8000-000000000003", "asset_ticker": "ETH", "reason_code": "wallet_missing", "message": "ethereum / ETH: Create a project wallet for this chain.", "action": "wallets" }] }
}
]
}Response fields
data[]object[]
assetobjectalways
Durable native or verified token asset.
asset_kindnative | tokenalwaysWhether settlement uses the chain currency or a verified contract/mint.
payment_railstringalwaysRuntime rail: utxo, evm-native, solana-native, account-native, privacy-native, or token-transfer.
contract_addressstring | nullalwaysCanonical ERC-20 contract or SPL mint for tokens; null for native assets.
coingecko_idstring | nullalwaysDiscovery/pricing identity. Null for custom contracts; never infer a market price from their ticker. CoinGecko metadata alone never makes a token selectable.
custom_tokenbooleanalwaysCustom on-chain-verified contract with project-scoped fixed USD or selected DEX pool pricing.
token_standarderc20 | spl-token | nullalwaysVerified runtime token standard; null for native assets.
payment_supportedbooleanalwaysBuild-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.Details
Build-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.
scanner_readybooleanalwaysBuild-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.Details
Build-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.
balance_readybooleanalwaysBuild-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.Details
Build-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.
default_finality_modeconfirmations | finalizedalwaysDefault finality model inherited by a new project policy.
policyobject | nullalways
Project activation/finality policy, or null when not configured. Includes custom_price_mode (fixed/dex), custom_price_usd (fixed decimal string or null), custom_dex_pair (selected pool or null), and custom_dex (dex_id, quote_symbol, current price_usd or null, liquidity_usd, fetched_at, last_error). Custom pricing is shared by stores in this project.Details
Project activation/finality policy, or null when not configured. Includes custom_price_mode (fixed/dex), custom_price_usd (fixed decimal string or null), custom_dex_pair (selected pool or null), and custom_dex (dex_id, quote_symbol, current price_usd or null, liquidity_usd, fetched_at, last_error). Custom pricing is shared by stores in this project.
enabledbooleanrequiredEnables or disables the asset for the project. The native chain must be enabled before any token.
finality_modeconfirmations | finalizedrequiredFinality policy supported by the asset rail. finalized requires required_confirmations=1.
required_confirmationsintegerrequiredBitcoin and EVM rails accept zero; other confirmation rails require at least one, finalized-only rails require exactly one, and EVM rails are bounded to 0–48 so every transfer remains inside the transaction replay window.Details
Bitcoin and EVM rails accept zero; other confirmation rails require at least one, finalized-only rails require exactly one, and EVM rails are bounded to 0–48 so every transfer remains inside the transaction replay window.
walletobject | nullalways
The chain's non-custodial project wallet. Tokens share their chain-native wallet.
primary_addressstring | nullalwaysPublic wallet identity; no seed phrase or private key is exposed.
activation_requiredbooleanalwaysXRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.Details
XRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.
activation_verified_attimestamp|nullalwaysXRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.Details
XRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.
receive_readinessobject | null5.5.0+
Included on wallet listings: project receiving setup and chain scanner prerequisites. Separate from balances, token gas and send readiness. Other wallet responses may leave it null.Details
Included on wallet listings: project receiving setup and chain scanner prerequisites. Separate from balances, token gas and send readiness. Other wallet responses may leave it null.
readybooleanalwaysReceive setup checks pass. Does not describe spending readiness, gas, balance refresh, or a guaranteed future quote.
invoice_creatableboolean6.0.6+Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.Details
Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.
checked_attimestampalwaysAssessment time. No network request or address allocation is made by a listing.
issues[]object[]always
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.Details
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.
reason_codestringalwaysscanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.Details
scanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.
messagestringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
actionstringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
required_endpoint_rolestring | nullon-chainPreferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.Details
Preferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.
accepted_endpoint_roles[]string[] | nullon-chainCompatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.Details
Compatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.
usable_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
required_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
last_checked_attimestamp | nullon-chainLatest matching endpoint health check, separate from assessment time.
monero_wallet_rpcMoneroWalletRpcBinding | nullalwaysSanitized external view-only wallet-RPC binding state for Monero. Includes endpoint, authentication mode, account-0 primary address, technical proof flags/heights, and operator attestation timestamps; credentials, wallet keys, and wallet files are never serialized.Details
Sanitized external view-only wallet-RPC binding state for Monero. Includes endpoint, authentication mode, account-0 primary address, technical proof flags/heights, and operator attestation timestamps; credentials, wallet keys, and wallet files are never serialized.
balances[]object[]always
Cached balances for every one of the 30 native chain rails, plus verified ERC-20 and SPL assets. A configured external view-only wallet-RPC is required for Monero.Details
Cached balances for every one of the 30 native chain rails, plus verified ERC-20 and SPL assets. A configured external view-only wallet-RPC is required for Monero.
active_store_countintegeralwaysNumber of enabled stores that currently select this asset. This is an acceptance projection; read-only balance tracking remains independent.
active_store_ids[]UUID[]alwaysEnabled stores in this project that currently accept the asset. This permits exact local store filtering without another API request.
tracking_activebooleanalwaysWhether this read-capable wallet and registered same-chain asset are eligible for background balance refreshes. Project and payment-method acceptance switches do not pause read-only tracking.Details
Whether this read-capable wallet and registered same-chain asset are eligible for background balance refreshes. Project and payment-method acceptance switches do not pause read-only tracking.
balancedecimal string|nullalwaysExact display and atomic balance across the wallet primary address and issued invoice addresses. Null while a complete value is unavailable.
balance_atomicinteger string|nullalwaysExact display and atomic balance across the wallet primary address and issued invoice addresses. Null while a complete value is unavailable.
statuspending | refreshing | fresh | stale | erroralwaysCached scan state. refreshing can retain a completed balance: use checked_at for its age. Pending means no completed snapshot. None of these states proves a transfer is pending or an invoice settled.Details
Cached scan state. refreshing can retain a completed balance: use checked_at for its age. Pending means no completed snapshot. None of these states proves a transfer is pending or an invoice settled.
balance_statuspending | refreshing | fresh | stale | error | unknownalwaysAggregated cache freshness; unknown is a defensive fallback and none of these states proves invoice settlement.
balance_checked_attimestamp | nullalwaysOldest relevant successful balance check represented by the aggregate.
recent_payments[]object[]always
Up to three newest valid detected, confirming, or final observations attributed to this exact wallet.
statusdetected | confirming | finalalwaysCurrent valid observation state. Reorged, replaced, and invalid observations are excluded.
wallet_readinessreadiness enumalwaysunsupported, project_disabled, project_asset_disabled, store_disabled, store_asset_disabled, wallet_missing, wallet_pending, wallet_disabled, wallet_error, backup_required, account_activation_required, external_wallet_rpc_required, or ready.Details
unsupported, project_disabled, project_asset_disabled, store_disabled, store_asset_disabled, wallet_missing, wallet_pending, wallet_disabled, wallet_error, backup_required, account_activation_required, external_wallet_rpc_required, or ready.
receive_readinessobject | null5.5.0+
Shared project receive-setup assessment. Includes wallet and independent scanner-provider checks, separate from balance freshness and sending gas. Null if no project policy exists. Invoice currency/rates are checked when creating an invoice.Details
Shared project receive-setup assessment. Includes wallet and independent scanner-provider checks, separate from balance freshness and sending gas. Null if no project policy exists. Invoice currency/rates are checked when creating an invoice.
readybooleanalwaysReceive setup checks pass. Does not describe spending readiness, gas, balance refresh, or a guaranteed future quote.
invoice_creatableboolean6.0.6+Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.Details
Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.
checked_attimestampalwaysAssessment time. No network request or address allocation is made by a listing.
issues[]object[]always
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.Details
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.
reason_codestringalwaysscanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.Details
scanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.
messagestringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
actionstringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
required_endpoint_rolestring | nullon-chainPreferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.Details
Preferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.
accepted_endpoint_roles[]string[] | nullon-chainCompatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.Details
Compatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.
usable_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
required_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
last_checked_attimestamp | nullon-chainLatest matching endpoint health check, separate from assessment time.
Usage notes
- A token can be listed globally yet remain unselectable when scanner_ready or payment_supported is false.
- The operator capability matrix also requires the scanner's exact endpoint role; a healthy endpoint serving an incompatible API is not counted.
- Tokens share the project wallet of their native chain; they do not create another seed phrase.
- Embedded wallet summaries are readiness-only and keep balances empty; use GET /v1/projects/{project_id}/wallets for enriched balances.
Errors
PUTUpdate project asset policy/v1/projects/{project_id}/payment-assets/{asset_id}Read + write
Creates or replaces the project policy for one durable asset and returns the refreshed project asset list. Disabling a native chain makes its native asset and tokens unavailable to new invoices, but preserves token policies, wallets, and store selections so they can be resumed later.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Content-Type | required | application/json |
| Accept | recommended | application/json |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request PUT \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-assets/YOUR_ASSET_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Content-Type: application/json' \
--data-raw '{
"enabled": true,
"finality_mode": "confirmations",
"required_confirmations": 2,
"monitoring_minutes": 60,
"late_monitoring_days": 30
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const body = `{
"enabled": true,
"finality_mode": "confirmations",
"required_confirmations": 2,
"monitoring_minutes": 60,
"late_monitoring_days": 30
}`;
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-assets/YOUR_ASSET_ID", {
method: "PUT",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$body = <<<'JSON'
{
"enabled": true,
"finality_mode": "confirmations",
"required_confirmations": 2,
"monitoring_minutes": 60,
"late_monitoring_days": 30
}
JSON;
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-assets/YOUR_ASSET_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"enabled": true,
"finality_mode": "confirmations",
"required_confirmations": 2,
"monitoring_minutes": 60,
"late_monitoring_days": 30
}""".encode("utf-8")
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-assets/YOUR_ASSET_ID",
method="PUT", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
enabledbooleanrequiredEnables or disables the asset for the project. The native chain must be enabled before any token.
finality_modeconfirmations | finalizedrequiredFinality policy supported by the asset rail. finalized requires required_confirmations=1.
required_confirmationsintegerrequiredBitcoin and EVM rails accept zero; other confirmation rails require at least one, finalized-only rails require exactly one, and EVM rails are bounded to 0–48 so every transfer remains inside the transaction replay window.Details
Bitcoin and EVM rails accept zero; other confirmation rails require at least one, finalized-only rails require exactly one, and EVM rails are bounded to 0–48 so every transfer remains inside the transaction replay window.
Response 200 application/json
Example response
{
"data": [
{ "asset": { "id": "ASSET_UUID", "symbol": "USDC", "asset_kind": "token", "scanner_ready": true }, "policy": { "enabled": true, "finality_mode": "confirmations", "required_confirmations": 2, "monitoring_minutes": 60, "late_monitoring_days": 30 }, "wallet_readiness": "ready" }
]
}Response fields
data[]object[]
assetobjectalways
Durable native or verified token asset.
asset_kindnative | tokenalwaysWhether settlement uses the chain currency or a verified contract/mint.
payment_railstringalwaysRuntime rail: utxo, evm-native, solana-native, account-native, privacy-native, or token-transfer.
contract_addressstring | nullalwaysCanonical ERC-20 contract or SPL mint for tokens; null for native assets.
coingecko_idstring | nullalwaysDiscovery/pricing identity. Null for custom contracts; never infer a market price from their ticker. CoinGecko metadata alone never makes a token selectable.
custom_tokenbooleanalwaysCustom on-chain-verified contract with project-scoped fixed USD or selected DEX pool pricing.
token_standarderc20 | spl-token | nullalwaysVerified runtime token standard; null for native assets.
payment_supportedbooleanalwaysBuild-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.Details
Build-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.
scanner_readybooleanalwaysBuild-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.Details
Build-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.
balance_readybooleanalwaysBuild-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.Details
Build-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.
default_finality_modeconfirmations | finalizedalwaysDefault finality model inherited by a new project policy.
policyobject | nullalways
Project activation/finality policy, or null when not configured. Includes custom_price_mode (fixed/dex), custom_price_usd (fixed decimal string or null), custom_dex_pair (selected pool or null), and custom_dex (dex_id, quote_symbol, current price_usd or null, liquidity_usd, fetched_at, last_error). Custom pricing is shared by stores in this project.Details
Project activation/finality policy, or null when not configured. Includes custom_price_mode (fixed/dex), custom_price_usd (fixed decimal string or null), custom_dex_pair (selected pool or null), and custom_dex (dex_id, quote_symbol, current price_usd or null, liquidity_usd, fetched_at, last_error). Custom pricing is shared by stores in this project.
enabledbooleanrequiredEnables or disables the asset for the project. The native chain must be enabled before any token.
finality_modeconfirmations | finalizedrequiredFinality policy supported by the asset rail. finalized requires required_confirmations=1.
required_confirmationsintegerrequiredBitcoin and EVM rails accept zero; other confirmation rails require at least one, finalized-only rails require exactly one, and EVM rails are bounded to 0–48 so every transfer remains inside the transaction replay window.Details
Bitcoin and EVM rails accept zero; other confirmation rails require at least one, finalized-only rails require exactly one, and EVM rails are bounded to 0–48 so every transfer remains inside the transaction replay window.
walletobject | nullalways
The chain's non-custodial project wallet. Tokens share their chain-native wallet.
primary_addressstring | nullalwaysPublic wallet identity; no seed phrase or private key is exposed.
activation_requiredbooleanalwaysXRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.Details
XRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.
activation_verified_attimestamp|nullalwaysXRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.Details
XRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.
receive_readinessobject | null5.5.0+
Included on wallet listings: project receiving setup and chain scanner prerequisites. Separate from balances, token gas and send readiness. Other wallet responses may leave it null.Details
Included on wallet listings: project receiving setup and chain scanner prerequisites. Separate from balances, token gas and send readiness. Other wallet responses may leave it null.
readybooleanalwaysReceive setup checks pass. Does not describe spending readiness, gas, balance refresh, or a guaranteed future quote.
invoice_creatableboolean6.0.6+Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.Details
Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.
checked_attimestampalwaysAssessment time. No network request or address allocation is made by a listing.
issues[]object[]always
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.Details
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.
reason_codestringalwaysscanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.Details
scanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.
messagestringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
actionstringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
required_endpoint_rolestring | nullon-chainPreferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.Details
Preferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.
accepted_endpoint_roles[]string[] | nullon-chainCompatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.Details
Compatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.
usable_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
required_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
last_checked_attimestamp | nullon-chainLatest matching endpoint health check, separate from assessment time.
monero_wallet_rpcMoneroWalletRpcBinding | nullalwaysSanitized external view-only wallet-RPC binding state for Monero. Includes endpoint, authentication mode, account-0 primary address, technical proof flags/heights, and operator attestation timestamps; credentials, wallet keys, and wallet files are never serialized.Details
Sanitized external view-only wallet-RPC binding state for Monero. Includes endpoint, authentication mode, account-0 primary address, technical proof flags/heights, and operator attestation timestamps; credentials, wallet keys, and wallet files are never serialized.
balances[]object[]always
Cached balances for every one of the 30 native chain rails, plus verified ERC-20 and SPL assets. A configured external view-only wallet-RPC is required for Monero.Details
Cached balances for every one of the 30 native chain rails, plus verified ERC-20 and SPL assets. A configured external view-only wallet-RPC is required for Monero.
active_store_countintegeralwaysNumber of enabled stores that currently select this asset. This is an acceptance projection; read-only balance tracking remains independent.
active_store_ids[]UUID[]alwaysEnabled stores in this project that currently accept the asset. This permits exact local store filtering without another API request.
tracking_activebooleanalwaysWhether this read-capable wallet and registered same-chain asset are eligible for background balance refreshes. Project and payment-method acceptance switches do not pause read-only tracking.Details
Whether this read-capable wallet and registered same-chain asset are eligible for background balance refreshes. Project and payment-method acceptance switches do not pause read-only tracking.
balancedecimal string|nullalwaysExact display and atomic balance across the wallet primary address and issued invoice addresses. Null while a complete value is unavailable.
balance_atomicinteger string|nullalwaysExact display and atomic balance across the wallet primary address and issued invoice addresses. Null while a complete value is unavailable.
statuspending | refreshing | fresh | stale | erroralwaysCached scan state. refreshing can retain a completed balance: use checked_at for its age. Pending means no completed snapshot. None of these states proves a transfer is pending or an invoice settled.Details
Cached scan state. refreshing can retain a completed balance: use checked_at for its age. Pending means no completed snapshot. None of these states proves a transfer is pending or an invoice settled.
balance_statuspending | refreshing | fresh | stale | error | unknownalwaysAggregated cache freshness; unknown is a defensive fallback and none of these states proves invoice settlement.
balance_checked_attimestamp | nullalwaysOldest relevant successful balance check represented by the aggregate.
recent_payments[]object[]always
Up to three newest valid detected, confirming, or final observations attributed to this exact wallet.
statusdetected | confirming | finalalwaysCurrent valid observation state. Reorged, replaced, and invalid observations are excluded.
wallet_readinessreadiness enumalwaysunsupported, project_disabled, project_asset_disabled, store_disabled, store_asset_disabled, wallet_missing, wallet_pending, wallet_disabled, wallet_error, backup_required, account_activation_required, external_wallet_rpc_required, or ready.Details
unsupported, project_disabled, project_asset_disabled, store_disabled, store_asset_disabled, wallet_missing, wallet_pending, wallet_disabled, wallet_error, backup_required, account_activation_required, external_wallet_rpc_required, or ready.
receive_readinessobject | null5.5.0+
Shared project receive-setup assessment. Includes wallet and independent scanner-provider checks, separate from balance freshness and sending gas. Null if no project policy exists. Invoice currency/rates are checked when creating an invoice.Details
Shared project receive-setup assessment. Includes wallet and independent scanner-provider checks, separate from balance freshness and sending gas. Null if no project policy exists. Invoice currency/rates are checked when creating an invoice.
readybooleanalwaysReceive setup checks pass. Does not describe spending readiness, gas, balance refresh, or a guaranteed future quote.
invoice_creatableboolean6.0.6+Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.Details
Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.
checked_attimestampalwaysAssessment time. No network request or address allocation is made by a listing.
issues[]object[]always
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.Details
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.
reason_codestringalwaysscanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.Details
scanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.
messagestringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
actionstringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
required_endpoint_rolestring | nullon-chainPreferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.Details
Preferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.
accepted_endpoint_roles[]string[] | nullon-chainCompatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.Details
Compatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.
usable_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
required_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
last_checked_attimestamp | nullon-chainLatest matching endpoint health check, separate from assessment time.
Usage notes
- The body is a full policy replacement and rejects unknown fields.
- Project enablement does not itself select the asset for any store.
Errors
GETBrowse payment-token candidates/v1/projects/{project_id}/payment-token-candidatesRead only
Searches locally cached CoinGecko contract mappings only on chains whose token invoice scanner and balance adapter are implemented. Results are discovery candidates, not trusted payment assets.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | recommended | application/json |
Path parameters
Query parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-candidates?chain_slug=ethereum&q=USDC&limit=50" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-candidates?chain_slug=ethereum&q=USDC&limit=50", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-candidates?chain_slug=ethereum&q=USDC&limit=50");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-candidates?chain_slug=ethereum&q=USDC&limit=50",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [
{
"coingecko_id": "usd-coin",
"chain_slug": "ethereum",
"symbol": "USDC",
"name": "USDC",
"contract_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"market_cap_rank": 7,
"icon_path": "/assets/coingecko/usd-coin.png",
"current_price_usd": "1.0001",
"token_standard": "erc20",
"scanner_ready": true,
"registered_asset_id": null,
"project_enabled": false
}
]
}Response fields
data[]object[]
Usage notes
- Supported token adapters: ERC-20 on Ethereum, Base, BNB Chain, HyperEVM, Avalanche, Polygon, Arbitrum, and Optimism; SPL on Solana.
- Unsupported catalog chains are rejected instead of appearing selectable.
- CoinGecko rank, icon, and price are advisory discovery data.
Errors
POSTVerify and register token/v1/projects/{project_id}/payment-token-assetsRead + write
Promotes one current candidate into the durable payment registry only after configured nodes verify chain identity, contract/mint identity, decimals, and a usable balance query. Registration never trusts CoinGecko metadata by itself, and each project is capped at 20 registered token assets.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Content-Type | required | application/json |
| Accept | recommended | application/json |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-assets" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Content-Type: application/json' \
--data-raw '{
"chain_slug": "ethereum",
"coingecko_id": "usd-coin",
"enabled": true
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const body = `{
"chain_slug": "ethereum",
"coingecko_id": "usd-coin",
"enabled": true
}`;
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-assets", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$body = <<<'JSON'
{
"chain_slug": "ethereum",
"coingecko_id": "usd-coin",
"enabled": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-assets");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"chain_slug": "ethereum",
"coingecko_id": "usd-coin",
"enabled": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-assets",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
chain_slugstringrequiredethereum, base, bnb-chain, hyperliquid, avalanche, polygon, arbitrum, optimism, or solana.
coingecko_idstringrequiredExact candidate identity returned by token search. Preserve leading underscores or hyphens, such as _ or -6. Do not derive this ID from the token name or ticker.Details
Exact candidate identity returned by token search. Preserve leading underscores or hyphens, such as _ or -6. Do not derive this ID from the token name or ticker.
Response 201 application/json
Example response
{
"data": {
"asset_id": "44444444-4444-4444-8444-444444444444",
"chain_slug": "ethereum",
"coingecko_id": "usd-coin",
"contract_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"token_standard": "erc20",
"symbol": "USDC",
"name": "USDC",
"decimals": 6,
"enabled": true,
"metadata_verified_at": "2026-08-31T18:00:00Z"
}
}Response fields
dataobject
Usage notes
- Enable the chain's native project asset before registering its tokens.
- A project can register at most 20 token assets; a new candidate beyond that ceiling returns token_chain_not_ready (409). Reusing an already-registered asset does not consume another slot.
- Node verification can take longer than a catalog read; use an explicit client timeout.
- After registration, select the asset for each store that should offer it.
Errors
GETFind custom token DEX pools/v1/projects/{project_id}/payment-token-dex-poolsRead only
Find up to 12 eligible pools by exact chain and base-token contract via DEX Screener, ordered by liquidity. This does not register or enable a token.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
Path parameters
Query parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-dex-pools?chain_slug=ethereum&contract_address=YOUR_TOKEN_CONTRACT" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-dex-pools?chain_slug=ethereum&contract_address=YOUR_TOKEN_CONTRACT", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-dex-pools?chain_slug=ethereum&contract_address=YOUR_TOKEN_CONTRACT");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-dex-pools?chain_slug=ethereum&contract_address=YOUR_TOKEN_CONTRACT",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{"data":[{"pair_address":"0x2222222222222222222222222222222222222222","dex_id":"uniswap","quote_symbol":"WETH","price_usd":"0.25","liquidity_usd":"250000.00","fetched_at":"2026-09-09T12:00:00Z","url":"https://dexscreener.com/ethereum/0x2222222222222222222222222222222222222222"}]}Response fields
data[]object[]
pair_addressstringalwaysExact pool identifier, exchange ID (e.g. uniswap/pancakeswap), and display-only paired ticker.
dex_idstringalwaysExact pool identifier, exchange ID (e.g. uniswap/pancakeswap), and display-only paired ticker.
quote_symbolstringalwaysExact pool identifier, exchange ID (e.g. uniswap/pancakeswap), and display-only paired ticker.
price_usddecimal stringalwaysUSD price of the requested base token and total pool liquidity. At least $10,000 liquidity and a trade in the last hour are required.
liquidity_usddecimal stringalwaysUSD price of the requested base token and total pool liquidity. At least $10,000 liquidity and a trade in the last hour are required.
fetched_atRFC 3339 timestampalwaysWhen the server retrieved the provider observation, not the timestamp of an on-chain trade.
Usage notes
- An empty data array means no qualifying pool was found. Only pools where the exact requested contract is the base token are returned; quote-side USD prices are never assumed.
- DEX listing is not a security audit. Minimum liquidity and recent activity reduce unusable quotes but do not prevent market manipulation.
- Uniswap, PancakeSwap and other indexed DEXs are supported where the existing chain scanner supports tokens. API access remains project-scoped and rate-limited. Provider calls are also serialized and throttled.
Errors
POSTAdd or reprice custom token/v1/projects/{project_id}/payment-token-assets/customRead + write
Verifies a custom contract using configured chain nodes and registers it without requiring a CoinGecko listing. The fixed USD price or selected automatic DEX pool belongs to this project, not to the ticker or other projects. Repeating the same identity updates its project price without changing an existing enabled/disabled policy.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Content-Type | required | application/json |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-assets/custom" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Content-Type: application/json' \
--data-raw '{
"chain_slug": "ethereum",
"contract_address": "YOUR_VERIFIED_TOKEN_CONTRACT",
"name": "Example token",
"symbol": "EXAMPLE",
"price_usd": "0.25"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const body = `{
"chain_slug": "ethereum",
"contract_address": "YOUR_VERIFIED_TOKEN_CONTRACT",
"name": "Example token",
"symbol": "EXAMPLE",
"price_usd": "0.25"
}`;
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-assets/custom", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$body = <<<'JSON'
{
"chain_slug": "ethereum",
"contract_address": "YOUR_VERIFIED_TOKEN_CONTRACT",
"name": "Example token",
"symbol": "EXAMPLE",
"price_usd": "0.25"
}
JSON;
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-assets/custom");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"chain_slug": "ethereum",
"contract_address": "YOUR_VERIFIED_TOKEN_CONTRACT",
"name": "Example token",
"symbol": "EXAMPLE",
"price_usd": "0.25"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-assets/custom",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
chain_slugstringrequiredethereum, base, bnb-chain, hyperliquid, avalanche, polygon, arbitrum, optimism, or solana. Fixed for this contract.
contract_addressstringrequiredERC-20 contract (0x plus 40 hexadecimal characters), or a classic SPL mint. Nodes verify network identity and exact decimals; caller-supplied decimals and RPC URLs are rejected.Details
ERC-20 contract (0x plus 40 hexadecimal characters), or a classic SPL mint. Nodes verify network identity and exact decimals; caller-supplied decimals and RPC URLs are rejected.
namestringrequiredDisplay name (1–80 characters) and ticker (1–16 letters/digits/dots/underscores/hyphens, first character alphanumeric). Existing identities cannot be renamed by this endpoint.Details
Display name (1–80 characters) and ticker (1–16 letters/digits/dots/underscores/hyphens, first character alphanumeric). Existing identities cannot be renamed by this endpoint.
symbolstringrequiredDisplay name (1–80 characters) and ticker (1–16 letters/digits/dots/underscores/hyphens, first character alphanumeric). Existing identities cannot be renamed by this endpoint.Details
Display name (1–80 characters) and ticker (1–16 letters/digits/dots/underscores/hyphens, first character alphanumeric). Existing identities cannot be renamed by this endpoint.
price_modefixed | dexoptionalDefaults to fixed for backwards compatibility. DEX uses a specific pool discovered for the exact chain and contract.
price_usddecimal stringfixed modeFixed USD value of ONE token, positive, at most 30 decimals, maximum 1000000000000000000000000. No exponent or floats. Omit in dex mode.
dex_pair_addressstringdex modePool address from payment-token-dex-pools. Required in dex mode; omit in fixed mode. The server rechecks pool identity, price, liquidity and activity on every save.Details
Pool address from payment-token-dex-pools. Required in dex mode; omit in fixed mode. The server rechecks pool identity, price, liquidity and activity on every save.
Response 200 application/json
Example response
{"data":{"asset_id":"44444444-4444-4444-8444-444444444444"}}Response fields
Usage notes
- After registration, select asset_id in the store's payment-assets endpoint; registration alone never enables a store method.
- Custom and catalog tokens share the 20-token project limit. The same contract on different chains is a different payment asset.
- Existing catalog contracts return 409: use catalog registration to retain automatic market rates. A custom ticker never borrows a namesake token's price.
- Fixed prices are operator estimates. Automatic DEX prices are spot observations from the selected pool via DEX Screener, not a manipulation-resistant oracle. The store spread and upward rounding still apply, with fresh fiat rates. Already-issued quotes are unchanged.
- For DEX mode first discover a pool, then send price_mode: dex and dex_pair_address, omitting price_usd. A shared background task refreshes selected pools every minute. Failed checks or prices older than five minutes remove this token from new quotes; there is no silent fixed-price or ticker fallback.
- Only standard ERC-20 and classic SPL tokens are accepted. Token-2022/extensions and native-only chains are rejected. Technical verification is not an issuer/contract security audit; fee-on-transfer, rebasing or blacklisting tokens can behave incompatibly.
- Use a client timeout of at least 60 seconds. Verification is bounded and can try node fallbacks. Invalid input returns 400; failed chain/contract checks return 422; identity conflicts or limits return 409.
Errors
GETList store payment methods/v1/projects/{project_id}/stores/{store_id}/payment-assetsRead only
Lists on-chain assets in data and separate Lightning readiness in lightning. On-chain methods require ready chain wallets. Lightning uses the verified external receiving connection selected in the store, independently of the on-chain Bitcoin wallet.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | recommended | application/json |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [
{ "asset": { "id": "ASSET_UUID", "chain_slug": "ethereum", "symbol": "USDC", "asset_kind": "token", "token_standard": "erc20", "scanner_ready": true }, "project_policy": { "enabled": true, "required_confirmations": 12 }, "selected": true, "display_order": 0, "confirmation_policy": { "finality_mode": "confirmations", "project_required_confirmations": 12, "override_required_confirmations": 3, "effective_required_confirmations": 3, "editable": true, "minimum_required_confirmations": 0, "maximum_required_confirmations": 48 }, "wallet": { "id": "WALLET_UUID", "status": "active" }, "wallet_readiness": "ready" }
],
"lightning": { "payment_rail": "lightning", "symbol": "BTC", "asset_decimals": 11, "enabled": true, "ready": true }
}Response fields
data[]object[]
assetobjectalways
Project-visible native or verified token asset.
asset_kindnative | tokenalwaysWhether settlement uses the chain currency or a verified contract/mint.
payment_railstringalwaysRuntime rail: utxo, evm-native, solana-native, account-native, privacy-native, or token-transfer.
contract_addressstring | nullalwaysCanonical ERC-20 contract or SPL mint for tokens; null for native assets.
coingecko_idstring | nullalwaysDiscovery/pricing identity. Null for custom contracts; never infer a market price from their ticker. CoinGecko metadata alone never makes a token selectable.
custom_tokenbooleanalwaysCustom on-chain-verified contract with project-scoped fixed USD or selected DEX pool pricing.
token_standarderc20 | spl-token | nullalwaysVerified runtime token standard; null for native assets.
payment_supportedbooleanalwaysBuild-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.Details
Build-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.
scanner_readybooleanalwaysBuild-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.Details
Build-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.
balance_readybooleanalwaysBuild-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.Details
Build-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.
default_finality_modeconfirmations | finalizedalwaysDefault finality model inherited by a new project policy.
project_policyobject | nullalways
Parent project policy.
enabledbooleanrequiredEnables or disables the asset for the project. The native chain must be enabled before any token.
finality_modeconfirmations | finalizedrequiredFinality policy supported by the asset rail. finalized requires required_confirmations=1.
required_confirmationsintegerrequiredBitcoin and EVM rails accept zero; other confirmation rails require at least one, finalized-only rails require exactly one, and EVM rails are bounded to 0–48 so every transfer remains inside the transaction replay window.Details
Bitcoin and EVM rails accept zero; other confirmation rails require at least one, finalized-only rails require exactly one, and EVM rails are bounded to 0–48 so every transfer remains inside the transaction replay window.
selectedbooleanalwaysWhether this method is part of the store's saved desired configuration. It is offered when its project policy, wallet, installed adapter and pricing are valid. Temporary scanner outages do not remove it from new invoices.Details
Whether this method is part of the store's saved desired configuration. It is offered when its project policy, wallet, installed adapter and pricing are valid. Temporary scanner outages do not remove it from new invoices.
confirmation_policyobject | nullalways
Effective store policy for a project-configured asset. Null when no project policy exists.
finality_modeconfirmations | finalizedalwaysWhether settlement uses a configurable block count or network finality.
project_required_confirmationsintegeralwaysCurrent project default used by future invoices when no store override is set.
override_required_confirmationsinteger | nullalwaysStore-specific count, or null to inherit the project default.
effective_required_confirmationsintegeralwaysCount that new invoices for this store and asset will snapshot.
minimum_required_confirmationsintegeralwaysInclusive chain-aware lower bound; 0 is exposed only on rails that support detection-time acceptance.
walletobject | nullalways
Chain wallet shared by native and token assets.
primary_addressstring | nullalwaysPublic wallet identity; no seed phrase or private key is exposed.
activation_requiredbooleanalwaysXRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.Details
XRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.
activation_verified_attimestamp|nullalwaysXRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.Details
XRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.
receive_readinessobject | null5.5.0+
Included on wallet listings: project receiving setup and chain scanner prerequisites. Separate from balances, token gas and send readiness. Other wallet responses may leave it null.Details
Included on wallet listings: project receiving setup and chain scanner prerequisites. Separate from balances, token gas and send readiness. Other wallet responses may leave it null.
readybooleanalwaysReceive setup checks pass. Does not describe spending readiness, gas, balance refresh, or a guaranteed future quote.
invoice_creatableboolean6.0.6+Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.Details
Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.
checked_attimestampalwaysAssessment time. No network request or address allocation is made by a listing.
issues[]object[]always
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.Details
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.
reason_codestringalwaysscanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.Details
scanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.
messagestringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
actionstringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
required_endpoint_rolestring | nullon-chainPreferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.Details
Preferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.
accepted_endpoint_roles[]string[] | nullon-chainCompatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.Details
Compatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.
usable_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
required_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
last_checked_attimestamp | nullon-chainLatest matching endpoint health check, separate from assessment time.
monero_wallet_rpcMoneroWalletRpcBinding | nullalwaysSanitized external view-only wallet-RPC binding state for Monero. Includes endpoint, authentication mode, account-0 primary address, technical proof flags/heights, and operator attestation timestamps; credentials, wallet keys, and wallet files are never serialized.Details
Sanitized external view-only wallet-RPC binding state for Monero. Includes endpoint, authentication mode, account-0 primary address, technical proof flags/heights, and operator attestation timestamps; credentials, wallet keys, and wallet files are never serialized.
balances[]object[]always
Cached balances for every one of the 30 native chain rails, plus verified ERC-20 and SPL assets. A configured external view-only wallet-RPC is required for Monero.Details
Cached balances for every one of the 30 native chain rails, plus verified ERC-20 and SPL assets. A configured external view-only wallet-RPC is required for Monero.
active_store_countintegeralwaysNumber of enabled stores that currently select this asset. This is an acceptance projection; read-only balance tracking remains independent.
active_store_ids[]UUID[]alwaysEnabled stores in this project that currently accept the asset. This permits exact local store filtering without another API request.
tracking_activebooleanalwaysWhether this read-capable wallet and registered same-chain asset are eligible for background balance refreshes. Project and payment-method acceptance switches do not pause read-only tracking.Details
Whether this read-capable wallet and registered same-chain asset are eligible for background balance refreshes. Project and payment-method acceptance switches do not pause read-only tracking.
balancedecimal string|nullalwaysExact display and atomic balance across the wallet primary address and issued invoice addresses. Null while a complete value is unavailable.
balance_atomicinteger string|nullalwaysExact display and atomic balance across the wallet primary address and issued invoice addresses. Null while a complete value is unavailable.
statuspending | refreshing | fresh | stale | erroralwaysCached scan state. refreshing can retain a completed balance: use checked_at for its age. Pending means no completed snapshot. None of these states proves a transfer is pending or an invoice settled.Details
Cached scan state. refreshing can retain a completed balance: use checked_at for its age. Pending means no completed snapshot. None of these states proves a transfer is pending or an invoice settled.
balance_statuspending | refreshing | fresh | stale | error | unknownalwaysAggregated cache freshness; unknown is a defensive fallback and none of these states proves invoice settlement.
balance_checked_attimestamp | nullalwaysOldest relevant successful balance check represented by the aggregate.
recent_payments[]object[]always
Up to three newest valid detected, confirming, or final observations attributed to this exact wallet.
statusdetected | confirming | finalalwaysCurrent valid observation state. Reorged, replaced, and invalid observations are excluded.
wallet_readinessreadiness enumalwaysWallet/policy status only; use receive_readiness for scanner prerequisites.
receive_readinessobject | null5.5.0+
Shared receive setup plus store acceptance. Uses cached observations; not a reservation or guarantee. Creation rechecks requirements and the actual invoice exchange rate.Details
Shared receive setup plus store acceptance. Uses cached observations; not a reservation or guarantee. Creation rechecks requirements and the actual invoice exchange rate.
readybooleanalwaysReceive setup checks pass. Does not describe spending readiness, gas, balance refresh, or a guaranteed future quote.
invoice_creatableboolean6.0.6+Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.Details
Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.
checked_attimestampalwaysAssessment time. No network request or address allocation is made by a listing.
issues[]object[]always
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.Details
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.
reason_codestringalwaysscanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.Details
scanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.
messagestringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
actionstringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
required_endpoint_rolestring | nullon-chainPreferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.Details
Preferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.
accepted_endpoint_roles[]string[] | nullon-chainCompatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.Details
Compatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.
usable_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
required_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
last_checked_attimestamp | nullon-chainLatest matching endpoint health check, separate from assessment time.
Usage notes
- selected is on-chain configuration; wallet_readiness is its live eligibility gate.
- The lightning response member contains payment_rail: lightning, symbol: BTC, asset_decimals: 11, enabled and ready. It never contains node credentials. Configure this method in the store's console; updating the assets array does not change Lightning.
- confirmation_policy applies only to on-chain methods. Lightning settles without block confirmations and requires its full BOLT11 amount, without partial-payment tolerance.
- Native and token methods on one chain use the same invoice destination for that chain wallet.
- Embedded wallet summaries are readiness-only and keep balances empty; use the dedicated project wallets route for current values.
Errors
PUTReplace store payment methods/v1/projects/{project_id}/stores/{store_id}/payment-assetsRead + write
Atomically replaces the store's complete ordered asset subset and returns the refreshed list. Omitted assets are deselected.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Content-Type | required | application/json |
| Accept | recommended | application/json |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request PUT \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Content-Type: application/json' \
--data-raw '{
"assets": [
{
"asset_id": "YOUR_ASSET_ID",
"display_order": 0
}
]
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const body = `{
"assets": [
{
"asset_id": "YOUR_ASSET_ID",
"display_order": 0
}
]
}`;
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets", {
method: "PUT",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$body = <<<'JSON'
{
"assets": [
{
"asset_id": "YOUR_ASSET_ID",
"display_order": 0
}
]
}
JSON;
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"assets": [
{
"asset_id": "YOUR_ASSET_ID",
"display_order": 0
}
]
}""".encode("utf-8")
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets",
method="PUT", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": [
{ "asset": { "id": "44444444-4444-4444-8444-444444444444", "symbol": "USDC" }, "selected": true, "display_order": 0, "confirmation_policy": { "finality_mode": "confirmations", "project_required_confirmations": 12, "override_required_confirmations": null, "effective_required_confirmations": 12, "editable": true, "minimum_required_confirmations": 0, "maximum_required_confirmations": 48 }, "wallet_readiness": "ready" }
]
}Response fields
data[]object[]
assetobjectalways
Project-visible native or verified token asset.
asset_kindnative | tokenalwaysWhether settlement uses the chain currency or a verified contract/mint.
payment_railstringalwaysRuntime rail: utxo, evm-native, solana-native, account-native, privacy-native, or token-transfer.
contract_addressstring | nullalwaysCanonical ERC-20 contract or SPL mint for tokens; null for native assets.
coingecko_idstring | nullalwaysDiscovery/pricing identity. Null for custom contracts; never infer a market price from their ticker. CoinGecko metadata alone never makes a token selectable.
custom_tokenbooleanalwaysCustom on-chain-verified contract with project-scoped fixed USD or selected DEX pool pricing.
token_standarderc20 | spl-token | nullalwaysVerified runtime token standard; null for native assets.
payment_supportedbooleanalwaysBuild-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.Details
Build-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.
scanner_readybooleanalwaysBuild-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.Details
Build-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.
balance_readybooleanalwaysBuild-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.Details
Build-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.
default_finality_modeconfirmations | finalizedalwaysDefault finality model inherited by a new project policy.
project_policyobject | nullalways
Parent project policy.
enabledbooleanrequiredEnables or disables the asset for the project. The native chain must be enabled before any token.
finality_modeconfirmations | finalizedrequiredFinality policy supported by the asset rail. finalized requires required_confirmations=1.
required_confirmationsintegerrequiredBitcoin and EVM rails accept zero; other confirmation rails require at least one, finalized-only rails require exactly one, and EVM rails are bounded to 0–48 so every transfer remains inside the transaction replay window.Details
Bitcoin and EVM rails accept zero; other confirmation rails require at least one, finalized-only rails require exactly one, and EVM rails are bounded to 0–48 so every transfer remains inside the transaction replay window.
selectedbooleanalwaysWhether this method is part of the store's saved desired configuration. It is offered when its project policy, wallet, installed adapter and pricing are valid. Temporary scanner outages do not remove it from new invoices.Details
Whether this method is part of the store's saved desired configuration. It is offered when its project policy, wallet, installed adapter and pricing are valid. Temporary scanner outages do not remove it from new invoices.
confirmation_policyobject | nullalways
Effective store policy for a project-configured asset. Null when no project policy exists.
finality_modeconfirmations | finalizedalwaysWhether settlement uses a configurable block count or network finality.
project_required_confirmationsintegeralwaysCurrent project default used by future invoices when no store override is set.
override_required_confirmationsinteger | nullalwaysStore-specific count, or null to inherit the project default.
effective_required_confirmationsintegeralwaysCount that new invoices for this store and asset will snapshot.
minimum_required_confirmationsintegeralwaysInclusive chain-aware lower bound; 0 is exposed only on rails that support detection-time acceptance.
walletobject | nullalways
Chain wallet shared by native and token assets.
primary_addressstring | nullalwaysPublic wallet identity; no seed phrase or private key is exposed.
activation_requiredbooleanalwaysXRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.Details
XRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.
activation_verified_attimestamp|nullalwaysXRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.Details
XRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.
receive_readinessobject | null5.5.0+
Included on wallet listings: project receiving setup and chain scanner prerequisites. Separate from balances, token gas and send readiness. Other wallet responses may leave it null.Details
Included on wallet listings: project receiving setup and chain scanner prerequisites. Separate from balances, token gas and send readiness. Other wallet responses may leave it null.
readybooleanalwaysReceive setup checks pass. Does not describe spending readiness, gas, balance refresh, or a guaranteed future quote.
invoice_creatableboolean6.0.6+Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.Details
Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.
checked_attimestampalwaysAssessment time. No network request or address allocation is made by a listing.
issues[]object[]always
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.Details
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.
reason_codestringalwaysscanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.Details
scanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.
messagestringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
actionstringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
required_endpoint_rolestring | nullon-chainPreferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.Details
Preferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.
accepted_endpoint_roles[]string[] | nullon-chainCompatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.Details
Compatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.
usable_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
required_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
last_checked_attimestamp | nullon-chainLatest matching endpoint health check, separate from assessment time.
monero_wallet_rpcMoneroWalletRpcBinding | nullalwaysSanitized external view-only wallet-RPC binding state for Monero. Includes endpoint, authentication mode, account-0 primary address, technical proof flags/heights, and operator attestation timestamps; credentials, wallet keys, and wallet files are never serialized.Details
Sanitized external view-only wallet-RPC binding state for Monero. Includes endpoint, authentication mode, account-0 primary address, technical proof flags/heights, and operator attestation timestamps; credentials, wallet keys, and wallet files are never serialized.
balances[]object[]always
Cached balances for every one of the 30 native chain rails, plus verified ERC-20 and SPL assets. A configured external view-only wallet-RPC is required for Monero.Details
Cached balances for every one of the 30 native chain rails, plus verified ERC-20 and SPL assets. A configured external view-only wallet-RPC is required for Monero.
active_store_countintegeralwaysNumber of enabled stores that currently select this asset. This is an acceptance projection; read-only balance tracking remains independent.
active_store_ids[]UUID[]alwaysEnabled stores in this project that currently accept the asset. This permits exact local store filtering without another API request.
tracking_activebooleanalwaysWhether this read-capable wallet and registered same-chain asset are eligible for background balance refreshes. Project and payment-method acceptance switches do not pause read-only tracking.Details
Whether this read-capable wallet and registered same-chain asset are eligible for background balance refreshes. Project and payment-method acceptance switches do not pause read-only tracking.
balancedecimal string|nullalwaysExact display and atomic balance across the wallet primary address and issued invoice addresses. Null while a complete value is unavailable.
balance_atomicinteger string|nullalwaysExact display and atomic balance across the wallet primary address and issued invoice addresses. Null while a complete value is unavailable.
statuspending | refreshing | fresh | stale | erroralwaysCached scan state. refreshing can retain a completed balance: use checked_at for its age. Pending means no completed snapshot. None of these states proves a transfer is pending or an invoice settled.Details
Cached scan state. refreshing can retain a completed balance: use checked_at for its age. Pending means no completed snapshot. None of these states proves a transfer is pending or an invoice settled.
balance_statuspending | refreshing | fresh | stale | error | unknownalwaysAggregated cache freshness; unknown is a defensive fallback and none of these states proves invoice settlement.
balance_checked_attimestamp | nullalwaysOldest relevant successful balance check represented by the aggregate.
recent_payments[]object[]always
Up to three newest valid detected, confirming, or final observations attributed to this exact wallet.
statusdetected | confirming | finalalwaysCurrent valid observation state. Reorged, replaced, and invalid observations are excluded.
wallet_readinessreadiness enumalwaysWallet/policy status only; use receive_readiness for scanner prerequisites.
receive_readinessobject | null5.5.0+
Shared receive setup plus store acceptance. Uses cached observations; not a reservation or guarantee. Creation rechecks requirements and the actual invoice exchange rate.Details
Shared receive setup plus store acceptance. Uses cached observations; not a reservation or guarantee. Creation rechecks requirements and the actual invoice exchange rate.
readybooleanalwaysReceive setup checks pass. Does not describe spending readiness, gas, balance refresh, or a guaranteed future quote.
invoice_creatableboolean6.0.6+Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.Details
Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.
checked_attimestampalwaysAssessment time. No network request or address allocation is made by a listing.
issues[]object[]always
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.Details
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.
reason_codestringalwaysscanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.Details
scanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.
messagestringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
actionstringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
required_endpoint_rolestring | nullon-chainPreferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.Details
Preferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.
accepted_endpoint_roles[]string[] | nullon-chainCompatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.Details
Compatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.
usable_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
required_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
last_checked_attimestamp | nullon-chainLatest matching endpoint health check, separate from assessment time.
Usage notes
- The array accepts at most 64 unique assets and display orders.
- Selections are saved desired configuration and may be staged before a wallet is backed up or while a chain is paused. Invoice creation still offers only methods whose project policy, native parent policy, wallet, and runtime checks are ready.
- Send an empty assets array to configure no payment methods.
Errors
PUTSet a store confirmation policy/v1/projects/{project_id}/stores/{store_id}/payment-assets/{asset_id}/confirmation-policyRead + write
Sets or clears one store-specific confirmation override and returns the refreshed store payment-method list. The asset must already be selected for the store. Configuration remains available while the project, store, chain, or wallet is paused.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Content-Type | required | application/json |
| Accept | recommended | application/json |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request PUT \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets/YOUR_ASSET_ID/confirmation-policy" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Content-Type: application/json' \
--data-raw '{
"strategy": "custom",
"required_confirmations": 0
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const body = `{
"strategy": "custom",
"required_confirmations": 0
}`;
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets/YOUR_ASSET_ID/confirmation-policy", {
method: "PUT",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$body = <<<'JSON'
{
"strategy": "custom",
"required_confirmations": 0
}
JSON;
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets/YOUR_ASSET_ID/confirmation-policy");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"strategy": "custom",
"required_confirmations": 0
}""".encode("utf-8")
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets/YOUR_ASSET_ID/confirmation-policy",
method="PUT", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
Response 200 application/json
Example response
{
"data": [
{
"asset": { "id": "YOUR_ASSET_ID", "chain_slug": "bitcoin", "symbol": "BTC" },
"selected": true,
"display_order": 0,
"confirmation_policy": {
"finality_mode": "confirmations",
"project_required_confirmations": 2,
"override_required_confirmations": 0,
"effective_required_confirmations": 0,
"editable": true,
"minimum_required_confirmations": 0,
"maximum_required_confirmations": 10000
},
"wallet_readiness": "ready"
}
]
}Response fields
data[]object[]
assetobjectalways
Project-visible native or verified token asset.
asset_kindnative | tokenalwaysWhether settlement uses the chain currency or a verified contract/mint.
payment_railstringalwaysRuntime rail: utxo, evm-native, solana-native, account-native, privacy-native, or token-transfer.
contract_addressstring | nullalwaysCanonical ERC-20 contract or SPL mint for tokens; null for native assets.
coingecko_idstring | nullalwaysDiscovery/pricing identity. Null for custom contracts; never infer a market price from their ticker. CoinGecko metadata alone never makes a token selectable.
custom_tokenbooleanalwaysCustom on-chain-verified contract with project-scoped fixed USD or selected DEX pool pricing.
token_standarderc20 | spl-token | nullalwaysVerified runtime token standard; null for native assets.
payment_supportedbooleanalwaysBuild-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.Details
Build-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.
scanner_readybooleanalwaysBuild-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.Details
Build-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.
balance_readybooleanalwaysBuild-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.Details
Build-time registry gates. scanner_ready means the payment scanner runtime is installed; payment confirmation needs the configured number of healthy exact-role providers (2 by default, optional 1); temporary scanner availability does not block invoice creation from 6.0.6. balance_ready is true only for implemented balance adapters.
default_finality_modeconfirmations | finalizedalwaysDefault finality model inherited by a new project policy.
project_policyobject | nullalways
Parent project policy.
enabledbooleanrequiredEnables or disables the asset for the project. The native chain must be enabled before any token.
finality_modeconfirmations | finalizedrequiredFinality policy supported by the asset rail. finalized requires required_confirmations=1.
required_confirmationsintegerrequiredBitcoin and EVM rails accept zero; other confirmation rails require at least one, finalized-only rails require exactly one, and EVM rails are bounded to 0–48 so every transfer remains inside the transaction replay window.Details
Bitcoin and EVM rails accept zero; other confirmation rails require at least one, finalized-only rails require exactly one, and EVM rails are bounded to 0–48 so every transfer remains inside the transaction replay window.
selectedbooleanalwaysWhether this method is part of the store's saved desired configuration. It is offered when its project policy, wallet, installed adapter and pricing are valid. Temporary scanner outages do not remove it from new invoices.Details
Whether this method is part of the store's saved desired configuration. It is offered when its project policy, wallet, installed adapter and pricing are valid. Temporary scanner outages do not remove it from new invoices.
confirmation_policyobject | nullalways
Effective store policy for a project-configured asset. Null when no project policy exists.
finality_modeconfirmations | finalizedalwaysWhether settlement uses a configurable block count or network finality.
project_required_confirmationsintegeralwaysCurrent project default used by future invoices when no store override is set.
override_required_confirmationsinteger | nullalwaysStore-specific count, or null to inherit the project default.
effective_required_confirmationsintegeralwaysCount that new invoices for this store and asset will snapshot.
minimum_required_confirmationsintegeralwaysInclusive chain-aware lower bound; 0 is exposed only on rails that support detection-time acceptance.
walletobject | nullalways
Chain wallet shared by native and token assets.
primary_addressstring | nullalwaysPublic wallet identity; no seed phrase or private key is exposed.
activation_requiredbooleanalwaysXRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.Details
XRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.
activation_verified_attimestamp|nullalwaysXRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.Details
XRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.
receive_readinessobject | null5.5.0+
Included on wallet listings: project receiving setup and chain scanner prerequisites. Separate from balances, token gas and send readiness. Other wallet responses may leave it null.Details
Included on wallet listings: project receiving setup and chain scanner prerequisites. Separate from balances, token gas and send readiness. Other wallet responses may leave it null.
readybooleanalwaysReceive setup checks pass. Does not describe spending readiness, gas, balance refresh, or a guaranteed future quote.
invoice_creatableboolean6.0.6+Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.Details
Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.
checked_attimestampalwaysAssessment time. No network request or address allocation is made by a listing.
issues[]object[]always
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.Details
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.
reason_codestringalwaysscanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.Details
scanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.
messagestringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
actionstringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
required_endpoint_rolestring | nullon-chainPreferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.Details
Preferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.
accepted_endpoint_roles[]string[] | nullon-chainCompatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.Details
Compatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.
usable_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
required_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
last_checked_attimestamp | nullon-chainLatest matching endpoint health check, separate from assessment time.
monero_wallet_rpcMoneroWalletRpcBinding | nullalwaysSanitized external view-only wallet-RPC binding state for Monero. Includes endpoint, authentication mode, account-0 primary address, technical proof flags/heights, and operator attestation timestamps; credentials, wallet keys, and wallet files are never serialized.Details
Sanitized external view-only wallet-RPC binding state for Monero. Includes endpoint, authentication mode, account-0 primary address, technical proof flags/heights, and operator attestation timestamps; credentials, wallet keys, and wallet files are never serialized.
balances[]object[]always
Cached balances for every one of the 30 native chain rails, plus verified ERC-20 and SPL assets. A configured external view-only wallet-RPC is required for Monero.Details
Cached balances for every one of the 30 native chain rails, plus verified ERC-20 and SPL assets. A configured external view-only wallet-RPC is required for Monero.
active_store_countintegeralwaysNumber of enabled stores that currently select this asset. This is an acceptance projection; read-only balance tracking remains independent.
active_store_ids[]UUID[]alwaysEnabled stores in this project that currently accept the asset. This permits exact local store filtering without another API request.
tracking_activebooleanalwaysWhether this read-capable wallet and registered same-chain asset are eligible for background balance refreshes. Project and payment-method acceptance switches do not pause read-only tracking.Details
Whether this read-capable wallet and registered same-chain asset are eligible for background balance refreshes. Project and payment-method acceptance switches do not pause read-only tracking.
balancedecimal string|nullalwaysExact display and atomic balance across the wallet primary address and issued invoice addresses. Null while a complete value is unavailable.
balance_atomicinteger string|nullalwaysExact display and atomic balance across the wallet primary address and issued invoice addresses. Null while a complete value is unavailable.
statuspending | refreshing | fresh | stale | erroralwaysCached scan state. refreshing can retain a completed balance: use checked_at for its age. Pending means no completed snapshot. None of these states proves a transfer is pending or an invoice settled.Details
Cached scan state. refreshing can retain a completed balance: use checked_at for its age. Pending means no completed snapshot. None of these states proves a transfer is pending or an invoice settled.
balance_statuspending | refreshing | fresh | stale | error | unknownalwaysAggregated cache freshness; unknown is a defensive fallback and none of these states proves invoice settlement.
balance_checked_attimestamp | nullalwaysOldest relevant successful balance check represented by the aggregate.
recent_payments[]object[]always
Up to three newest valid detected, confirming, or final observations attributed to this exact wallet.
statusdetected | confirming | finalalwaysCurrent valid observation state. Reorged, replaced, and invalid observations are excluded.
wallet_readinessreadiness enumalwaysWallet/policy status only; use receive_readiness for scanner prerequisites.
receive_readinessobject | null5.5.0+
Shared receive setup plus store acceptance. Uses cached observations; not a reservation or guarantee. Creation rechecks requirements and the actual invoice exchange rate.Details
Shared receive setup plus store acceptance. Uses cached observations; not a reservation or guarantee. Creation rechecks requirements and the actual invoice exchange rate.
readybooleanalwaysReceive setup checks pass. Does not describe spending readiness, gas, balance refresh, or a guaranteed future quote.
invoice_creatableboolean6.0.6+Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.Details
Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.
checked_attimestampalwaysAssessment time. No network request or address allocation is made by a listing.
issues[]object[]always
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.Details
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.
reason_codestringalwaysscanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.Details
scanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.
messagestringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
actionstringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
required_endpoint_rolestring | nullon-chainPreferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.Details
Preferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.
accepted_endpoint_roles[]string[] | nullon-chainCompatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.Details
Compatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.
usable_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
required_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
last_checked_attimestamp | nullon-chainLatest matching endpoint health check, separate from assessment time.
Usage notes
- Use {"strategy":"inherit"} to remove the store override and follow the current project default for future invoices.
- Finalized networks return editable false and use Network finality; they do not accept a custom block-count override.
- A value of 0 means accept on detection with no network confirmation and no reorg protection. It is accepted only where minimum_required_confirmations is 0.
- Policy changes affect only new invoices. Existing invoices retain the project/store confirmation policy snapshot captured at creation.
- Updates are one asset at a time; serialize concurrent edits for the same store asset and use the refreshed response as current state.
Errors
GETList project wallets and balances/v1/projects/{project_id}/walletsRead only
Returns public wallet metadata plus every registered balance-capable asset on the wallet's exact chain and network. All 30 native chain rails are covered; verified ERC-20 and SPL assets are also tracked. Assets appear immediately, even before their first scan or when they are not accepted for payments. project_enabled reports payment acceptance; tracking_active independently reports read-only refresh eligibility. Monero requires its project-bound external view-only wallet-RPC. Console Scan balances prioritizes bounded reads with per-asset progress/errors; only complete cycles update fresh totals. Invoice settlement remains driven by transaction-level monitoring and confirmation policy, not these cached balances.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | recommended | application/json |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/wallets" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/wallets", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/wallets");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/wallets",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [
{
"id": "55555555-5555-4555-8555-555555555555",
"project_id": "11111111-1111-4111-8111-111111111111",
"native_asset_id": "10000000-0000-4000-8000-000000000003",
"chain_slug": "ethereum",
"network": "mainnet",
"asset_symbol": "ETH",
"asset_name": "Ethereum",
"status": "active",
"label": "Primary Ethereum wallet",
"public_key": "0x…",
"primary_address": "0x…",
"derivation_scheme": "bip44",
"address_format": "eip55",
"backup_confirmed_at": "2026-08-31T17:00:00Z",
"last_secret_revealed_at": null,
"secret_reveal_count": 0,
"next_receive_index": 43,
"last_scanned_height": 23123456,
"last_scanned_at": "2026-08-31T18:05:00Z",
"last_error": null,
"balances": [
{ "wallet_id": "55555555-5555-4555-8555-555555555555", "asset_id": "10000000-0000-4000-8000-000000000003", "project_enabled": true, "tracking_active": true, "asset_kind": "native", "contract_address": null, "symbol": "ETH", "name": "Ethereum", "decimals": 18, "coingecko_id": "ethereum", "balance": "0.125", "balance_atomic": "125000000000000000", "price_usd": "4500", "value_usd": "562.50", "status": "fresh", "checked_at": "2026-08-31T18:05:00Z", "last_error": null },
{ "wallet_id": "55555555-5555-4555-8555-555555555555", "asset_id": "10000000-0000-4000-8000-000000000099", "project_enabled": false, "tracking_active": true, "asset_kind": "token", "contract_address": "0xA0b86991c6218b36c1d19d4a2e9eb0cE3606eB48", "symbol": "USDC", "name": "USDC", "decimals": 6, "coingecko_id": "usd-coin", "balance": null, "balance_atomic": null, "price_usd": "1", "value_usd": null, "status": "pending", "checked_at": null, "last_error": null }
],
"total_value_usd": "562.50",
"balance_status": "fresh",
"balance_checked_at": "2026-08-31T18:05:00Z",
"recent_payments": [
{ "invoice_public_id": "0a6a98db-d93d-48ee-8c3c-fd45f90c4a50", "chain_slug": "ethereum", "symbol": "USDC", "transaction_id": "0x…", "event_index": 0, "amount": "25", "status": "final", "confirmations": 12, "observed_at": "2026-08-31T18:04:00Z" }
],
"created_at": "2026-08-31T16:00:00Z",
"updated_at": "2026-08-31T18:05:00Z"
}
]
}Response fields
data[]object[]
primary_addressstring | nullalwaysPublic wallet identity; no seed phrase or private key is exposed.
activation_requiredbooleanalwaysXRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.Details
XRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.
activation_verified_attimestamp|nullalwaysXRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.Details
XRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and the configured scanner providers verify that exact account. The durable proof does not expire; live scanner health is checked separately for payment verification, not invoice creation.
receive_readinessobject | null5.5.0+
Included on wallet listings: project receiving setup and chain scanner prerequisites. Separate from balances, token gas and send readiness. Other wallet responses may leave it null.Details
Included on wallet listings: project receiving setup and chain scanner prerequisites. Separate from balances, token gas and send readiness. Other wallet responses may leave it null.
readybooleanalwaysReceive setup checks pass. Does not describe spending readiness, gas, balance refresh, or a guaranteed future quote.
invoice_creatableboolean6.0.6+Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.Details
Configuration permits an invoice method despite temporary scanner warnings. Currency pricing is checked at creation. This is not payment verification: ready can be false while invoice_creatable is true. Missing wallets, disabled policy and unsupported adapters still fail safely.
checked_attimestampalwaysAssessment time. No network request or address allocation is made by a listing.
issues[]object[]always
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.Details
Empty when ready; otherwise a receive warning or configuration blocker. Check invoice_creatable to distinguish temporary scanner warnings from invoice setup failures.
reason_codestringalwaysscanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.Details
scanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled, or asset_not_accepted.
messagestringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
actionstringwhen availableMerchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.Details
Merchant-facing explanation and action identifier: chain_connections, wallets, rates, payment_methods, project_settings or store_settings. No credentials or private provider URLs.
required_endpoint_rolestring | nullon-chainPreferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.Details
Preferred scanner API role (legacy field). Use accepted_endpoint_roles for the complete compatibility list. Basic node health does not prove payment-history support.
accepted_endpoint_roles[]string[] | nullon-chainCompatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.Details
Compatible API dialects, not proof of endpoint history or capacity. Raw node-rpc supports BTC/BCH/LTC/DOGE/DASH and transparent ZEC (complete decoded blocks, 1–48 confirmations), solidified native TRX, native ALGO via algod, XTZ via Octez, finalized Asset Hub DOT via SCALE metadata and native XLM via Stellar RPC with invoice memo ID. Pruned or incomplete history does not qualify. These raw adapters do not add token rails. Indexed APIs remain alternatives; see the rail table below. Mixed raw/indexed sources verify bounded windows independently; the default remains two independent providers, not aliases of one operator. Basic node height, ORDnet chain info and an EVM relay for a non-EVM rail are not receive proofs. Monero still needs a project-bound view-only wallet-RPC.
usable_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
required_independent_providersintegeron-chainUsable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.Details
Usable verification slots, capped at two. required_independent_providers is the chain setting: 2 by default, or 1 after explicit administrator opt-in. In two-provider mode, different provider keys AND hosts are required. Disabled, stale (over ten minutes), or cooling-down sources do not fill a slot. Lightning uses its own connection rules.
last_checked_attimestamp | nullon-chainLatest matching endpoint health check, separate from assessment time.
monero_wallet_rpcMoneroWalletRpcBinding | nullalwaysSanitized external view-only wallet-RPC binding state for Monero. Includes endpoint, authentication mode, account-0 primary address, technical proof flags/heights, and operator attestation timestamps; credentials, wallet keys, and wallet files are never serialized.Details
Sanitized external view-only wallet-RPC binding state for Monero. Includes endpoint, authentication mode, account-0 primary address, technical proof flags/heights, and operator attestation timestamps; credentials, wallet keys, and wallet files are never serialized.
balances[]object[]always
Cached balances for every one of the 30 native chain rails, plus verified ERC-20 and SPL assets. A configured external view-only wallet-RPC is required for Monero.Details
Cached balances for every one of the 30 native chain rails, plus verified ERC-20 and SPL assets. A configured external view-only wallet-RPC is required for Monero.
active_store_countintegeralwaysNumber of enabled stores that currently select this asset. This is an acceptance projection; read-only balance tracking remains independent.
active_store_ids[]UUID[]alwaysEnabled stores in this project that currently accept the asset. This permits exact local store filtering without another API request.
tracking_activebooleanalwaysWhether this read-capable wallet and registered same-chain asset are eligible for background balance refreshes. Project and payment-method acceptance switches do not pause read-only tracking.Details
Whether this read-capable wallet and registered same-chain asset are eligible for background balance refreshes. Project and payment-method acceptance switches do not pause read-only tracking.
balancedecimal string|nullalwaysExact display and atomic balance across the wallet primary address and issued invoice addresses. Null while a complete value is unavailable.
balance_atomicinteger string|nullalwaysExact display and atomic balance across the wallet primary address and issued invoice addresses. Null while a complete value is unavailable.
statuspending | refreshing | fresh | stale | erroralwaysCached scan state. refreshing can retain a completed balance: use checked_at for its age. Pending means no completed snapshot. None of these states proves a transfer is pending or an invoice settled.Details
Cached scan state. refreshing can retain a completed balance: use checked_at for its age. Pending means no completed snapshot. None of these states proves a transfer is pending or an invoice settled.
balance_statuspending | refreshing | fresh | stale | error | unknownalwaysAggregated cache freshness; unknown is a defensive fallback and none of these states proves invoice settlement.
balance_checked_attimestamp | nullalwaysOldest relevant successful balance check represented by the aggregate.
recent_payments[]object[]always
Up to three newest valid detected, confirming, or final observations attributed to this exact wallet.
statusdetected | confirming | finalalwaysCurrent valid observation state. Reorged, replaced, and invalid observations are excluded.
Usage notes
- This bearer route never returns a recovery phrase, private key, encrypted secret, or spending method.
- A newly registered same-chain asset is returned with null balances and pending status before its first completed scan; it is never reported as a fabricated zero.
- Disabling a project, wallet for payment acceptance, native rail, or individual asset does not stop read-only balance tracking: active and disabled wallets with a primary address continue refreshing every registered, supported same-chain asset. Pending and error wallets are not scanned.
- project_enabled reports only the project's asset acceptance policy and can be false while tracking_active remains true.
- balance and balance_atomic are exact strings; price_usd, value_usd, and total_value_usd are advisory and can be null. Fresh balance status does not guarantee a fresh market price.
- Valuation prefers CoinGecko prices up to two hours old. Native coins and verified canonical USDC/USDT can fall back to enabled Kraken/Binance USD quotes up to five minutes old, primary provider first. No assumed dollar peg or ticker-only custom-token pricing; project fixed/DEX prices stay separate. Invoice quotes are unchanged.
- Pending has no complete snapshot. Refreshing keeps the last completed amount and checked_at; it does not mean a blockchain transfer is pending. Stale/error amounts may also retain older values. Never treat an unavailable cache as zero or as a missing payment. Routine EVM/Solana refreshes reuse recently checked empty addresses for up to 30 minutes between audits, while funded, new and changed addresses are rechecked. Explicit console Scan balances requests a full scan.
- recent_payments is bounded to three observations per wallet and excludes invalidated history.
Errors
POSTCreate invoice/v1/projects/{project_id}/stores/{store_id}/invoicesRead + write
Creates an invoice atomically with wallet destinations, fresh exact quotes, audit history, and notification outbox entries. Replaying identical raw body bytes with the same credential and Idempotency-Key returns the original invoice.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Idempotency-Key | required | Unique 1–128 visible ASCII characters without whitespace. |
| Content-Type | recommended | application/json. The current raw-body handler parses JSON without enforcing the media type. |
| Accept | recommended | application/json |
Path parameters
project_idUUIDrequiredCopy Project API ID from Project → Settings → API IDs. Must be assigned to the credential; a readable project identifier is not accepted.
store_idUUIDrequiredCopy Store API ID from Project → Stores → select a store → Basic → API IDs. Required even for the default store; must be enabled and belong to project_id.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new invoice.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: order-1042-attempt-1' \
--header 'Content-Type: application/json' \
--data-raw '{
"amount": "49.90",
"currency": "USD"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new invoice.
const body = `{
"amount": "49.90",
"currency": "USD"
}`;
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "order-1042-attempt-1",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new invoice.
$body = <<<'JSON'
{
"amount": "49.90",
"currency": "USD"
}
JSON;
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: order-1042-attempt-1", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new invoice.
headers = {
"Idempotency-Key": "order-1042-attempt-1",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"amount": "49.90",
"currency": "USD"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response)): "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new invoice.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: order-1042-attempt-1' \
--header 'Content-Type: application/json' \
--data-raw '{
"payment_methods": [
{
"chain_slug": "bitcoin",
"asset_tickers": [
"BTC"
]
}
],
"amount": "49.90",
"currency": "USD",
"order_id": "order-1042",
"email": "ada@example.com",
"description": "Annual plan",
"underpayment_tolerance_percent": "1",
"ipn_url": "https://merchant.example/wholly/ipn",
"redirect_url": "https://merchant.example/orders/1042",
"cancel_url": "https://merchant.example/cart",
"redirect_automatically": true,
"language": "en",
"metadata": {
"cart_id": "cart-681",
"firstname": "Ada",
"lastname": "Lovelace",
"street": "12 Example Street",
"street2": "Suite 2",
"zip": "10115",
"city": "Berlin",
"country": "Germany",
"countryiso2": "DE",
"company": "Example GmbH",
"vatid": "DE123456789"
},
"exchange_rate_spread_percent": "0.5",
"checkout_appearance": {
"title": "Complete your order",
"intro": "Thanks for choosing our annual plan.",
"outro": "Questions? https://merchant.example/help",
"intro_font_size": 18,
"outro_font_size": 14,
"theme": "light",
"accent_color": "#1768CE",
"messages": {
"en": {
"paid": "Your order is ready."
}
}
}
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new invoice.
const body = `{
"payment_methods": [
{
"chain_slug": "bitcoin",
"asset_tickers": [
"BTC"
]
}
],
"amount": "49.90",
"currency": "USD",
"order_id": "order-1042",
"email": "ada@example.com",
"description": "Annual plan",
"underpayment_tolerance_percent": "1",
"ipn_url": "https://merchant.example/wholly/ipn",
"redirect_url": "https://merchant.example/orders/1042",
"cancel_url": "https://merchant.example/cart",
"redirect_automatically": true,
"language": "en",
"metadata": {
"cart_id": "cart-681",
"firstname": "Ada",
"lastname": "Lovelace",
"street": "12 Example Street",
"street2": "Suite 2",
"zip": "10115",
"city": "Berlin",
"country": "Germany",
"countryiso2": "DE",
"company": "Example GmbH",
"vatid": "DE123456789"
},
"exchange_rate_spread_percent": "0.5",
"checkout_appearance": {
"title": "Complete your order",
"intro": "Thanks for choosing our annual plan.",
"outro": "Questions? https://merchant.example/help",
"intro_font_size": 18,
"outro_font_size": 14,
"theme": "light",
"accent_color": "#1768CE",
"messages": {
"en": {
"paid": "Your order is ready."
}
}
}
}`;
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "order-1042-attempt-1",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new invoice.
$body = <<<'JSON'
{
"payment_methods": [
{
"chain_slug": "bitcoin",
"asset_tickers": [
"BTC"
]
}
],
"amount": "49.90",
"currency": "USD",
"order_id": "order-1042",
"email": "ada@example.com",
"description": "Annual plan",
"underpayment_tolerance_percent": "1",
"ipn_url": "https://merchant.example/wholly/ipn",
"redirect_url": "https://merchant.example/orders/1042",
"cancel_url": "https://merchant.example/cart",
"redirect_automatically": true,
"language": "en",
"metadata": {
"cart_id": "cart-681",
"firstname": "Ada",
"lastname": "Lovelace",
"street": "12 Example Street",
"street2": "Suite 2",
"zip": "10115",
"city": "Berlin",
"country": "Germany",
"countryiso2": "DE",
"company": "Example GmbH",
"vatid": "DE123456789"
},
"exchange_rate_spread_percent": "0.5",
"checkout_appearance": {
"title": "Complete your order",
"intro": "Thanks for choosing our annual plan.",
"outro": "Questions? https://merchant.example/help",
"intro_font_size": 18,
"outro_font_size": 14,
"theme": "light",
"accent_color": "#1768CE",
"messages": {
"en": {
"paid": "Your order is ready."
}
}
}
}
JSON;
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: order-1042-attempt-1", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new invoice.
headers = {
"Idempotency-Key": "order-1042-attempt-1",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"payment_methods": [
{
"chain_slug": "bitcoin",
"asset_tickers": [
"BTC"
]
}
],
"amount": "49.90",
"currency": "USD",
"order_id": "order-1042",
"email": "ada@example.com",
"description": "Annual plan",
"underpayment_tolerance_percent": "1",
"ipn_url": "https://merchant.example/wholly/ipn",
"redirect_url": "https://merchant.example/orders/1042",
"cancel_url": "https://merchant.example/cart",
"redirect_automatically": true,
"language": "en",
"metadata": {
"cart_id": "cart-681",
"firstname": "Ada",
"lastname": "Lovelace",
"street": "12 Example Street",
"street2": "Suite 2",
"zip": "10115",
"city": "Berlin",
"country": "Germany",
"countryiso2": "DE",
"company": "Example GmbH",
"vatid": "DE123456789"
},
"exchange_rate_spread_percent": "0.5",
"checkout_appearance": {
"title": "Complete your order",
"intro": "Thanks for choosing our annual plan.",
"outro": "Questions? https://merchant.example/help",
"intro_font_size": 18,
"outro_font_size": 14,
"theme": "light",
"accent_color": "#1768CE",
"messages": {
"en": {
"paid": "Your order is ready."
}
}
}
}""".encode("utf-8")
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Request body
amountstringrequiredUnsigned plain decimal string; no sign or exponent, up to 48 integer digits and 30 decimal places. Must be positive by default. A store can allow zero-amount invoices in Stores → Invoice; zero totals settle without receiving funds, allocating addresses or processing fees.Details
Unsigned plain decimal string; no sign or exponent, up to 48 integer digits and 30 decimal places. Must be positive by default. A store can allow zero-amount invoices in Stores → Invoice; zero totals settle without receiving funds, allocating addresses or processing fees.
currencystring | nulloptionalSupported three-letter fiat currency, normalized to uppercase. Omitted or null inherits the store invoice currency. Creation also requires an independently available billing conversion rate.Details
Supported three-letter fiat currency, normalized to uppercase. Omitted or null inherits the store invoice currency. Creation also requires an independently available billing conversion rate.
payment_methods[]object[] | nulloptional
Select store-enabled methods for this invoice. Merchant 5.4.0+: ignore unknown/inactive/unaccepted choices; if none match, use store defaults. Omitted/null also uses store defaults; [] is invalid. Never enables a method or changes store settings. See selection schema below.Details
Select store-enabled methods for this invoice. Merchant 5.4.0+: ignore unknown/inactive/unaccepted choices; if none match, use store defaults. Omitted/null also uses store defaults; [] is invalid. Never enables a method or changes store settings. See selection schema below.
chain_slugstringrequiredCopy chain_slug in Project → Stores → Payment methods, or read it from GET /v1/projects/{project_id}/stores/{store_id}/payment-assets, such as ethereum, base or bitcoin. A chain/rail pair may occur only once.Details
Copy chain_slug in Project → Stores → Payment methods, or read it from GET /v1/projects/{project_id}/stores/{store_id}/payment-assets, such as ethereum, base or bitcoin. A chain/rail pair may occur only once.
asset_ids[]UUID[] | nulloptionalOn-chain asset.id UUIDs, not contract addresses or invoice payment-method IDs. Use this OR asset_tickers. Omit both selectors for all active accepted assets on this chain. [] and duplicate/nil IDs are invalid. On 5.4.0+, ignore IDs not active/accepted on this chain in this store; an entirely unmatched selection uses store defaults.Details
On-chain asset.id UUIDs, not contract addresses or invoice payment-method IDs. Use this OR asset_tickers. Omit both selectors for all active accepted assets on this chain. [] and duplicate/nil IDs are invalid. On 5.4.0+, ignore IDs not active/accepted on this chain in this store; an entirely unmatched selection uses store defaults.
asset_tickers[]string[] | nulloptionalMerchant 5.3.0+. Symbols such as BTC, USDC or PEPE, scoped to chain_slug and this store. 1–64 unique tickers; trim/case-insensitive, 1–40 ASCII letters/digits/dot/underscore/hyphen. Use this OR asset_ids. On 5.4.0+, ignore unknown/inactive/unaccepted tickers. Ambiguous accepted symbols still fail: use asset_ids. Active selected methods need valid wallets and pricing; temporary on-chain scanner outages do not block creation from 6.0.6. Lightning optionally accepts only BTC.Details
Merchant 5.3.0+. Symbols such as BTC, USDC or PEPE, scoped to chain_slug and this store. 1–64 unique tickers; trim/case-insensitive, 1–40 ASCII letters/digits/dot/underscore/hyphen. Use this OR asset_ids. On 5.4.0+, ignore unknown/inactive/unaccepted tickers. Ambiguous accepted symbols still fail: use asset_ids. Active selected methods need valid wallets and pricing; temporary on-chain scanner outages do not block creation from 6.0.6. Lightning optionally accepts only BTC.
payment_railonchain | lightningoptionalDefaults to onchain. To choose Bitcoin Lightning, use {chain_slug: bitcoin, payment_rail: lightning} with no asset_ids; asset_tickers may optionally be [BTC]. Bitcoin on-chain does not include Lightning. The store's Lightning connection must already be enabled and ready.Details
Defaults to onchain. To choose Bitcoin Lightning, use {chain_slug: bitcoin, payment_rail: lightning} with no asset_ids; asset_tickers may optionally be [BTC]. Bitcoin on-chain does not include Lightning. The store's Lightning connection must already be enabled and ready.
order_idstring | nulloptionalMerchant order reference, 1–128 characters after trimming; control characters are rejected.
emailstring | nulloptionalMerchant-only customer email, normalized to a practical ASCII address with at most 254 characters. Omitted or null stores no email.
descriptionstring | nulloptionalCustomer-facing description, 1–500 characters; line breaks and tabs are allowed.
expires_in_secondsinteger | nulloptionalInvoice quote lifetime from 300 through 86,400 seconds; omitted or null inherits store policy.
exchange_rate_spread_percentdecimal string | nulloptionalQuote markup from 0 through 100, with at most two decimal places. Omitted or null inherits the store default; "0" disables it for this invoice. Applied before upward rounding, then locked. Does not change the invoice fiat amount or the processing-fee basis.Details
Quote markup from 0 through 100, with at most two decimal places. Omitted or null inherits the store default; "0" disables it for this invoice. Applied before upward rounding, then locked. Does not change the invoice fiat amount or the processing-fee basis.
underpayment_tolerance_percentdecimal string | nulloptionalAccepted shortfall from 0 through 99.99 with at most two decimal places. Omitted or null inherits the store default.
ipn_urlstring | nulloptionalPublic HTTPS callback, at most 2,048 bytes and without credentials or fragment. Overrides the store default; null/omitted inherits it.
redirect_urlstring | nulloptionalHTTPS success URL used after settlement, at most 2,048 bytes and without embedded credentials. Omitted or null inherits the store default and cannot clear it.
cancel_urlstring | nulloptionalHTTPS return URL used when checkout ends without successful payment. Omitted or null inherits the store default and cannot clear it.
redirect_automaticallyboolean | nulloptionalOmitted or null inherits store policy. true requires an effective redirect_url.
languagestring | nulloptionalCustomer language: en, de, es, pt-BR, it, ru, zh-CN, fr, ko or ja. pt means Brazilian Portuguese and zh means Simplified Chinese. Supported regional tags such as de-DE normalize to the listed language. Omitted or null inherits the store language. Console language is separate.Details
Customer language: en, de, es, pt-BR, it, ru, zh-CN, fr, ko or ja. pt means Brazilian Portuguese and zh means Simplified Chinese. Supported regional tags such as de-DE normalize to the listed language. Omitted or null inherits the store language. Console language is separate.
checkout_appearanceobject | nulloptional
Partial presentation settings for this invoice. Omitted/null follows the store's current design. An object, including {}, freezes the resolved design and images at creation. See the override schema below; no financial settings, HTML, CSS, JavaScript or remote image URLs.Details
Partial presentation settings for this invoice. Omitted/null follows the store's current design. An object, including {}, freezes the resolved design and images at creation. See the override schema below; no financial settings, HTML, CSS, JavaScript or remote image URLs.
inherit_default_storebooleanoptionaltrue selects the project's default-store design as the base; otherwise use the target store's effective design. Overrides are then applied and saved independently; the resolved invoice flag is false.Details
true selects the project's default-store design as the base; otherwise use the target store's effective design. Overrides are then applied and saved independently; the resolved invoice flag is false.
introstringoptionalPlain text, up to 2,000 characters each. Intro appears at the top, Outro at the bottom in every state. Newlines are preserved; safe text URLs become links. Empty string clears. Legacy customer_message is accepted as an alias for intro; do not send both.Details
Plain text, up to 2,000 characters each. Intro appears at the top, Outro at the bottom in every state. Newlines are preserved; safe text URLs become links. Empty string clears. Legacy customer_message is accepted as an alias for intro; do not send both.
outrostringoptionalPlain text, up to 2,000 characters each. Intro appears at the top, Outro at the bottom in every state. Newlines are preserved; safe text URLs become links. Empty string clears. Legacy customer_message is accepted as an alias for intro; do not send both.Details
Plain text, up to 2,000 characters each. Intro appears at the top, Outro at the bottom in every state. Newlines are preserved; safe text URLs become links. Empty string clears. Legacy customer_message is accepted as an alias for intro; do not send both.
intro_font_sizeintegeroptionalPixels: 12, 14, 16, 18, 20 or 24. Default 16 unless inherited differently.
outro_font_sizeintegeroptionalPixels: 12, 14, 16, 18, 20 or 24. Default 16 unless inherited differently.
accent_colorstringoptional#RRGGBB. Background, card and button may be empty for automatic colors. Text contrast is automatic.
background_colorstringoptional#RRGGBB. Background, card and button may be empty for automatic colors. Text contrast is automatic.
card_colorstringoptional#RRGGBB. Background, card and button may be empty for automatic colors. Text contrast is automatic.
button_colorstringoptional#RRGGBB. Background, card and button may be empty for automatic colors. Text contrast is automatic.
imagesobjectoptional
Keys logo_light, logo_dark, favicon. Omitted key keeps the base image; null removes it. An object {store_id: UUID, kind?: logo_light|logo_dark|favicon} reuses that store's effective uploaded image in the SAME project. kind defaults to the target key. Upload in Store → Checkout first; copy the Store API ID from Basic → API IDs. Missing images or cross-project IDs return 400. No external URLs or image data accepted.Details
Keys logo_light, logo_dark, favicon. Omitted key keeps the base image; null removes it. An object {store_id: UUID, kind?: logo_light|logo_dark|favicon} reuses that store's effective uploaded image in the SAME project. kind defaults to the target key. Upload in Store → Checkout first; copy the Store API ID from Basic → API IDs. Missing images or cross-project IDs return 400. No external URLs or image data accepted.
show_order_idbooleanoptionalShow order ID details and a plain-text description below the title. details_expanded opens order ID details initially. Display only, not data redaction.
show_descriptionbooleanoptionalShow order ID details and a plain-text description below the title. details_expanded opens order ID details initially. Display only, not data redaction.
details_expandedbooleanoptionalShow order ID details and a plain-text description below the title. details_expanded opens order ID details initially. Display only, not data redaction.
show_project_namebooleanoptionalMerchant 5.6.0+: show or hide each name in the customer checkout header. Both default to true. Also available in Store → Checkout; inherited and invoice-snapshotted like other appearance settings. Display only, not data redaction.Details
Merchant 5.6.0+: show or hide each name in the customer checkout header. Both default to true. Also available in Store → Checkout; inherited and invoice-snapshotted like other appearance settings. Display only, not data redaction.
show_store_namebooleanoptionalMerchant 5.6.0+: show or hide each name in the customer checkout header. Both default to true. Also available in Store → Checkout; inherited and invoice-snapshotted like other appearance settings. Display only, not data redaction.Details
Merchant 5.6.0+: show or hide each name in the customer checkout header. Both default to true. Also available in Store → Checkout; inherited and invoice-snapshotted like other appearance settings. Display only, not data redaction.
featured_chains[]string[]optionalOrdered chain slugs, at most 60 unique values (lowercase letters, digits, hyphens; up to 64 characters). [] clears. Only available invoice methods are reordered.Details
Ordered chain slugs, at most 60 unique values (lowercase letters, digits, hyphens; up to 64 characters). [] clears. Only available invoice methods are reordered.
featured_asset_ids[]UUID[]optionalUp to 100 unique ordered asset IDs; [] clears. Default asset may be null. IDs come from payment-assets, not payment-intent IDs. These never enable methods; received payments and valid customer preferences take priority.Details
Up to 100 unique ordered asset IDs; [] clears. Default asset may be null. IDs come from payment-assets, not payment-intent IDs. These never enable methods; received payments and valid customer preferences take priority.
default_asset_idUUID|nulloptionalUp to 100 unique ordered asset IDs; [] clears. Default asset may be null. IDs come from payment-assets, not payment-intent IDs. These never enable methods; received payments and valid customer preferences take priority.Details
Up to 100 unique ordered asset IDs; [] clears. Default asset may be null. IDs come from payment-assets, not payment-intent IDs. These never enable methods; received payments and valid customer preferences take priority.
messagesobjectoptional
Language objects keyed by en, de, es, pt-BR, it, ru, zh-CN, fr, ko or ja, with waiting, confirming, paid, underpaid, expired plain strings (500 characters each). Only supplied languages/states change; {} clears all messages, {en:{}} clears English, and an empty state string clears that state. English is the fallback. Does not replace actual status.Details
Language objects keyed by en, de, es, pt-BR, it, ru, zh-CN, fr, ko or ja, with waiting, confirming, paid, underpaid, expired plain strings (500 characters each). Only supplied languages/states change; {} clears all messages, {en:{}} clears English, and an empty state string clears that state. English is the fallback. Does not replace actual status.
enobjectoptional
deobjectoptional
esobjectoptional
pt-BRobjectoptional
itobjectoptional
ruobjectoptional
zh-CNobjectoptional
frobjectoptional
koobjectoptional
support_urlstringoptionalHTTPS URLs up to 2,048 characters, without credentials. Empty clears. Links open in a new window.
terms_urlstringoptionalHTTPS URLs up to 2,048 characters, without credentials. Empty clears. Links open in a new window.
privacy_urlstringoptionalHTTPS URLs up to 2,048 characters, without credentials. Empty clears. Links open in a new window.
return_button_textstringoptionalLabel up to 60 characters. Use top-level redirect_url/cancel_url/redirect_automatically/language for invoice behavior.
show_return_buttonbooleanoptionalShow the manual Return to merchant/cancel button in checkout, including terminal states. Omit to inherit the store appearance (default true); false hides it. Does not cancel an invoice, hide URLs in JSON or disable automatic success redirects. Set top-level redirect_automatically to false separately if needed. Requires merchant 8.1.4 or newer.Details
Show the manual Return to merchant/cancel button in checkout, including terminal states. Omit to inherit the store appearance (default true); false hides it. Does not cancel an invoice, hide URLs in JSON or disable automatic success redirects. Set top-level redirect_automatically to false separately if needed. Requires merchant 8.1.4 or newer.
metadataobject | nulloptional
Merchant-only JSON object; omitted or null becomes {}, maximum 4,096 encoded bytes and five nested levels. firstname, lastname, street, street2, zip, city, country, countryiso2, company, and vatid are validated, normalized, and projected into customer summary fields.Details
Merchant-only JSON object; omitted or null becomes {}, maximum 4,096 encoded bytes and five nested levels. firstname, lastname, street, street2, zip, city, country, countryiso2, company, and vatid are validated, normalized, and projected into customer summary fields.
Response 201 new invoice; 200 exact idempotent replay
Example response
{
"data": {
"id": "2f798f9f-01f2-42f0-9d10-5581d6116b4c",
"invoice_id": "0a6a98db-d93d-48ee-8c3c-fd45f90c4a50",
"project_id": "11111111-1111-4111-8111-111111111111",
"store_id": "22222222-2222-4222-8222-222222222222",
"source": "api",
"order_id": "order-1042",
"email": "ada@example.com",
"customer_name": "Ada Lovelace · Example GmbH",
"customer_address": "Example GmbH · 12 Example Street · Suite 2 · 10115 Berlin · Germany (DE) · VAT DE123456789",
"description": "Annual plan",
"amount": "49.9",
"currency": "USD",
"exchange_rate_spread_percent": "0.5",
"underpayment_tolerance_percent": "1",
"status": "new",
"amount_status": "none",
"timing_status": "on_time",
"resolution": "automatic",
"sequence": 1,
"winning_payment_intent_id": null,
"expires_at": "2026-08-31T18:15:00Z",
"monitoring_expires_at": "2026-09-07T18:15:00Z",
"settled_at": null,
"cancelled_at": null,
"archived_at": null,
"created_at": "2026-08-31T18:00:00Z",
"updated_at": "2026-08-31T18:00:00Z",
"ipn_url": "https://merchant.example/wholly/ipn",
"redirect_url": "https://merchant.example/orders/1042",
"cancel_url": "https://merchant.example/cart",
"redirect_automatically": true,
"checkout_language": "en",
"metadata": { "cart_id": "cart-681", "firstname": "Ada", "lastname": "Lovelace", "street": "12 Example Street", "street2": "Suite 2", "zip": "10115", "city": "Berlin", "country": "Germany", "countryiso2": "DE", "company": "Example GmbH", "vatid": "DE123456789" },
"payment_intents": [
{
"id": "33333333-3333-4333-8333-333333333333",
"asset_id": "10000000-0000-4000-8000-000000000001",
"asset_key": "bip122:000000000019d6689c085ae165831e93/slip44:0",
"chain_slug": "bitcoin",
"network": "mainnet",
"caip_network_id": "bip122:000000000019d6689c085ae165831e93",
"caip_asset_id": "bip122:000000000019d6689c085ae165831e93/slip44:0",
"symbol": "BTC",
"asset_decimals": 8,
"status": "pending",
"finality_mode": "confirmations",
"required_confirmations": 1,
"quote_rate": "0.000009218",
"quote_details": null,
"expected_amount": "0.00046",
"expected_amount_atomic": "46000",
"minimum_payment_amount": "0.0004554",
"minimum_payment_amount_atomic": "45540",
"received_amount": "0",
"received_amount_atomic": "0",
"confirmed_amount": "0",
"confirmed_amount_atomic": "0",
"destination_address": "bc1q…example",
"destination_tag": null,
"derivation_index": 42,
"quote_expires_at": "2026-08-31T18:15:00Z",
"monitoring_expires_at": "2026-09-07T18:15:00Z",
"next_check_at": "2026-08-31T18:00:00Z",
"last_checked_at": null,
"last_chain_height": null,
"last_anchor_hash": null,
"last_monitor_error": null,
"first_payment_at": null,
"fully_paid_at": null,
"finalized_at": null
}
]
},
"links": {
"checkout": "https://pay.example.com/invoice/0a6a98db-d93d-48ee-8c3c-fd45f90c4a50"
}
}Response fields
dataobject
customer_namestring | nullalwaysDerived display name from private firstname, lastname, and company metadata.
customer_addressstring | nullalwaysDerived one-line merchant address from private company, street, street2, zip, city, country, countryiso2, and vatid metadata.
exchange_rate_spread_percentdecimal stringalwaysLocked quote spread: the creation override, or the store default when omitted. Applied before upward rounding; never changes on this invoice.
underpayment_tolerance_percentdecimal stringalwaysImmutable accepted shortfall percentage snapshotted when the invoice was created.
amount_statusamount statusalwaysnone, partial, paid, or overpaid. An explicitly allowed zero-amount invoice settles with none and no payment methods.
monitoring_expires_atRFC 3339 timestampalwaysLatest configured late-monitoring cutoff across payment methods.
ipn_urlstring | nullalwaysEffective per-invoice IPN target. Merchant response only; omitted from public checkout.
cancel_urlstring | nullalwaysEffective return URL used when checkout ends without successful payment.
metadataobjectalways
Merchant metadata. Never returned by public checkout. Console payment tools add payment_tool provenance (id, kind, title, pricing, requested_amount, request_id); it does not prove payment. Authenticate callbacks and fulfill invoice.settled once per invoice_id.Details
Merchant metadata. Never returned by public checkout. Console payment tools add payment_tool provenance (id, kind, title, pricing, requested_amount, request_id); it does not prove payment. Authenticate callbacks and fulfill invoice.settled once per invoice_id.
payment_intents[]object[]always
Quoted payment methods and monitoring state.
payment_railonchain | lightningalwaysInvoice transport. Bitcoin on-chain and Lightning can share asset_id; use intent id plus this field, not symbol alone. This differs from the asset catalog's scanner payment_rail.Details
Invoice transport. Bitcoin on-chain and Lightning can share asset_id; use intent id plus this field, not symbol alone. This differs from the asset catalog's scanner payment_rail.
bolt11string | nullalwaysLightning payment request, otherwise null. Pay this request with a Lightning wallet, never send on-chain funds to its payment hash.
asset_decimalsintegeralwaysAtomic-unit precision. Lightning BTC uses 11 (millisatoshis), not on-chain Bitcoin's 8. Quotes are whole satoshis; receipts retain millisatoshi precision.
quote_ratedecimal stringalwaysAsset units per one invoice currency unit, including the locked spread. For example 1.02 USDC per USD. Not the inverse rate.
quote_detailsobject | nullalwaysLocked quote provenance: reference_rate before spread, unrounded_payment_amount, rounding_adjustment, pricing_provider, asset_provider, pricing_fetched_at and asset_fetched_at. Null on older invoices; no historical values are fabricated.Details
Locked quote provenance: reference_rate before spread, unrounded_payment_amount, rounding_adjustment, pricing_provider, asset_provider, pricing_fetched_at and asset_fetched_at. Null on older invoices; no historical values are fabricated.
expected_amountdecimal stringalwaysExact locked asset amount to pay after spread and upward rounding. From 4.1.1, recognized verified fiat stablecoins (such as USDC, USDT, DAI, USDS, EURC) round up to at most two decimals; 1.321 becomes 1.33, never 1.32. This is the expected amount even with zero tolerance. Other assets keep adaptive precision. Existing invoices are never repriced.Details
Exact locked asset amount to pay after spread and upward rounding. From 4.1.1, recognized verified fiat stablecoins (such as USDC, USDT, DAI, USDS, EURC) round up to at most two decimals; 1.321 becomes 1.33, never 1.32. This is the expected amount even with zero tolerance. Other assets keep adaptive precision. Existing invoices are never repriced.
minimum_payment_amountdecimal stringalwaysSmallest amount accepted as paid after applying the invoice tolerance.
minimum_payment_amount_atomicinteger stringalwaysExact accepted threshold in the asset's smallest unit.
destination_addressstringalwaysOn-chain receiving address, or the 64-character payment hash for Lightning. Use bolt11 to pay Lightning; its hash is not a Bitcoin address.
destination_tagstring | nullalwaysRequired public payment reference where the rail uses one: XRP destination tag, Stellar memo ID, or TON invoice comment. Null for unique-address rails.
Usage notes
- payment_methods filters store-enabled methods for this invoice only. Omitted/null keeps all store methods; [] is invalid. Find the small chain_slug hint and displayed asset tickers in Project → Stores → Payment methods. The API payment-assets list supplies chain_slug, asset.symbol and asset.id. Use {chain_slug: ethereum, asset_tickers: [USDC, USDT]} for accepted Ethereum tokens; BTC and PEPE work the same way on their selected chains. Tickers are case-insensitive, chain-scoped, and resolve only inside the store. Two accepted contracts with the same ticker return 400 instead of choosing one, even if one is not ready; use asset_ids for that case. Native assets, catalog tokens and custom tokens follow the same rules. Each chain/rail may appear once; at most 64 final methods. Merchant 5.4.0+: unknown, disabled, wrong-chain or unaccepted choices are ignored. If the entire selection has no active accepted matches, store defaults apply; otherwise only matching choices are used. A chain-only entry includes every active accepted on-chain asset. Active selected methods need valid wallets, installed scanner adapters and trustworthy rates. From 6.0.6, unavailable scanners, cooldowns and pending/stale health checks do not block invoice creation or remove configured on-chain methods. Detection retries automatically; settlement still requires provider quorum and confirmations. Monitor receive_readiness and keep providers available: an invoice can remain unverified until scanners recover. Monero subaddress allocation and Lightning BOLT11 generation still require their external wallet/node service. Failures return error.message plus error.details.payment_methods with chain_slug, asset_ticker, reason_code and, for scanner diagnostics, required_endpoint_role, healthy_endpoints and required_independent_providers. TRON accepts indexed history or supported raw solidified native-block APIs; basic health alone does not prove scanner compatibility. Pricing failures identify the asset/currency. Nothing enables an unaccepted asset or changes store policy. On merchant versions before 5.4.0, unknown/inactive explicit choices fail instead. Existing invoice methods never expand when store settings change. Lightning must be selected separately. Replays keep the original methods, and changing selections with the same Idempotency-Key returns 409.
- checkout_appearance supports every presentation setting listed above. Omitted fields inherit, arrays replace, and nested message fields merge; an empty message object clears that scope. The resolved design and images are saved for this invoice without editing the store. Read appearance from public checkout JSON to inspect the result. The whole request is limited to 32 KiB and resolved settings to 20 KiB.
- Changing checkout_appearance with the same Idempotency-Key returns 409; retry with identical raw bytes. Appearance does not change amounts, rates, accepted assets, confirmation requirements, real status or embedding permissions. No HTML, CSS, scripts or remote image fetching.
- exchange_rate_spread_percent overrides the store default for this invoice: omit or send null to inherit, or send "0" to disable it. Existing invoice quotes never change.
- Spread is applied before upward rounding. Fees remain based on the original invoice fiat amount, excluding the spread.
- Always send the returned expected_amount or expected_amount_atomic. Rounding is upward, limited by asset precision, 0.1% of the amount and one fiat minor unit.
- Retries must keep the same credential, Idempotency-Key and exact body bytes. Changing the spread with the same key returns 409 idempotency_conflict.
- An exact replay is checked before new quote, callback DNS or address preparation. Credential scope and project/store authorization are still checked on every request.
- An effective ipn_url requires the store IPN signing secret. Unknown body fields are rejected.
Errors
GETList invoices/v1/projects/{project_id}/invoicesRead only
Returns a compact newest-first page of scoped invoice summaries, including merchant-only email and customer fields derived from recognized metadata. Search, status, and store filters are evaluated server-side; the response includes total and has_more for predictable paging.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | recommended | application/json |
Path parameters
Query parameters
searchstringOptional case-insensitive invoice-id, order-id, or email prefix; exact invoice UUID; or substring across description and recognized customer fields. All metadata keys and text, numeric and boolean values (including nested objects/arrays) also support indexed word-prefix search: every search word must match, punctuation is treated as a separator. Trimmed, at most 100 characters, no controls. Metadata matches do not add raw metadata to list responses; use invoice detail to read it.Details
Optional case-insensitive invoice-id, order-id, or email prefix; exact invoice UUID; or substring across description and recognized customer fields. All metadata keys and text, numeric and boolean values (including nested objects/arrays) also support indexed word-prefix search: every search word must match, punctuation is treated as a separator. Trimmed, at most 100 characters, no controls. Metadata matches do not add raw metadata to list responses; use invoice detail to read it.
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices?search=order-1042&status=processing&limit=50&offset=0" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Accept: application/json'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices?search=order-1042&status=processing&limit=50&offset=0", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
"Accept": "application/json"
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices?search=order-1042&status=processing&limit=50&offset=0");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Accept: application/json"],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Accept": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices?search=order-1042&status=processing&limit=50&offset=0",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": [
{
"id": "2f798f9f-01f2-42f0-9d10-5581d6116b4c",
"invoice_id": "0a6a98db-d93d-48ee-8c3c-fd45f90c4a50",
"project_id": "11111111-1111-4111-8111-111111111111",
"store_id": "22222222-2222-4222-8222-222222222222",
"source": "api",
"order_id": "order-1042",
"email": "ada@example.com",
"customer_name": "Ada Lovelace · Example GmbH",
"customer_address": "Example GmbH · 12 Example Street · Suite 2 · 10115 Berlin · Germany (DE) · VAT DE123456789",
"description": "Annual plan",
"amount": "49.9",
"currency": "USD",
"exchange_rate_spread_percent": "0.5",
"underpayment_tolerance_percent": "1",
"status": "processing",
"amount_status": "paid",
"timing_status": "on_time",
"resolution": "automatic",
"sequence": 3,
"winning_payment_intent_id": "33333333-3333-4333-8333-333333333333",
"expires_at": "2026-08-31T18:15:00Z",
"monitoring_expires_at": "2026-09-07T18:15:00Z",
"settled_at": null,
"cancelled_at": null,
"archived_at": null,
"created_at": "2026-08-31T18:00:00Z",
"updated_at": "2026-08-31T18:04:10Z"
}
],
"pagination": {
"limit": 50,
"offset": 0,
"total": 143,
"has_more": true
}
}Response fields
data[]object[]
customer_namestring | nullalwaysDerived display name from private firstname, lastname, and company metadata.
customer_addressstring | nullalwaysDerived one-line merchant address from private company, street, street2, zip, city, country, countryiso2, and vatid metadata.
exchange_rate_spread_percentdecimal stringalwaysLocked quote spread: the creation override, or the store default when omitted. Applied before upward rounding; never changes on this invoice.
underpayment_tolerance_percentdecimal stringalwaysImmutable accepted shortfall percentage snapshotted when the invoice was created.
amount_statusamount statusalwaysnone, partial, paid, or overpaid. An explicitly allowed zero-amount invoice settles with none and no payment methods.
monitoring_expires_atRFC 3339 timestampalwaysLatest configured late-monitoring cutoff across payment methods.
paginationobject
Usage notes
- Sorted by created_at descending, then internal id descending.
- List items are InvoiceSummary objects; email, customer_name, and customer_address are merchant-only. Call detail for raw metadata and payment intents.
- For the next page set offset to pagination.offset + pagination.limit only when has_more is true.
- The count and page are read from one repeatable-read database snapshot; concurrent writes appear on a later request.
Errors
GETRetrieve invoice/v1/projects/{project_id}/invoices/{invoice_id}Read only
Returns a complete merchant invoice detail and the current active checkout URL. Use this route for polling and reconciliation.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | recommended | application/json |
Path parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices/YOUR_PUBLIC_INVOICE_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Accept: application/json'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices/YOUR_PUBLIC_INVOICE_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
"Accept": "application/json"
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices/YOUR_PUBLIC_INVOICE_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Accept: application/json"],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Accept": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices/YOUR_PUBLIC_INVOICE_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": {
"id": "2f798f9f-01f2-42f0-9d10-5581d6116b4c",
"invoice_id": "0a6a98db-d93d-48ee-8c3c-fd45f90c4a50",
"project_id": "11111111-1111-4111-8111-111111111111",
"store_id": "22222222-2222-4222-8222-222222222222",
"source": "api",
"order_id": "order-1042",
"email": "ada@example.com",
"customer_name": "Ada Lovelace · Example GmbH",
"customer_address": "Example GmbH · 12 Example Street · Suite 2 · 10115 Berlin · Germany (DE) · VAT DE123456789",
"description": "Annual plan",
"amount": "49.9",
"currency": "USD",
"exchange_rate_spread_percent": "0.5",
"underpayment_tolerance_percent": "1",
"status": "settled",
"amount_status": "paid",
"timing_status": "on_time",
"resolution": "automatic",
"sequence": 4,
"winning_payment_intent_id": "33333333-3333-4333-8333-333333333333",
"expires_at": "2026-08-31T18:15:00Z",
"monitoring_expires_at": "2026-09-07T18:15:00Z",
"settled_at": "2026-08-31T18:05:00Z",
"cancelled_at": null,
"archived_at": null,
"created_at": "2026-08-31T18:00:00Z",
"updated_at": "2026-08-31T18:05:00Z",
"ipn_url": "https://merchant.example/wholly/ipn",
"redirect_url": "https://merchant.example/orders/1042",
"cancel_url": "https://merchant.example/cart",
"redirect_automatically": true,
"checkout_language": "en",
"metadata": { "cart_id": "cart-681", "firstname": "Ada", "lastname": "Lovelace", "street": "12 Example Street", "street2": "Suite 2", "zip": "10115", "city": "Berlin", "country": "Germany", "countryiso2": "DE", "company": "Example GmbH", "vatid": "DE123456789" },
"payment_intents": [
{
"id": "33333333-3333-4333-8333-333333333333",
"asset_id": "10000000-0000-4000-8000-000000000001",
"asset_key": "bip122:000000000019d6689c085ae165831e93/slip44:0",
"chain_slug": "bitcoin",
"network": "mainnet",
"caip_network_id": "bip122:000000000019d6689c085ae165831e93",
"caip_asset_id": "bip122:000000000019d6689c085ae165831e93/slip44:0",
"symbol": "BTC",
"asset_decimals": 8,
"status": "paid",
"finality_mode": "confirmations",
"required_confirmations": 1,
"quote_rate": "0.000009218",
"quote_details": null,
"expected_amount": "0.00046",
"expected_amount_atomic": "46000",
"minimum_payment_amount": "0.0004554",
"minimum_payment_amount_atomic": "45540",
"received_amount": "0.0004554",
"received_amount_atomic": "45540",
"confirmed_amount": "0.0004554",
"confirmed_amount_atomic": "45540",
"destination_address": "bc1q…example",
"destination_tag": null,
"derivation_index": 42,
"quote_expires_at": "2026-08-31T18:15:00Z",
"monitoring_expires_at": "2026-09-07T18:15:00Z",
"next_check_at": null,
"last_checked_at": "2026-08-31T18:05:00Z",
"last_chain_height": 912345,
"last_anchor_hash": "000000000000000000example",
"last_monitor_error": null,
"first_payment_at": "2026-08-31T18:03:00Z",
"fully_paid_at": "2026-08-31T18:03:00Z",
"finalized_at": "2026-08-31T18:05:00Z"
}
]
},
"links": {
"checkout": "https://pay.example.com/invoice/0a6a98db-d93d-48ee-8c3c-fd45f90c4a50"
}
}Response fields
dataobject
customer_namestring | nullalwaysDerived display name from private firstname, lastname, and company metadata.
customer_addressstring | nullalwaysDerived one-line merchant address from private company, street, street2, zip, city, country, countryiso2, and vatid metadata.
exchange_rate_spread_percentdecimal stringalwaysLocked quote spread: the creation override, or the store default when omitted. Applied before upward rounding; never changes on this invoice.
underpayment_tolerance_percentdecimal stringalwaysImmutable accepted shortfall percentage snapshotted when the invoice was created.
amount_statusamount statusalwaysnone, partial, paid, or overpaid. An explicitly allowed zero-amount invoice settles with none and no payment methods.
monitoring_expires_atRFC 3339 timestampalwaysLatest configured late-monitoring cutoff across payment methods.
ipn_urlstring | nullalwaysEffective per-invoice IPN target. Merchant response only; omitted from public checkout.
cancel_urlstring | nullalwaysEffective return URL used when checkout ends without successful payment.
metadataobjectalways
Merchant metadata. Never returned by public checkout. Console payment tools add payment_tool provenance (id, kind, title, pricing, requested_amount, request_id); it does not prove payment. Authenticate callbacks and fulfill invoice.settled once per invoice_id.Details
Merchant metadata. Never returned by public checkout. Console payment tools add payment_tool provenance (id, kind, title, pricing, requested_amount, request_id); it does not prove payment. Authenticate callbacks and fulfill invoice.settled once per invoice_id.
payment_intents[]object[]always
Quoted payment methods and monitoring state.
payment_railonchain | lightningalwaysInvoice transport. Bitcoin on-chain and Lightning can share asset_id; use intent id plus this field, not symbol alone. This differs from the asset catalog's scanner payment_rail.Details
Invoice transport. Bitcoin on-chain and Lightning can share asset_id; use intent id plus this field, not symbol alone. This differs from the asset catalog's scanner payment_rail.
bolt11string | nullalwaysLightning payment request, otherwise null. Pay this request with a Lightning wallet, never send on-chain funds to its payment hash.
asset_decimalsintegeralwaysAtomic-unit precision. Lightning BTC uses 11 (millisatoshis), not on-chain Bitcoin's 8. Quotes are whole satoshis; receipts retain millisatoshi precision.
quote_ratedecimal stringalwaysAsset units per one invoice currency unit, including the locked spread. For example 1.02 USDC per USD. Not the inverse rate.
quote_detailsobject | nullalwaysLocked quote provenance: reference_rate before spread, unrounded_payment_amount, rounding_adjustment, pricing_provider, asset_provider, pricing_fetched_at and asset_fetched_at. Null on older invoices; no historical values are fabricated.Details
Locked quote provenance: reference_rate before spread, unrounded_payment_amount, rounding_adjustment, pricing_provider, asset_provider, pricing_fetched_at and asset_fetched_at. Null on older invoices; no historical values are fabricated.
expected_amountdecimal stringalwaysExact locked asset amount to pay after spread and upward rounding. From 4.1.1, recognized verified fiat stablecoins (such as USDC, USDT, DAI, USDS, EURC) round up to at most two decimals; 1.321 becomes 1.33, never 1.32. This is the expected amount even with zero tolerance. Other assets keep adaptive precision. Existing invoices are never repriced.Details
Exact locked asset amount to pay after spread and upward rounding. From 4.1.1, recognized verified fiat stablecoins (such as USDC, USDT, DAI, USDS, EURC) round up to at most two decimals; 1.321 becomes 1.33, never 1.32. This is the expected amount even with zero tolerance. Other assets keep adaptive precision. Existing invoices are never repriced.
minimum_payment_amountdecimal stringalwaysSmallest amount accepted as paid after applying the invoice tolerance.
minimum_payment_amount_atomicinteger stringalwaysExact accepted threshold in the asset's smallest unit.
destination_addressstringalwaysOn-chain receiving address, or the 64-character payment hash for Lightning. Use bolt11 to pay Lightning; its hash is not a Bitcoin address.
destination_tagstring | nullalwaysRequired public payment reference where the rail uses one: XRP destination tag, Stellar memo ID, or TON invoice comment. Null for unique-address rails.
Usage notes
- A scoped lookup deliberately returns invoice_not_found when the public ID is not in the authorized project.
- links.checkout uses Store → Basic → Store domains: this store's active pay hostname, then its default store's choice, then the system primary. Retired/draft or wrong-service hosts are ignored. This also applies to creation and MCP responses; links are resolved at response time, including idempotent replays. Signed callback links are frozen at event creation, not rewritten on retries. These preferences generate links only; they do not redirect traffic or change IP restrictions.
Errors
GETList invoice payments/v1/projects/{project_id}/invoices/{invoice_id}/paymentsRead only
Complete current transfer history, including invalidated observations. Use this when a callback marks payments_truncated. This is current state, not a reconstruction of an older event.
Authentication & headers
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | recommended | application/json |
Path parameters
Query parameters
Request
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices/YOUR_PUBLIC_INVOICE_ID/payments?limit=25&offset=0" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Accept: application/json'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices/YOUR_PUBLIC_INVOICE_ID/payments?limit=25&offset=0", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
"Accept": "application/json"
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices/YOUR_PUBLIC_INVOICE_ID/payments?limit=25&offset=0");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Accept: application/json"],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Accept": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices/YOUR_PUBLIC_INVOICE_ID/payments?limit=25&offset=0",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"invoice_id": "11111111-2222-4333-8444-555555555555",
"data": [{
"payment_id": "44444444-4444-4444-8444-444444444444",
"payment_method_id": "33333333-3333-4333-8333-333333333333",
"transaction_id": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"payment_hash": null,
"event_index": 12,
"payment_rail": "onchain",
"chain_slug": "ethereum",
"network": "mainnet",
"asset_id": "55555555-5555-4555-8555-555555555555",
"asset_key": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"caip_asset_id": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"symbol": "USDC",
"asset_decimals": 6,
"amount": "58.17342",
"amount_atomic": "58173420",
"status": "final",
"counts_towards_received": true,
"confirmations": 2,
"block_height": 25975377,
"observed_at": "2026-09-14T12:03:00Z",
"chain_time": "2026-09-14T12:02:48Z",
"finalized_at": "2026-09-14T12:04:00Z",
"explorer_name": "Etherscan",
"explorer_url": "https://etherscan.io/tx/0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}],
"pagination": {"limit": 25, "offset": 0, "total": 1, "has_more": false}
}Response fields
invoice_idstringdata[]object[]
payment_idstringpayment_method_idstringtransaction_idstringpayment_hashJSON | nullevent_indexintegerpayment_railstringchain_slugstringnetworkstringasset_idstringasset_keystringcaip_asset_idstringsymbolstringasset_decimalsintegeramountstringamount_atomicstringstatusstringcounts_towards_receivedbooleanconfirmationsintegerblock_heightintegerobserved_atstringchain_timestringfinalized_atstringexplorer_namestringexplorer_urlstringUsage notes
- An observation is a token log, UTXO output or other rail transfer, not necessarily a unique transaction hash. Deduplicate by payment_id; transaction_id plus event_index identifies the chain transfer.
- status is detected, confirming, final, reorged, replaced or invalid. Only counts_towards_received observations contribute to received amounts. Never add amounts across different assets.
- Lightning records use payment_hash with null transaction_id, confirmations and explorer links; BTC precision is 11 (millisatoshis). No preimages, BOLT11 or wallet secrets are exposed.
- Sorted by observed_at descending, then payment_id descending. Count and page use one repeatable-read snapshot; later pages may change as payments arrive. Deduplicate by payment_id when paginating a live invoice.
- Existing read-only project scope, IP restrictions and per-credential rate limits apply. Never follow a callback-provided link with your token unless its origin matches your configured API host.
Errors
GETCheckout shell/Public
Managed checkout-host root that serves the checkout application without selecting an invoice. Customer integrations should normally use links.checkout instead.
Authentication & headers
Public
Request
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/" \
--output 'checkout.html'// Node.js 18+ · run on your server, never in browser code.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://pay.example.com/", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
await writeFile("checkout.html", Buffer.from(await response.arrayBuffer()));<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
file_put_contents("checkout.html", $response);# Python 3 · standard library; run on your server.
import json
from pathlib import Path
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://pay.example.com/",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
Path("checkout.html").write_bytes(response.read())Response 200 text/html
Example response
<!doctype html>
<!-- Hosted Wholly Crypto checkout shell -->Usage notes
- No bearer token is needed.
- The managed checkout edge allows GET/HEAD and rejects other methods.
Errors
GETHosted checkout page/invoice/{invoice_id}Public
Customer-facing HTML checkout. The page fetches checkout-safe JSON from the same checkout host. Embedding is denied unless the store enables it and explicitly allows the parent HTTPS origin.
Authentication & headers
Public
Path parameters
Request
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/invoice/YOUR_PUBLIC_INVOICE_ID" \
--output 'checkout.html'// Node.js 18+ · run on your server, never in browser code.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://pay.example.com/invoice/YOUR_PUBLIC_INVOICE_ID", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
await writeFile("checkout.html", Buffer.from(await response.arrayBuffer()));<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/invoice/YOUR_PUBLIC_INVOICE_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
file_put_contents("checkout.html", $response);# Python 3 · standard library; run on your server.
import json
from pathlib import Path
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://pay.example.com/invoice/YOUR_PUBLIC_INVOICE_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
Path("checkout.html").write_bytes(response.read())Response 200 text/html
Example response
<!doctype html>
<!-- Hosted Wholly Crypto checkout application -->Usage notes
- No bearer token is accepted or needed.
- The HTML shell itself returns 200 even when an invoice is absent; its checkout JSON request then receives invoice_not_found.
- The response is no-store, noindex, and has invoice-specific frame-ancestors CSP.
- Disabled project/store or an unknown invoice does not expose checkout data.
Errors
GETCheckout-safe invoice/checkout-api/invoices/{invoice_id}Public
Returns only fields needed to render checkout. It deliberately omits internal IDs, customer email and derived address fields, IPN URL, merchant metadata, wallet IDs, derivation paths, and monitor diagnostics.
Authentication & headers
Public
Path parameters
Request
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID" \
--header 'Accept: application/json'// Node.js 18+ · run on your server, never in browser code.
const response = await fetch("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID", {
method: "GET",
headers: {
"Accept": "application/json"
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ["Accept: application/json"],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Accept": "application/json"
}
request = Request("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": {
"invoice_id": "0a6a98db-d93d-48ee-8c3c-fd45f90c4a50",
"order_id": "order-1042",
"description": "Annual plan",
"amount": "49.9",
"currency": "USD",
"exchange_rate_spread_percent": "0.5",
"underpayment_tolerance_percent": "1",
"status": "processing",
"amount_status": "partial",
"timing_status": "on_time",
"sequence": 3,
"active_payment_method_id": "33333333-3333-4333-8333-333333333333",
"payment_method_locked": true,
"server_time": "2026-08-31T18:10:00Z",
"expires_at": "2026-08-31T18:15:00Z",
"expires_in_seconds": 300,
"payment_open": true,
"redirect_url": "https://merchant.example/orders/1042",
"cancel_url": "https://merchant.example/cart",
"redirect_automatically": true,
"checkout_language": "en",
"project": {
"name": "Example project",
"checkout_title": "Complete your payment",
"checkout_description": "Send the exact amount shown.",
"theme": "system",
"accent_color": "#42e39b",
"logo_url": "/checkout-api/invoices/…/logo/…/image.png"
},
"store": { "name": "Online shop" },
"payment_methods": [
{
"id": "33333333-3333-4333-8333-333333333333",
"asset_key": "bip122:000000000019d6689c085ae165831e93/slip44:0",
"chain_slug": "bitcoin",
"chain_name": "Bitcoin",
"network": "mainnet",
"caip_network_id": "bip122:000000000019d6689c085ae165831e93",
"caip_asset_id": "bip122:000000000019d6689c085ae165831e93/slip44:0",
"asset_name": "Bitcoin",
"symbol": "BTC",
"asset_icon_url": "/assets/coingecko/bitcoin.png",
"asset_kind": "native",
"contract_address": null,
"token_standard": null,
"asset_decimals": 8,
"status": "partial",
"payable": true,
"finality_mode": "confirmations",
"required_confirmations": 1,
"expected_amount": "0.00046",
"expected_amount_atomic": "46000",
"minimum_payment_amount": "0.0004554",
"minimum_payment_amount_atomic": "45540",
"received_amount": "0.0002",
"received_amount_atomic": "20000",
"remaining_amount": "0.0002554",
"remaining_amount_atomic": "25540",
"confirmed_amount": "0",
"confirmed_amount_atomic": "0",
"destination_address": "bc1q…example",
"destination_tag": null,
"quote_expires_at": "2026-08-31T18:15:00Z",
"payment_uri": "bitcoin:bc1q…example?amount=0.00026",
"qr_url": "/checkout-api/invoices/…/payment-methods/…/qr.svg?sequence=3&amount_atomic=26000",
"address_explorer_name": "mempool.space",
"address_explorer_url": "https://mempool.space/address/…",
"transaction_count": 0,
"transactions_truncated": false,
"transactions": []
}
]
}
}Response fields
dataobject
exchange_rate_spread_percentdecimal stringalwaysEffective quote spread locked at creation, including a per-invoice override.
amount_statusamount statusalwaysnone, partial, paid, or overpaid. An explicitly allowed zero-amount invoice settles with none and no payment methods.
active_payment_method_idUUID | nullalwaysThe listed payment method that has received funds. Checkout remains on this method so an underpayment is not continued with an incompatible asset.
server_timeRFC 3339 timestampalwaysServer clock captured for this response; use it with expires_at to avoid customer-device clock skew.
expires_in_secondsintegeralwaysWhole seconds remaining at server_time, rounded up and clamped to zero.
payment_openbooleanalwaysTrue only while a new or processing invoice is before its deadline and has at least one payable method with an amount remaining.
projectobjectalways
name, checkout_title, checkout_description, theme, accent_color, and logo_url.
appearanceobjectalways
Effective presentation: frozen per-invoice override when supplied, otherwise the store's current design. Never changes financial fields or safety warnings.
inherit_default_storebooleanalwaysTrue when the project's default store supplies this appearance. False for independent stores and frozen invoice overrides.
invoice_overridebooleanalwaysTrue when checkout_appearance was supplied at invoice creation. Omitted/null keeps this false.
titlestringalwaysPlain merchant heading, top message and bottom message. intro replaces customer_message; old stored copy is preserved. Never evaluate as markup.
introstringalwaysPlain merchant heading, top message and bottom message. intro replaces customer_message; old stored copy is preserved. Never evaluate as markup.
outrostringalwaysPlain merchant heading, top message and bottom message. intro replaces customer_message; old stored copy is preserved. Never evaluate as markup.
customer_messagestringalwaysDeprecated compatibility alias of intro. Use intro for new integrations.
accent_colorstringalwaysStrict #RRGGBB colors. Optional colors are empty for automatic values; foreground contrast is calculated.
background_colorstringalwaysStrict #RRGGBB colors. Optional colors are empty for automatic values; foreground contrast is calculated.
card_colorstringalwaysStrict #RRGGBB colors. Optional colors are empty for automatic values; foreground contrast is calculated.
button_colorstringalwaysStrict #RRGGBB colors. Optional colors are empty for automatic values; foreground contrast is calculated.
logo_alignmentstringalwayssmall, medium or large; left or center. Images are contained, not cropped.
imagesobjectalwaysOptional logo_light, logo_dark and favicon URLs: scoped, same-origin normalized PNG images.
show_order_idbooleanalwaysOrder ID visibility, description below the title and initial order ID expansion. Amount remains visible; these are display controls, not data redaction.
show_descriptionbooleanalwaysOrder ID visibility, description below the title and initial order ID expansion. Amount remains visible; these are display controls, not data redaction.
details_expandedbooleanalwaysOrder ID visibility, description below the title and initial order ID expansion. Amount remains visible; these are display controls, not data redaction.
show_project_namebooleanalwaysMerchant 5.6.0+: header name visibility. Both default to true. Project/store identity remains available in JSON.
show_store_namebooleanalwaysMerchant 5.6.0+: header name visibility. Both default to true. Project/store identity remains available in JSON.
featured_chains[]arrayalwaysOrdered preferences, applied only to methods already present in the invoice. Missing or disabled methods are ignored.
featured_asset_ids[]arrayalwaysOrdered preferences, applied only to methods already present in the invoice. Missing or disabled methods are ignored.
default_asset_idUUID | nullalwaysSuggested initial method. A valid remembered customer preference or a method already receiving funds takes priority.
messagesobjectalways
Plain text for en, de, es, pt-BR, it, ru, zh-CN, fr, ko and ja, keyed by waiting, confirming, paid, underpaid and expired. English fallback for missing merchant messages. Supplementary; never replaces actual status. Merchant-written content is not automatically translated.Details
Plain text for en, de, es, pt-BR, it, ru, zh-CN, fr, ko and ja, keyed by waiting, confirming, paid, underpaid and expired. English fallback for missing merchant messages. Supplementary; never replaces actual status. Merchant-written content is not automatically translated.
support_emailstringalwaysOptional contact and HTTPS links, without URL credentials. External links open in a new window.
support_urlstringalwaysOptional contact and HTTPS links, without URL credentials. External links open in a new window.
terms_urlstringalwaysOptional contact and HTTPS links, without URL credentials. External links open in a new window.
privacy_urlstringalwaysOptional contact and HTTPS links, without URL credentials. External links open in a new window.
return_button_textstringalwaysOptional label only. Success/cancel targets and redirect policy still belong to the invoice.
show_return_buttonbooleanalwaysManual return/cancel button visibility; defaults to true. Applies to active and terminal states, not automatic success redirects. Available from merchant 8.1.4.
payment_methods[]object[]always
Checkout-safe payment methods.
payment_railonchain | lightningalwaysLightning remains a Bitcoin method, separate from on-chain BTC. Identify the choice by intent id and rail, not only asset_id.
bolt11string | nullalwaysSigned Lightning request; null for on-chain methods. Never pay after payable becomes false.
payment_hashstring | nullalwaysLightning payment hash for reconciliation, not a receiving address. Null for on-chain methods.
asset_idUUIDalwaysAsset UUID used by appearance preferences; distinct from this invoice's payment intent id.
caip_asset_idstring | nullalwaysCanonical exact asset identity, including a verified token contract or mint when applicable.
asset_icon_urlstring | nullalwaysSame-origin locally cached asset icon, or null when no verified CoinGecko mapping exists.
contract_addressstring | nullalwaysCanonical ERC-20 contract or SPL mint for tokens; null for native currency.
asset_decimalsintegeralwaysAtomic-unit precision: 11 for Lightning BTC millisatoshis, 8 for on-chain BTC satoshis.
payablebooleanalwaysTrue only when this exact method can currently accept payment; false for inactive methods after another asset receives funds.
expected_amountdecimalalwaysFull locked quote in display and actual on-chain units. Recognized fiat stablecoins use at most two quote decimals, always rounded up after spread; other assets use adaptive precision. Actual token decimals, received funds and partial-payment remainders stay exact. Use returned amounts unchanged.Details
Full locked quote in display and actual on-chain units. Recognized fiat stablecoins use at most two quote decimals, always rounded up after spread; other assets use adaptive precision. Actual token decimals, received funds and partial-payment remainders stay exact. Use returned amounts unchanged.
expected_amount_atomicinteger stringalwaysFull locked quote in display and actual on-chain units. Recognized fiat stablecoins use at most two quote decimals, always rounded up after spread; other assets use adaptive precision. Actual token decimals, received funds and partial-payment remainders stay exact. Use returned amounts unchanged.Details
Full locked quote in display and actual on-chain units. Recognized fiat stablecoins use at most two quote decimals, always rounded up after spread; other assets use adaptive precision. Actual token decimals, received funds and partial-payment remainders stay exact. Use returned amounts unchanged.
minimum_payment_amountdecimalalwaysAccepted settlement threshold after applying underpayment tolerance.
minimum_payment_amount_atomicinteger stringalwaysAccepted settlement threshold after applying underpayment tolerance.
remaining_amountdecimal stringalwaysExact display amount still needed to reach the accepted threshold, clamped to zero.
remaining_amount_atomicinteger stringalwaysShortfall to the accepted threshold in atomic units. This is not the requested payment amount: tolerance affects acceptance only.
destination_addressstringalwaysOn-chain destination and optional reference. For Lightning this is the payment hash with no tag; pay the bolt11/payment_uri instead.
destination_tagstring|nullalwaysOn-chain destination and optional reference. For Lightning this is the payment hash with no tag; pay the bolt11/payment_uri instead.
payment_uristring | nullalwaysChain-aware request: ERC-681, Solana Pay, native URI, or lightning:<bolt11>. Amount-bearing requests use the full expected amount minus received funds, never the tolerance threshold. Null when payable is false, including after a tolerated shortfall is accepted. Lightning QR encodes the full Lightning request, not the payment hash.Details
Chain-aware request: ERC-681, Solana Pay, native URI, or lightning:<bolt11>. Amount-bearing requests use the full expected amount minus received funds, never the tolerance threshold. Null when payable is false, including after a tolerated shortfall is accepted. Lightning QR encodes the full Lightning request, not the payment hash.
qr_urlpath | nullalwaysSequence- and exact-remainder-revisioned same-origin SVG QR path, or null when payable is false. The SVG is no-store.
transactions_truncatedbooleanalwaysTrue when transaction_count exceeds the returned recent transaction list.
transactions[]object[]always
Up to 10 most recent public, valid transactions. Exact received totals remain independent of this display bound.
Usage notes
- No bearer token is needed.
- Cache-Control is no-store and search indexing is disabled.
- Treat invoice_id as customer-facing capability data; avoid publishing it unnecessarily.
- asset_icon_url is a same-origin local asset; customer checkout never needs to contact CoinGecko to render it.
- When destination_tag is non-null, display and copy it beside the address: it is a required XRP destination tag, Stellar memo ID, or TON invoice comment and must be sent exactly.
- For verified tokens, asset_kind is token, contract_address identifies the exact ERC-20 contract or SPL mint, token_standard identifies the rail, and payment_uri carries that token identity.
Errors
GETStore checkout preview/invoice/preview/{project_id}Public
Renders the saved store appearance with an illustrative amount and real accepted-asset metadata. Switch between waiting, confirming, paid, underpaid and expired examples without creating payments.
Authentication & headers
Public
Path parameters
Query parameters
Request
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/invoice/preview/YOUR_PROJECT_ID?store_id=YOUR_STORE_ID&state=confirming" \
--output 'checkout-preview.html'// Node.js 18+ · run on your server, never in browser code.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://pay.example.com/invoice/preview/YOUR_PROJECT_ID?store_id=YOUR_STORE_ID&state=confirming", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
await writeFile("checkout-preview.html", Buffer.from(await response.arrayBuffer()));<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/invoice/preview/YOUR_PROJECT_ID?store_id=YOUR_STORE_ID&state=confirming");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
file_put_contents("checkout-preview.html", $response);# Python 3 · standard library; run on your server.
import json
from pathlib import Path
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://pay.example.com/invoice/preview/YOUR_PROJECT_ID?store_id=YOUR_STORE_ID&state=confirming",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
Path("checkout-preview.html").write_bytes(response.read())Response 200 text/html
Example response
<!doctype html>
<!-- Hosted branding preview; no invoice is created -->Usage notes
- The preview is branding-only and must never be sent to a customer as a payment request.
- No receiving address, payable QR, wallet action, redirect or payment polling. Examples do not change actual invoice status.
- The response is no-store, noindex, and cannot be embedded.
Errors
GETCheckout preview data/checkout-api/previews/{project_id}Public
Returns effective store appearance and safe accepted-asset metadata. payment_methods remains empty; preview_methods contains no payment addresses, quotes or private wallet data.
Authentication & headers
Public
Path parameters
Query parameters
store_idUUID,optionalMust belong to this project; mismatched IDs return 404. Unknown query fields are rejected.
Request
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID?store_id=YOUR_STORE_ID" \
--header 'Accept: application/json'// Node.js 18+ · run on your server, never in browser code.
const response = await fetch("https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID?store_id=YOUR_STORE_ID", {
method: "GET",
headers: {
"Accept": "application/json"
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID?store_id=YOUR_STORE_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ["Accept: application/json"],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Accept": "application/json"
}
request = Request("https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID?store_id=YOUR_STORE_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))Response 200 application/json
Example response
{
"data": {
"preview": true,
"invoice_id": "YOUR_PROJECT_ID",
"amount": "100.00",
"currency": "USD",
"project": {
"name": "Example project",
"checkout_title": "Complete your payment",
"checkout_description": "Choose a network and send the exact amount shown.",
"theme": "system",
"accent_color": "#42e39b",
"logo_url": "/checkout-api/previews/…/logo/…/image.png"
},
"appearance": {"inherit_default_store": true, "theme": "system", "accent_color": "#42E39B", "images": {}},
"preview_methods": [],
"payment_methods": []
}
}Response fields
dataobject
previewbooleaninvoice_idstringamountstringcurrencystringprojectobject
appearanceobject
inherit_default_storebooleanalwaysTrue when the project's default store supplies this appearance. False for independent stores and frozen invoice overrides.
invoice_overridebooleanalwaysTrue when checkout_appearance was supplied at invoice creation. Omitted/null keeps this false.
titlestringalwaysPlain merchant heading, top message and bottom message. intro replaces customer_message; old stored copy is preserved. Never evaluate as markup.
introstringalwaysPlain merchant heading, top message and bottom message. intro replaces customer_message; old stored copy is preserved. Never evaluate as markup.
outrostringalwaysPlain merchant heading, top message and bottom message. intro replaces customer_message; old stored copy is preserved. Never evaluate as markup.
customer_messagestringalwaysDeprecated compatibility alias of intro. Use intro for new integrations.
accent_colorstringalwaysStrict #RRGGBB colors. Optional colors are empty for automatic values; foreground contrast is calculated.
background_colorstringalwaysStrict #RRGGBB colors. Optional colors are empty for automatic values; foreground contrast is calculated.
card_colorstringalwaysStrict #RRGGBB colors. Optional colors are empty for automatic values; foreground contrast is calculated.
button_colorstringalwaysStrict #RRGGBB colors. Optional colors are empty for automatic values; foreground contrast is calculated.
logo_alignmentstringalwayssmall, medium or large; left or center. Images are contained, not cropped.
imagesobjectalwaysOptional logo_light, logo_dark and favicon URLs: scoped, same-origin normalized PNG images.
show_order_idbooleanalwaysOrder ID visibility, description below the title and initial order ID expansion. Amount remains visible; these are display controls, not data redaction.
show_descriptionbooleanalwaysOrder ID visibility, description below the title and initial order ID expansion. Amount remains visible; these are display controls, not data redaction.
details_expandedbooleanalwaysOrder ID visibility, description below the title and initial order ID expansion. Amount remains visible; these are display controls, not data redaction.
show_project_namebooleanalwaysMerchant 5.6.0+: header name visibility. Both default to true. Project/store identity remains available in JSON.
show_store_namebooleanalwaysMerchant 5.6.0+: header name visibility. Both default to true. Project/store identity remains available in JSON.
featured_chains[]arrayalwaysOrdered preferences, applied only to methods already present in the invoice. Missing or disabled methods are ignored.
featured_asset_ids[]arrayalwaysOrdered preferences, applied only to methods already present in the invoice. Missing or disabled methods are ignored.
default_asset_idUUID | nullalwaysSuggested initial method. A valid remembered customer preference or a method already receiving funds takes priority.
messagesobjectalways
Plain text for en, de, es, pt-BR, it, ru, zh-CN, fr, ko and ja, keyed by waiting, confirming, paid, underpaid and expired. English fallback for missing merchant messages. Supplementary; never replaces actual status. Merchant-written content is not automatically translated.Details
Plain text for en, de, es, pt-BR, it, ru, zh-CN, fr, ko and ja, keyed by waiting, confirming, paid, underpaid and expired. English fallback for missing merchant messages. Supplementary; never replaces actual status. Merchant-written content is not automatically translated.
support_emailstringalwaysOptional contact and HTTPS links, without URL credentials. External links open in a new window.
support_urlstringalwaysOptional contact and HTTPS links, without URL credentials. External links open in a new window.
terms_urlstringalwaysOptional contact and HTTPS links, without URL credentials. External links open in a new window.
privacy_urlstringalwaysOptional contact and HTTPS links, without URL credentials. External links open in a new window.
return_button_textstringalwaysOptional label only. Success/cancel targets and redirect policy still belong to the invoice.
show_return_buttonbooleanalwaysManual return/cancel button visibility; defaults to true. Applies to active and terminal states, not automatic success redirects. Available from merchant 8.1.4.
preview_methods[]arraypayment_methods[]arrayUsage notes
- No bearer token is accepted or needed.
- No invoice, destination, wallet, transaction, IPN, webhook, or merchant metadata is returned.
- Use the authenticated console to obtain the correct pay-domain preview link.
Errors
GETStore checkout image/checkout-api/invoices/{invoice_id}/appearance-images/{kind}/{revision}/image.pngPublic
Returns a normalized store logo or favicon belonging to this invoice. Use appearance.images URLs from checkout data.
Authentication & headers
Public
Path parameters
Request
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/appearance-images/logo_light/YOUR_IMAGE_REVISION/image.png" \
--output 'store-logo.png'// Node.js 18+ · run on your server, never in browser code.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/appearance-images/logo_light/YOUR_IMAGE_REVISION/image.png", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
await writeFile("store-logo.png", Buffer.from(await response.arrayBuffer()));<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/appearance-images/logo_light/YOUR_IMAGE_REVISION/image.png");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
file_put_contents("store-logo.png", $response);# Python 3 · standard library; run on your server.
import json
from pathlib import Path
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/appearance-images/logo_light/YOUR_IMAGE_REVISION/image.png",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
Path("store-logo.png").write_bytes(response.read())Response 200 image/png
Example response
(binary PNG response)Usage notes
- Use appearance.images from checkout JSON. Frozen invoice images keep working after their source store replaces or removes an upload. Explicitly removed, wrong-invoice, wrong-kind and unknown revisions return 404; a snapshot never falls back to a current store image.
- Without an invoice override, the current effective store image is used and replaced/removed revisions return 404. PNG only, nosniff and private caching.
- Store image uploads accept bounded PNG, JPEG or WebP in the authenticated console; never SVG, HTML or remote image URLs.
Errors
GETStore preview image/checkout-api/previews/{project_id}/stores/{store_id}/appearance-images/{kind}/{revision}/image.pngPublic
Returns a normalized preview image only for the matching project, store, kind and current revision.
Authentication & headers
Public
Path parameters
Request
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/appearance-images/logo_light/YOUR_IMAGE_REVISION/image.png" \
--output 'store-preview-logo.png'// Node.js 18+ · run on your server, never in browser code.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/appearance-images/logo_light/YOUR_IMAGE_REVISION/image.png", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
await writeFile("store-preview-logo.png", Buffer.from(await response.arrayBuffer()));<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/appearance-images/logo_light/YOUR_IMAGE_REVISION/image.png");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
file_put_contents("store-preview-logo.png", $response);# Python 3 · standard library; run on your server.
import json
from pathlib import Path
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/appearance-images/logo_light/YOUR_IMAGE_REVISION/image.png",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
Path("store-preview-logo.png").write_bytes(response.read())Response 200 image/png
Example response
(binary PNG response)Usage notes
- Use appearance.images from preview data. Unknown or mismatched IDs return 404. No wallet or payment information is exposed.
Errors
GETRevisioned preview logo/checkout-api/previews/{project_id}/logo/{revision}/image.pngPublic
Returns the normalized project logo only when the project and cache-safe logo revision match. Use project.logo_url from preview data instead of constructing this URL.
Authentication & headers
Public
Path parameters
Request
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID/logo/YOUR_LOGO_REVISION/image.png" \
--output 'checkout-preview-logo.png'// Node.js 18+ · run on your server, never in browser code.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID/logo/YOUR_LOGO_REVISION/image.png", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
await writeFile("checkout-preview-logo.png", Buffer.from(await response.arrayBuffer()));<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID/logo/YOUR_LOGO_REVISION/image.png");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
file_put_contents("checkout-preview-logo.png", $response);# Python 3 · standard library; run on your server.
import json
from pathlib import Path
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID/logo/YOUR_LOGO_REVISION/image.png",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
Path("checkout-preview-logo.png").write_bytes(response.read())Response 200 image/png
Example response
(binary PNG response)Usage notes
- Unknown projects and stale logo revisions return invoice_not_found without revealing which component was absent.
- The successful revisioned image is immutable and may be cached.
Errors
GETPayment QR image/checkout-api/invoices/{invoice_id}/payment-methods/{intent_id}/qr.svgPublic
Generates a 512×512 SVG QR for the exact chain-aware payment payload of an invoice payment method.
Authentication & headers
Public
Path parameters
Request
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/payment-methods/YOUR_INTENT_ID/qr.svg" \
--output 'payment-qr.svg'// Node.js 18+ · run on your server, never in browser code.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/payment-methods/YOUR_INTENT_ID/qr.svg", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
await writeFile("payment-qr.svg", Buffer.from(await response.arrayBuffer()));<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/payment-methods/YOUR_INTENT_ID/qr.svg");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
file_put_contents("payment-qr.svg", $response);# Python 3 · standard library; run on your server.
import json
from pathlib import Path
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/payment-methods/YOUR_INTENT_ID/qr.svg",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
Path("payment-qr.svg").write_bytes(response.read())Response 200 image/svg+xml
Example response
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">…</svg>Usage notes
- No bearer token is needed.
- Use the sequence- and remainder-revisioned qr_url returned by checkout JSON; the SVG is private and no-store.
- After a partial payment it requests the exact remaining amount and stays locked to that asset.
- Returns 409 after expiry, completion, or when another method is active; returns payment_qr_unavailable (422) if the request is too large to encode.
Errors
GETRevisioned checkout logo/checkout-api/invoices/{invoice_id}/logo/{revision}/image.pngPublic
Returns the normalized project checkout logo only when the invoice and current logo revision match. Prefer the project.logo_url returned by checkout JSON rather than constructing this route.
Authentication & headers
Public
Path parameters
Request
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/logo/YOUR_LOGO_REVISION/image.png" \
--output 'checkout-logo.png'// Node.js 18+ · run on your server, never in browser code.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/logo/YOUR_LOGO_REVISION/image.png", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
await writeFile("checkout-logo.png", Buffer.from(await response.arrayBuffer()));<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/logo/YOUR_LOGO_REVISION/image.png");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
file_put_contents("checkout-logo.png", $response);# Python 3 · standard library; run on your server.
import json
from pathlib import Path
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/logo/YOUR_LOGO_REVISION/image.png",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
Path("checkout-logo.png").write_bytes(response.read())Response 200 image/png
Example response
(binary PNG response)Usage notes
- No bearer token is needed.
- Public cache lifetime is one year with immutable because the revision is content-addressing state.
- Unknown/mismatched revisions return invoice_not_found.
Errors
Reference for Wholly Crypto 8.3.2. For your installed version, open Settings → API access → Documentation in your console. View releases.