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+.
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.
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 | Payment met settlement policy |
| 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. 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.
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. |
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.
Partial, late and other payment states
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; 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 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 authenticated invoice, match the stored order, project/store, amount and currency, and fulfil only after settlement. Make order updates idempotent and never apply an older sequence over a newer one. Reopening/reconciliation can change status; sequence, not a fixed status ranking, orders updates.
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.
- 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))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. |
| 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. Checks include wallet backup/activation, scanner and two independent healthy providers of the required endpoint role. |
| 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 two independent healthy mainnet endpoints; 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}Checkout
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/healthzGETList 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.
- 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.
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Project assigned to this credential. |
| status | query string | open (default), resolved, or all. |
| reason | query string | underpaid, overpaid, late, reorged, ambiguous, delivery_failed, disabled_method, or expired_method. |
| search | query string | Up to 100 characters: invoice ID, order, customer or store. |
| store_id | query UUID | Optional store filter. |
| page | query integer | 1–40001. Fixed 25 cases per page. |
Exception queue response
| Field | Type | Presence | Description |
|---|---|---|---|
| data | ExceptionRow[] | always | Newest updated cases first. Use invoice_id, not internal id, in merchant detail URLs. |
| pagination | object | always | page (1–40001), per_page (25), total matching rows, has_more. |
| counts | object | always | open and resolved totals for the whole project, independent of the current filters. |
ExceptionRow
| Field | Type | Presence | Description |
|---|---|---|---|
| id / invoice_id | UUID | always | Internal record ID / customer-facing invoice UUID. invoice_id matches callback payloads. |
| store_id / store_name | UUID / string | always | Owning store. |
| order_id / email | string | null | always | Private merchant order reference and customer email. |
| amount / currency | decimal string / string | always | Original fiat invoice amount and currency. |
| invoice_status | invoice status | always | Current payment lifecycle status. |
| status / reasons | open|resolved / string[] | always | Case state and the exception types listed in the reason filter. |
| revision / updated_at | integer / timestamp | always | Current review revision and update time. |
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))Example response · 200 application/json
{"data":[],"pagination":{"page":1,"per_page":25,"total":0,"has_more":false},"counts":{"open":0,"resolved":0}}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.
- 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.
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Assigned project. |
| invoice_id | path UUID | Public invoice UUID, not internal id. |
| page | query integer | Decision-history page, starting at 1; 25 decisions per page. |
Reconciliation response
| Field | Type | Presence | Description |
|---|---|---|---|
| invoice | InvoiceDetail | always | Full merchant invoice: summary fields, private metadata and payment_intents. Not wrapped in data. |
| case | object | null | always | Current case with status, reasons, revision and timestamps; null when no exception exists. Internal evidence is excluded. |
| methods | object[] | always | 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[] | always | Newest 25 decisions for this page: id, action, note, actor, result, created_at. |
| history_pagination | object | always | page, per_page (25), total. Only decision history is paginated by page. |
| refunds | object[] | always | 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[] | always | 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[] | always | Newest 50: id, kind, status, attempts, response_status, error, next_attempt_at, event_type and created_at. No callback secrets. |
Invoice summary
| Field | Type | Presence | Description |
|---|---|---|---|
| id | UUID | always | Internal invoice UUID. Do not use it in merchant detail or checkout paths. |
| invoice_id | UUID | always | Public invoice UUID used by merchant detail and checkout paths. |
| project_id | UUID | always | Owning project. |
| store_id | UUID | always | Owning store. |
| source | manual | api | always | How the invoice was created. |
| order_id | string | null | always | Merchant order reference. |
| string | null | always | Merchant-only customer email. Never returned by public checkout. | |
| customer_name | string | null | always | Derived display name from private firstname, lastname, and company metadata. |
| customer_address | string | null | always | Derived one-line merchant address from private company, street, street2, zip, city, country, countryiso2, and vatid metadata. |
| description | string | null | always | Customer-facing description. |
| amount | decimal string | always | Canonical invoice amount. |
| currency | string | always | Normalized invoice currency/asset code. |
| exchange_rate_spread_percent | decimal string | always | Locked quote spread: the creation override, or the store default when omitted. Applied before upward rounding; never changes on this invoice. |
| underpayment_tolerance_percent | decimal string | always | Immutable accepted shortfall percentage snapshotted when the invoice was created. |
| status | invoice status | always | new, processing, settled, expired, invalid, or cancelled. |
| amount_status | amount status | always | none, partial, paid, or overpaid. An explicitly allowed zero-amount invoice settles with none and no payment methods. |
| timing_status | timing status | always | on_time or late. |
| resolution | resolution | always | automatic, manually_settled, or manually_invalidated. |
| sequence | integer | always | Monotonic invoice state sequence, starting at 1. |
| winning_payment_intent_id | UUID | null | always | Payment method that resolved the invoice, when selected. |
| expires_at | RFC 3339 timestamp | always | Quote/payment deadline. |
| monitoring_expires_at | RFC 3339 timestamp | always | Latest configured late-monitoring cutoff across payment methods. |
| settled_at | timestamp | null | always | Settlement time when settled. |
| cancelled_at | timestamp | null | always | Cancellation time when cancelled. |
| archived_at | timestamp | null | always | Archival time when archived. |
| created_at | RFC 3339 timestamp | always | Creation time. |
| updated_at | RFC 3339 timestamp | always | Last state update time. |
Invoice detail additions
| Field | Type | Presence | Description |
|---|---|---|---|
| ipn_url | string | null | always | Effective per-invoice IPN target. Merchant response only; omitted from public checkout. |
| redirect_url | string | null | always | Effective success URL used after settlement. |
| cancel_url | string | null | always | Effective return URL used when checkout ends without successful payment. |
| redirect_automatically | boolean | always | Whether checkout should redirect automatically after success. |
| checkout_language | string | always | Effective checkout language tag. |
| metadata | object | always | Merchant metadata. Never returned by public checkout. |
| payment_intents | PaymentIntent[] | always | Quoted payment methods and monitoring state. |
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))Example response · 200 application/json
{"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":[]}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.
- No bearer token is needed.
- Only the managed API hostname guarantees this exact root response.
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))Example response · 200 application/json
{
"service": "Wholly Crypto API",
"status": "ready",
"version": "v1"
}GETService health/healthzPublic
Checks application reachability and a two-second database ping. Use for monitoring, not as a substitute for invoice status.
- No bearer token is needed.
- The version value is the running package version, not the API path version.
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))Example response · 200 healthy; 503 database unavailable
{
"status": "ok",
"database": "ok",
"version": "0.1.0"
}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. Invoice creation separately fails closed unless two independent healthy exact-role endpoints are available.
- 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.
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | recommended | application/json |
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Enabled project assigned to the credential. |
PaymentAsset
| Field | Type | Presence | Description |
|---|---|---|---|
| id | UUID | always | Durable payment-asset identifier used by project and store policy routes. |
| asset_key | string | always | Canonical CAIP-style native or contract asset identity. |
| chain_slug / network | string | always | Wholly Crypto chain identifier and configured network. |
| caip_network_id / caip_asset_id | string / string|null | always | Canonical network and asset identities. |
| asset_kind | native | token | always | Whether settlement uses the chain currency or a verified contract/mint. |
| payment_rail | string | always | Runtime rail: utxo, evm-native, solana-native, account-native, privacy-native, or token-transfer. |
| symbol / name / decimals | string / string / integer | always | Display identity and exact atomic-unit precision. |
| contract_address | string | null | always | Canonical ERC-20 contract or SPL mint for tokens; null for native assets. |
| coingecko_id | string | null | always | Discovery/pricing identity. Null for custom contracts; never infer a market price from their ticker. CoinGecko metadata alone never makes a token selectable. |
| custom_token | boolean | always | Custom on-chain-verified contract with project-scoped fixed USD or selected DEX pool pricing. |
| icon_path | path | null | always | Locally cached token icon when available. |
| token_standard | erc20 | spl-token | null | always | Verified runtime token standard; null for native assets. |
| metadata_verified_at | timestamp | null | always | On-chain metadata verification time for promoted tokens. |
| payment_supported / scanner_ready / balance_ready | boolean | always | Build-time registry gates. scanner_ready currently means the payment scanner runtime is installed; invoice creation separately requires two healthy independent exact-role endpoints. balance_ready is true only for implemented balance adapters. |
| default_finality_mode | confirmations | finalized | always | Default finality model inherited by a new project policy. |
| default_required_confirmations / default_monitoring_minutes | integer | always | Default confirmation and monitoring policy. |
ProjectPaymentAsset
| Field | Type | Presence | Description |
|---|---|---|---|
| asset | PaymentAsset | always | Durable native or verified token asset. |
| policy | ProjectAssetPolicy | null | always | 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. |
| wallet | WalletSummary | null | always | The chain's non-custodial project wallet. Tokens share their chain-native wallet. |
| wallet_readiness | readiness enum | always | 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_readiness | ReceiveReadiness | null | 5.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. |
WalletSummary
| Field | Type | Presence | Description |
|---|---|---|---|
| id / project_id / native_asset_id | UUID | always | Wallet, owner project, and chain-native asset identifiers. |
| chain_slug / network | string | always | Wallet chain and network. |
| asset_symbol / asset_name | string | always | Chain-native display identity. |
| status | pending | active | disabled | error | always | Operational wallet state. |
| label | string | always | Operator label. |
| public_key / primary_address | string | null | always | Public wallet identity; no seed phrase or private key is exposed. |
| derivation_scheme / address_format | string | null | always | Address policy and format. |
| backup_confirmed_at | timestamp | null | always | Non-null after the operator confirms recovery backup. |
| activation_required / activation_verified_at | boolean / timestamp|null | always | XRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and two independent strict providers verify that exact account. The durable proof does not expire; ordinary live scanner health remains a separate gate. |
| receive_readiness | ReceiveReadiness | null | 5.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. |
| monero_wallet_rpc | MoneroWalletRpcBinding | null | always | 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. |
| last_secret_revealed_at / secret_reveal_count | timestamp|null / integer | always | Console-side secret disclosure audit metadata. |
| next_receive_index | integer | always | Next reserved child-address index. |
| last_scanned_height / last_scanned_at / last_error | integer|null / timestamp|null / string|null | always | Wallet scanner state. |
| balances | WalletAssetBalance[] | 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. |
| total_value_usd | decimal string | null | always | Advisory sum of balances with a current USD price. |
| balance_status | pending | refreshing | fresh | stale | error | unknown | always | Aggregated cache freshness; unknown is a defensive fallback and none of these states proves invoice settlement. |
| balance_checked_at | timestamp | null | always | Oldest relevant successful balance check represented by the aggregate. |
| recent_payments | WalletRecentPayment[] | always | Up to three newest valid detected, confirming, or final observations attributed to this exact wallet. |
| created_at / updated_at | RFC 3339 timestamp | always | Creation and last wallet update time. |
ReceiveReadiness
| Field | Type | Presence | Description |
|---|---|---|---|
| ready | boolean | always | Receive setup checks pass. Does not describe spending readiness, gas, balance refresh, or a guaranteed future quote. |
| checked_at | timestamp | always | Assessment time. No network request or address allocation is made by a listing. |
| issues | PaymentMethodIssue[] | always | Empty when ready; otherwise the current actionable blocker for this asset. |
PaymentMethodIssue
| Field | Type | Presence | Description |
|---|---|---|---|
| chain_slug / asset_id / asset_ticker | string / UUID / string | when known | Identifies the affected chain and asset. Lightning can omit asset_id. |
| reason_code | string | always | 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. |
| message / action | string | when available | 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_role | string | null | on-chain | Scanner-compatible role, for example tron-indexer. A healthy general TRON node is not an indexer. |
| healthy_endpoints | integer | on-chain | Healthy matching endpoints, not the independent-provider count. |
| usable_independent_providers / required_independent_providers | integer | on-chain | Usable independent verification slots, capped at the required two. 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_at | timestamp | null | on-chain | Latest matching endpoint health check, separate from assessment time. |
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))Example response · 200 application/json
{
"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, "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" }] }
}
]
}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.
- The body is a full policy replacement and rejects unknown fields.
- Project enablement does not itself select the asset for any store.
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Content-Type | required | application/json |
| Accept | recommended | application/json |
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Enabled project assigned to the credential. |
| asset_id | path UUID | Asset id returned by the project asset list or token registration. |
Project asset policy update
| Field | Type | Presence | Description |
|---|---|---|---|
| enabled | boolean | required | Enables or disables the asset for the project. The native chain must be enabled before any token. |
| finality_mode | confirmations | finalized | required | Finality policy supported by the asset rail. finalized requires required_confirmations=1. |
| required_confirmations | integer | required | 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. |
| monitoring_minutes | integer | required | 1–10,080 minute polling window while an invoice is active. |
| late_monitoring_days | integer | required | 0–3,650 days of monitoring after invoice expiry. |
PaymentAsset
| Field | Type | Presence | Description |
|---|---|---|---|
| id | UUID | always | Durable payment-asset identifier used by project and store policy routes. |
| asset_key | string | always | Canonical CAIP-style native or contract asset identity. |
| chain_slug / network | string | always | Wholly Crypto chain identifier and configured network. |
| caip_network_id / caip_asset_id | string / string|null | always | Canonical network and asset identities. |
| asset_kind | native | token | always | Whether settlement uses the chain currency or a verified contract/mint. |
| payment_rail | string | always | Runtime rail: utxo, evm-native, solana-native, account-native, privacy-native, or token-transfer. |
| symbol / name / decimals | string / string / integer | always | Display identity and exact atomic-unit precision. |
| contract_address | string | null | always | Canonical ERC-20 contract or SPL mint for tokens; null for native assets. |
| coingecko_id | string | null | always | Discovery/pricing identity. Null for custom contracts; never infer a market price from their ticker. CoinGecko metadata alone never makes a token selectable. |
| custom_token | boolean | always | Custom on-chain-verified contract with project-scoped fixed USD or selected DEX pool pricing. |
| icon_path | path | null | always | Locally cached token icon when available. |
| token_standard | erc20 | spl-token | null | always | Verified runtime token standard; null for native assets. |
| metadata_verified_at | timestamp | null | always | On-chain metadata verification time for promoted tokens. |
| payment_supported / scanner_ready / balance_ready | boolean | always | Build-time registry gates. scanner_ready currently means the payment scanner runtime is installed; invoice creation separately requires two healthy independent exact-role endpoints. balance_ready is true only for implemented balance adapters. |
| default_finality_mode | confirmations | finalized | always | Default finality model inherited by a new project policy. |
| default_required_confirmations / default_monitoring_minutes | integer | always | Default confirmation and monitoring policy. |
ProjectPaymentAsset
| Field | Type | Presence | Description |
|---|---|---|---|
| asset | PaymentAsset | always | Durable native or verified token asset. |
| policy | ProjectAssetPolicy | null | always | 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. |
| wallet | WalletSummary | null | always | The chain's non-custodial project wallet. Tokens share their chain-native wallet. |
| wallet_readiness | readiness enum | always | 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_readiness | ReceiveReadiness | null | 5.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. |
ReceiveReadiness
| Field | Type | Presence | Description |
|---|---|---|---|
| ready | boolean | always | Receive setup checks pass. Does not describe spending readiness, gas, balance refresh, or a guaranteed future quote. |
| checked_at | timestamp | always | Assessment time. No network request or address allocation is made by a listing. |
| issues | PaymentMethodIssue[] | always | Empty when ready; otherwise the current actionable blocker for this asset. |
PaymentMethodIssue
| Field | Type | Presence | Description |
|---|---|---|---|
| chain_slug / asset_id / asset_ticker | string / UUID / string | when known | Identifies the affected chain and asset. Lightning can omit asset_id. |
| reason_code | string | always | 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. |
| message / action | string | when available | 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_role | string | null | on-chain | Scanner-compatible role, for example tron-indexer. A healthy general TRON node is not an indexer. |
| healthy_endpoints | integer | on-chain | Healthy matching endpoints, not the independent-provider count. |
| usable_independent_providers / required_independent_providers | integer | on-chain | Usable independent verification slots, capped at the required two. 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_at | timestamp | null | on-chain | Latest matching endpoint health check, separate from assessment time. |
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))Example response · 200 application/json
{
"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" }
]
}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.
- 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.
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | recommended | application/json |
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Enabled project assigned to the credential. |
| chain_slug | query string | Required supported EVM chain slug or solana. |
| q | query string | Optional name, symbol, CoinGecko id, contract, or mint substring; at most 80 characters. |
| limit | query integer | Optional 1–100; defaults to 50. |
TokenCandidate
| Field | Type | Presence | Description |
|---|---|---|---|
| coingecko_id | string | always | CoinGecko discovery identity used by the registration request. |
| chain_slug | string | always | Matched Wholly Crypto chain. |
| symbol / name | string | always | Catalog display identity. |
| contract_address | string | always | Matched contract or mint; it is verified on-chain before registration. |
| market_cap_rank | integer | null | always | Discovery rank, not a trust or payment-readiness signal. |
| icon_path | path | always | Locally cached CoinGecko icon path. |
| current_price_usd | decimal string | null | always | Advisory cached USD price. |
| token_standard | erc20 | spl-token | always | Token standard supported by the selected chain adapter. |
| scanner_ready | boolean | always | True only for candidates on a token rail implemented by this build. |
| registered_asset_id | UUID | null | always | Existing durable asset when already promoted. |
| project_enabled | boolean | always | Whether the registered asset is enabled for this project. |
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))Example response · 200 application/json
{
"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
}
]
}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.
- 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.
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Content-Type | required | application/json |
| Accept | recommended | application/json |
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Enabled project assigned to the credential. |
Token registration body
| Field | Type | Presence | Description |
|---|---|---|---|
| chain_slug | string | required | ethereum, base, bnb-chain, hyperliquid, avalanche, polygon, arbitrum, optimism, or solana. |
| coingecko_id | string | required | 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. |
| enabled | boolean | optional | Project policy state after verification; defaults to true. |
RegisteredTokenAsset
| Field | Type | Presence | Description |
|---|---|---|---|
| asset_id | UUID | always | Durable payment asset identifier. |
| chain_slug / coingecko_id | string | always | Verified chain and retained discovery/pricing identity. |
| contract_address | string | always | Canonical verified contract or mint. |
| token_standard | erc20 | spl-token | always | Verified runtime token standard. |
| symbol / name / decimals | string / string / integer | always | Promoted display identity and exact precision. |
| enabled | boolean | always | Initial project policy state. |
| metadata_verified_at | RFC 3339 timestamp | always | On-chain verification time. |
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))Example response · 201 application/json
{
"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"
}
}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.
- 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.
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Assigned project. |
| chain_slug | query string | Supported EVM token chain or solana. |
| contract_address | query string | Exact ERC-20 contract or classic SPL mint. |
CustomDexPool
| Field | Type | Presence | Description |
|---|---|---|---|
| pair_address / dex_id / quote_symbol | string | always | Exact pool identifier, exchange ID (e.g. uniswap/pancakeswap), and display-only paired ticker. |
| price_usd / liquidity_usd | decimal string | always | USD 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_at | RFC 3339 timestamp | always | When the server retrieved the provider observation, not the timestamp of an on-chain trade. |
| url | HTTPS URL | always | Validated DEX Screener link to this pool. |
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))Example response · 200 application/json
{"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"}]}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.
- 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.
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Content-Type | required | application/json |
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Project assigned to this write-capable credential. |
Custom token registration
| Field | Type | Presence | Description |
|---|---|---|---|
| chain_slug | string | required | ethereum, base, bnb-chain, hyperliquid, avalanche, polygon, arbitrum, optimism, or solana. Fixed for this contract. |
| contract_address | string | required | 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. |
| name / symbol | string / string | required | 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_mode | fixed | dex | optional | Defaults to fixed for backwards compatibility. DEX uses a specific pool discovered for the exact chain and contract. |
| price_usd | decimal string | fixed mode | Fixed USD value of ONE token, positive, at most 30 decimals, maximum 1000000000000000000000000. No exponent or floats. Omit in dex mode. |
| dex_pair_address | string | dex mode | 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. |
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))Example response · 200 application/json
{"data":{"asset_id":"44444444-4444-4444-8444-444444444444"}}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.
- 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.
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | recommended | application/json |
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Project assigned to the credential; it may be paused. |
| store_id | path UUID | Store belonging to project_id; it may be paused. |
PaymentAsset
| Field | Type | Presence | Description |
|---|---|---|---|
| id | UUID | always | Durable payment-asset identifier used by project and store policy routes. |
| asset_key | string | always | Canonical CAIP-style native or contract asset identity. |
| chain_slug / network | string | always | Wholly Crypto chain identifier and configured network. |
| caip_network_id / caip_asset_id | string / string|null | always | Canonical network and asset identities. |
| asset_kind | native | token | always | Whether settlement uses the chain currency or a verified contract/mint. |
| payment_rail | string | always | Runtime rail: utxo, evm-native, solana-native, account-native, privacy-native, or token-transfer. |
| symbol / name / decimals | string / string / integer | always | Display identity and exact atomic-unit precision. |
| contract_address | string | null | always | Canonical ERC-20 contract or SPL mint for tokens; null for native assets. |
| coingecko_id | string | null | always | Discovery/pricing identity. Null for custom contracts; never infer a market price from their ticker. CoinGecko metadata alone never makes a token selectable. |
| custom_token | boolean | always | Custom on-chain-verified contract with project-scoped fixed USD or selected DEX pool pricing. |
| icon_path | path | null | always | Locally cached token icon when available. |
| token_standard | erc20 | spl-token | null | always | Verified runtime token standard; null for native assets. |
| metadata_verified_at | timestamp | null | always | On-chain metadata verification time for promoted tokens. |
| payment_supported / scanner_ready / balance_ready | boolean | always | Build-time registry gates. scanner_ready currently means the payment scanner runtime is installed; invoice creation separately requires two healthy independent exact-role endpoints. balance_ready is true only for implemented balance adapters. |
| default_finality_mode | confirmations | finalized | always | Default finality model inherited by a new project policy. |
| default_required_confirmations / default_monitoring_minutes | integer | always | Default confirmation and monitoring policy. |
StorePaymentAsset
| Field | Type | Presence | Description |
|---|---|---|---|
| asset | PaymentAsset | always | Project-visible native or verified token asset. |
| project_policy | ProjectAssetPolicy | null | always | Parent project policy. |
| selected | boolean | always | Whether this method is part of the store's saved desired configuration. It is offered on new invoices only when its project policy, wallet, and runtime readiness gates also pass. |
| display_order | integer | null | always | Store checkout order when selected. |
| confirmation_policy | StoreConfirmationPolicy | null | always | Effective store policy for a project-configured asset. Null when no project policy exists. |
| wallet | WalletSummary | null | always | Chain wallet shared by native and token assets. |
| wallet_readiness | readiness enum | always | Wallet/policy status only; use receive_readiness for scanner prerequisites. |
| receive_readiness | ReceiveReadiness | null | 5.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. |
StoreConfirmationPolicy
| Field | Type | Presence | Description |
|---|---|---|---|
| finality_mode | confirmations | finalized | always | Whether settlement uses a configurable block count or network finality. |
| project_required_confirmations | integer | always | Current project default used by future invoices when no store override is set. |
| override_required_confirmations | integer | null | always | Store-specific count, or null to inherit the project default. |
| effective_required_confirmations | integer | always | Count that new invoices for this store and asset will snapshot. |
| editable | boolean | always | False for finalized networks whose finality policy cannot be overridden. |
| minimum_required_confirmations | integer | always | Inclusive chain-aware lower bound; 0 is exposed only on rails that support detection-time acceptance. |
| maximum_required_confirmations | integer | always | Inclusive chain-aware upper bound. |
WalletSummary
| Field | Type | Presence | Description |
|---|---|---|---|
| id / project_id / native_asset_id | UUID | always | Wallet, owner project, and chain-native asset identifiers. |
| chain_slug / network | string | always | Wallet chain and network. |
| asset_symbol / asset_name | string | always | Chain-native display identity. |
| status | pending | active | disabled | error | always | Operational wallet state. |
| label | string | always | Operator label. |
| public_key / primary_address | string | null | always | Public wallet identity; no seed phrase or private key is exposed. |
| derivation_scheme / address_format | string | null | always | Address policy and format. |
| backup_confirmed_at | timestamp | null | always | Non-null after the operator confirms recovery backup. |
| activation_required / activation_verified_at | boolean / timestamp|null | always | XRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and two independent strict providers verify that exact account. The durable proof does not expire; ordinary live scanner health remains a separate gate. |
| receive_readiness | ReceiveReadiness | null | 5.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. |
| monero_wallet_rpc | MoneroWalletRpcBinding | null | always | 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. |
| last_secret_revealed_at / secret_reveal_count | timestamp|null / integer | always | Console-side secret disclosure audit metadata. |
| next_receive_index | integer | always | Next reserved child-address index. |
| last_scanned_height / last_scanned_at / last_error | integer|null / timestamp|null / string|null | always | Wallet scanner state. |
| balances | WalletAssetBalance[] | 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. |
| total_value_usd | decimal string | null | always | Advisory sum of balances with a current USD price. |
| balance_status | pending | refreshing | fresh | stale | error | unknown | always | Aggregated cache freshness; unknown is a defensive fallback and none of these states proves invoice settlement. |
| balance_checked_at | timestamp | null | always | Oldest relevant successful balance check represented by the aggregate. |
| recent_payments | WalletRecentPayment[] | always | Up to three newest valid detected, confirming, or final observations attributed to this exact wallet. |
| created_at / updated_at | RFC 3339 timestamp | always | Creation and last wallet update time. |
ReceiveReadiness
| Field | Type | Presence | Description |
|---|---|---|---|
| ready | boolean | always | Receive setup checks pass. Does not describe spending readiness, gas, balance refresh, or a guaranteed future quote. |
| checked_at | timestamp | always | Assessment time. No network request or address allocation is made by a listing. |
| issues | PaymentMethodIssue[] | always | Empty when ready; otherwise the current actionable blocker for this asset. |
PaymentMethodIssue
| Field | Type | Presence | Description |
|---|---|---|---|
| chain_slug / asset_id / asset_ticker | string / UUID / string | when known | Identifies the affected chain and asset. Lightning can omit asset_id. |
| reason_code | string | always | 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. |
| message / action | string | when available | 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_role | string | null | on-chain | Scanner-compatible role, for example tron-indexer. A healthy general TRON node is not an indexer. |
| healthy_endpoints | integer | on-chain | Healthy matching endpoints, not the independent-provider count. |
| usable_independent_providers / required_independent_providers | integer | on-chain | Usable independent verification slots, capped at the required two. 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_at | timestamp | null | on-chain | Latest matching endpoint health check, separate from assessment time. |
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))Example response · 200 application/json
{
"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 }
}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.
- 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.
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Content-Type | required | application/json |
| Accept | recommended | application/json |
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Project assigned to the credential; it may be paused. |
| store_id | path UUID | Store belonging to project_id; it may be paused. |
Store payment asset selection body
| Field | Type | Presence | Description |
|---|---|---|---|
| assets | StoreAssetSelection[] | required | Complete replacement list, at most 64 entries. Each entry contains a unique asset_id and unique display_order from 0 through 10,000. |
PaymentAsset
| Field | Type | Presence | Description |
|---|---|---|---|
| id | UUID | always | Durable payment-asset identifier used by project and store policy routes. |
| asset_key | string | always | Canonical CAIP-style native or contract asset identity. |
| chain_slug / network | string | always | Wholly Crypto chain identifier and configured network. |
| caip_network_id / caip_asset_id | string / string|null | always | Canonical network and asset identities. |
| asset_kind | native | token | always | Whether settlement uses the chain currency or a verified contract/mint. |
| payment_rail | string | always | Runtime rail: utxo, evm-native, solana-native, account-native, privacy-native, or token-transfer. |
| symbol / name / decimals | string / string / integer | always | Display identity and exact atomic-unit precision. |
| contract_address | string | null | always | Canonical ERC-20 contract or SPL mint for tokens; null for native assets. |
| coingecko_id | string | null | always | Discovery/pricing identity. Null for custom contracts; never infer a market price from their ticker. CoinGecko metadata alone never makes a token selectable. |
| custom_token | boolean | always | Custom on-chain-verified contract with project-scoped fixed USD or selected DEX pool pricing. |
| icon_path | path | null | always | Locally cached token icon when available. |
| token_standard | erc20 | spl-token | null | always | Verified runtime token standard; null for native assets. |
| metadata_verified_at | timestamp | null | always | On-chain metadata verification time for promoted tokens. |
| payment_supported / scanner_ready / balance_ready | boolean | always | Build-time registry gates. scanner_ready currently means the payment scanner runtime is installed; invoice creation separately requires two healthy independent exact-role endpoints. balance_ready is true only for implemented balance adapters. |
| default_finality_mode | confirmations | finalized | always | Default finality model inherited by a new project policy. |
| default_required_confirmations / default_monitoring_minutes | integer | always | Default confirmation and monitoring policy. |
StorePaymentAsset
| Field | Type | Presence | Description |
|---|---|---|---|
| asset | PaymentAsset | always | Project-visible native or verified token asset. |
| project_policy | ProjectAssetPolicy | null | always | Parent project policy. |
| selected | boolean | always | Whether this method is part of the store's saved desired configuration. It is offered on new invoices only when its project policy, wallet, and runtime readiness gates also pass. |
| display_order | integer | null | always | Store checkout order when selected. |
| confirmation_policy | StoreConfirmationPolicy | null | always | Effective store policy for a project-configured asset. Null when no project policy exists. |
| wallet | WalletSummary | null | always | Chain wallet shared by native and token assets. |
| wallet_readiness | readiness enum | always | Wallet/policy status only; use receive_readiness for scanner prerequisites. |
| receive_readiness | ReceiveReadiness | null | 5.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. |
StoreConfirmationPolicy
| Field | Type | Presence | Description |
|---|---|---|---|
| finality_mode | confirmations | finalized | always | Whether settlement uses a configurable block count or network finality. |
| project_required_confirmations | integer | always | Current project default used by future invoices when no store override is set. |
| override_required_confirmations | integer | null | always | Store-specific count, or null to inherit the project default. |
| effective_required_confirmations | integer | always | Count that new invoices for this store and asset will snapshot. |
| editable | boolean | always | False for finalized networks whose finality policy cannot be overridden. |
| minimum_required_confirmations | integer | always | Inclusive chain-aware lower bound; 0 is exposed only on rails that support detection-time acceptance. |
| maximum_required_confirmations | integer | always | Inclusive chain-aware upper bound. |
ReceiveReadiness
| Field | Type | Presence | Description |
|---|---|---|---|
| ready | boolean | always | Receive setup checks pass. Does not describe spending readiness, gas, balance refresh, or a guaranteed future quote. |
| checked_at | timestamp | always | Assessment time. No network request or address allocation is made by a listing. |
| issues | PaymentMethodIssue[] | always | Empty when ready; otherwise the current actionable blocker for this asset. |
PaymentMethodIssue
| Field | Type | Presence | Description |
|---|---|---|---|
| chain_slug / asset_id / asset_ticker | string / UUID / string | when known | Identifies the affected chain and asset. Lightning can omit asset_id. |
| reason_code | string | always | 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. |
| message / action | string | when available | 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_role | string | null | on-chain | Scanner-compatible role, for example tron-indexer. A healthy general TRON node is not an indexer. |
| healthy_endpoints | integer | on-chain | Healthy matching endpoints, not the independent-provider count. |
| usable_independent_providers / required_independent_providers | integer | on-chain | Usable independent verification slots, capped at the required two. 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_at | timestamp | null | on-chain | Latest matching endpoint health check, separate from assessment time. |
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))Example response · 200 application/json
{
"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" }
]
}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.
- 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.
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Content-Type | required | application/json |
| Accept | recommended | application/json |
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Project assigned to the credential; it may be paused. |
| store_id | path UUID | Store belonging to project_id; it may be paused. |
| asset_id | path UUID | Currently selected store payment asset to update. |
Store confirmation policy body
| Field | Type | Presence | Description |
|---|---|---|---|
| strategy | inherit | custom | required | Tagged strategy. inherit removes the store override; custom requires required_confirmations. |
| required_confirmations | integer | custom only | Whole number inside the minimum/maximum returned for this asset. Unknown or extra fields are rejected. |
PaymentAsset
| Field | Type | Presence | Description |
|---|---|---|---|
| id | UUID | always | Durable payment-asset identifier used by project and store policy routes. |
| asset_key | string | always | Canonical CAIP-style native or contract asset identity. |
| chain_slug / network | string | always | Wholly Crypto chain identifier and configured network. |
| caip_network_id / caip_asset_id | string / string|null | always | Canonical network and asset identities. |
| asset_kind | native | token | always | Whether settlement uses the chain currency or a verified contract/mint. |
| payment_rail | string | always | Runtime rail: utxo, evm-native, solana-native, account-native, privacy-native, or token-transfer. |
| symbol / name / decimals | string / string / integer | always | Display identity and exact atomic-unit precision. |
| contract_address | string | null | always | Canonical ERC-20 contract or SPL mint for tokens; null for native assets. |
| coingecko_id | string | null | always | Discovery/pricing identity. Null for custom contracts; never infer a market price from their ticker. CoinGecko metadata alone never makes a token selectable. |
| custom_token | boolean | always | Custom on-chain-verified contract with project-scoped fixed USD or selected DEX pool pricing. |
| icon_path | path | null | always | Locally cached token icon when available. |
| token_standard | erc20 | spl-token | null | always | Verified runtime token standard; null for native assets. |
| metadata_verified_at | timestamp | null | always | On-chain metadata verification time for promoted tokens. |
| payment_supported / scanner_ready / balance_ready | boolean | always | Build-time registry gates. scanner_ready currently means the payment scanner runtime is installed; invoice creation separately requires two healthy independent exact-role endpoints. balance_ready is true only for implemented balance adapters. |
| default_finality_mode | confirmations | finalized | always | Default finality model inherited by a new project policy. |
| default_required_confirmations / default_monitoring_minutes | integer | always | Default confirmation and monitoring policy. |
StorePaymentAsset
| Field | Type | Presence | Description |
|---|---|---|---|
| asset | PaymentAsset | always | Project-visible native or verified token asset. |
| project_policy | ProjectAssetPolicy | null | always | Parent project policy. |
| selected | boolean | always | Whether this method is part of the store's saved desired configuration. It is offered on new invoices only when its project policy, wallet, and runtime readiness gates also pass. |
| display_order | integer | null | always | Store checkout order when selected. |
| confirmation_policy | StoreConfirmationPolicy | null | always | Effective store policy for a project-configured asset. Null when no project policy exists. |
| wallet | WalletSummary | null | always | Chain wallet shared by native and token assets. |
| wallet_readiness | readiness enum | always | Wallet/policy status only; use receive_readiness for scanner prerequisites. |
| receive_readiness | ReceiveReadiness | null | 5.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. |
StoreConfirmationPolicy
| Field | Type | Presence | Description |
|---|---|---|---|
| finality_mode | confirmations | finalized | always | Whether settlement uses a configurable block count or network finality. |
| project_required_confirmations | integer | always | Current project default used by future invoices when no store override is set. |
| override_required_confirmations | integer | null | always | Store-specific count, or null to inherit the project default. |
| effective_required_confirmations | integer | always | Count that new invoices for this store and asset will snapshot. |
| editable | boolean | always | False for finalized networks whose finality policy cannot be overridden. |
| minimum_required_confirmations | integer | always | Inclusive chain-aware lower bound; 0 is exposed only on rails that support detection-time acceptance. |
| maximum_required_confirmations | integer | always | Inclusive chain-aware upper bound. |
ReceiveReadiness
| Field | Type | Presence | Description |
|---|---|---|---|
| ready | boolean | always | Receive setup checks pass. Does not describe spending readiness, gas, balance refresh, or a guaranteed future quote. |
| checked_at | timestamp | always | Assessment time. No network request or address allocation is made by a listing. |
| issues | PaymentMethodIssue[] | always | Empty when ready; otherwise the current actionable blocker for this asset. |
PaymentMethodIssue
| Field | Type | Presence | Description |
|---|---|---|---|
| chain_slug / asset_id / asset_ticker | string / UUID / string | when known | Identifies the affected chain and asset. Lightning can omit asset_id. |
| reason_code | string | always | 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. |
| message / action | string | when available | 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_role | string | null | on-chain | Scanner-compatible role, for example tron-indexer. A healthy general TRON node is not an indexer. |
| healthy_endpoints | integer | on-chain | Healthy matching endpoints, not the independent-provider count. |
| usable_independent_providers / required_independent_providers | integer | on-chain | Usable independent verification slots, capped at the required two. 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_at | timestamp | null | on-chain | Latest matching endpoint health check, separate from assessment time. |
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))Example response · 200 application/json
{
"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"
}
]
}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. Values are useful for treasury visibility, but invoice settlement remains driven by transaction-level payment-intent monitoring and confirmation policy.
- 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.
- Pending, refreshing, stale, and error values are incomplete cache states and must never be interpreted as a zero balance or a missing payment.
- recent_payments is bounded to three observations per wallet and excludes invalidated history.
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | recommended | application/json |
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Enabled project assigned to the credential. |
WalletSummary
| Field | Type | Presence | Description |
|---|---|---|---|
| id / project_id / native_asset_id | UUID | always | Wallet, owner project, and chain-native asset identifiers. |
| chain_slug / network | string | always | Wallet chain and network. |
| asset_symbol / asset_name | string | always | Chain-native display identity. |
| status | pending | active | disabled | error | always | Operational wallet state. |
| label | string | always | Operator label. |
| public_key / primary_address | string | null | always | Public wallet identity; no seed phrase or private key is exposed. |
| derivation_scheme / address_format | string | null | always | Address policy and format. |
| backup_confirmed_at | timestamp | null | always | Non-null after the operator confirms recovery backup. |
| activation_required / activation_verified_at | boolean / timestamp|null | always | XRP and Stellar shared accounts remain unavailable until the operator funds the displayed address and two independent strict providers verify that exact account. The durable proof does not expire; ordinary live scanner health remains a separate gate. |
| receive_readiness | ReceiveReadiness | null | 5.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. |
| monero_wallet_rpc | MoneroWalletRpcBinding | null | always | 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. |
| last_secret_revealed_at / secret_reveal_count | timestamp|null / integer | always | Console-side secret disclosure audit metadata. |
| next_receive_index | integer | always | Next reserved child-address index. |
| last_scanned_height / last_scanned_at / last_error | integer|null / timestamp|null / string|null | always | Wallet scanner state. |
| balances | WalletAssetBalance[] | 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. |
| total_value_usd | decimal string | null | always | Advisory sum of balances with a current USD price. |
| balance_status | pending | refreshing | fresh | stale | error | unknown | always | Aggregated cache freshness; unknown is a defensive fallback and none of these states proves invoice settlement. |
| balance_checked_at | timestamp | null | always | Oldest relevant successful balance check represented by the aggregate. |
| recent_payments | WalletRecentPayment[] | always | Up to three newest valid detected, confirming, or final observations attributed to this exact wallet. |
| created_at / updated_at | RFC 3339 timestamp | always | Creation and last wallet update time. |
WalletAssetBalance
| Field | Type | Presence | Description |
|---|---|---|---|
| wallet_id / asset_id | UUID | always | Wallet and durable asset identities. |
| project_enabled | boolean | always | Whether this asset is currently enabled by the project's asset policy. |
| active_store_count | integer | always | Number of enabled stores that currently select this asset. This is an acceptance projection; read-only balance tracking remains independent. |
| active_store_ids | UUID[] | always | Enabled stores in this project that currently accept the asset. This permits exact local store filtering without another API request. |
| tracking_active | boolean | always | 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. |
| asset_kind | native | token | always | Native currency or verified contract/mint asset. |
| contract_address | string | null | always | Token contract or mint; null for native currency. |
| symbol / name / decimals | string / string / integer | always | Display identity and atomic precision. |
| coingecko_id | string | null | always | Pricing identity when mapped. |
| balance / balance_atomic | decimal string|null / integer string|null | always | Exact display and atomic balance across the wallet primary address and issued invoice addresses. Null while a complete value is unavailable. |
| price_usd | decimal string | null | always | Advisory cached USD unit price used for valuation. |
| value_usd | decimal string | null | always | Advisory fiat valuation when a current rate exists. |
| status | pending | refreshing | fresh | stale | error | always | Cached scan state for this asset. |
| checked_at | timestamp | null | always | Time represented by a completed balance scan. |
| last_error | string | null | always | Safe operator diagnostic. |
WalletRecentPayment
| Field | Type | Presence | Description |
|---|---|---|---|
| invoice_public_id | UUID | always | Customer-facing invoice identity associated with the observation. |
| chain_slug / symbol | string | always | Chain and native or verified token display symbol. |
| transaction_id / event_index | string / integer | always | Canonical transaction and transfer-event identity. |
| amount | decimal string | always | Exact observed asset amount without floating-point conversion. |
| status | detected | confirming | final | always | Current valid observation state. Reorged, replaced, and invalid observations are excluded. |
| confirmations | integer | always | Latest observed confirmation count. |
| observed_at | RFC 3339 timestamp | always | Time Wholly Crypto first observed the payment. |
ReceiveReadiness
| Field | Type | Presence | Description |
|---|---|---|---|
| ready | boolean | always | Receive setup checks pass. Does not describe spending readiness, gas, balance refresh, or a guaranteed future quote. |
| checked_at | timestamp | always | Assessment time. No network request or address allocation is made by a listing. |
| issues | PaymentMethodIssue[] | always | Empty when ready; otherwise the current actionable blocker for this asset. |
PaymentMethodIssue
| Field | Type | Presence | Description |
|---|---|---|---|
| chain_slug / asset_id / asset_ticker | string / UUID / string | when known | Identifies the affected chain and asset. Lightning can omit asset_id. |
| reason_code | string | always | 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. |
| message / action | string | when available | 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_role | string | null | on-chain | Scanner-compatible role, for example tron-indexer. A healthy general TRON node is not an indexer. |
| healthy_endpoints | integer | on-chain | Healthy matching endpoints, not the independent-provider count. |
| usable_independent_providers / required_independent_providers | integer | on-chain | Usable independent verification slots, capped at the required two. 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_at | timestamp | null | on-chain | Latest matching endpoint health check, separate from assessment time. |
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))Example response · 200 application/json
{
"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"
}
]
}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.
- 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 still need ready wallets, scanners, two independent healthy providers of the required scanner role, and trustworthy rates. 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 needs tron-indexer providers; healthy general full nodes alone do not qualify. 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.
| 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 |
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Copy Project API ID from Project → Settings → API IDs. Must be assigned to the credential; a readable project identifier is not accepted. |
| store_id | path UUID | Copy 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. |
Invoice creation body
| Field | Type | Presence | Description |
|---|---|---|---|
| amount | string | required | 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. |
| currency | string | null | optional | 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 | InvoicePaymentSelection[] | null | optional | 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. |
| order_id | string | null | optional | Merchant order reference, 1–128 characters after trimming; control characters are rejected. |
| string | null | optional | Merchant-only customer email, normalized to a practical ASCII address with at most 254 characters. Omitted or null stores no email. | |
| description | string | null | optional | Customer-facing description, 1–500 characters; line breaks and tabs are allowed. |
| expires_in_seconds | integer | null | optional | Invoice quote lifetime from 300 through 86,400 seconds; omitted or null inherits store policy. |
| exchange_rate_spread_percent | decimal string | null | optional | 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_percent | decimal string | null | optional | Accepted shortfall from 0 through 99.99 with at most two decimal places. Omitted or null inherits the store default. |
| ipn_url | string | null | optional | Public HTTPS callback, at most 2,048 bytes and without credentials or fragment. Overrides the store default; null/omitted inherits it. |
| redirect_url | string | null | optional | HTTPS 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_url | string | null | optional | HTTPS return URL used when checkout ends without successful payment. Omitted or null inherits the store default and cannot clear it. |
| redirect_automatically | boolean | null | optional | Omitted or null inherits store policy. true requires an effective redirect_url. |
| language | string | null | optional | English or German BCP 47 tag such as en, de, or de-DE; omitted or null inherits store policy. |
| checkout_appearance | CheckoutAppearanceOverride | null | optional | 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. |
| metadata | object | null | optional | 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. |
InvoicePaymentSelection · choose store chains and assets
| Field | Type | Presence | Description |
|---|---|---|---|
| chain_slug | string | required | 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[] | null | optional | 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[] | null | optional | 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 must pass readiness and pricing checks. Lightning optionally accepts only BTC. |
| payment_rail | onchain | lightning | optional | 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. |
CheckoutAppearanceOverride · all fields optional
| Field | Type | Presence | Description |
|---|---|---|---|
| inherit_default_store | boolean | optional | 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. |
| title | string | optional | Checkout heading, up to 120 characters. Empty uses the standard heading. |
| intro / outro | string | optional | 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_size / outro_font_size | integer | optional | Pixels: 12, 14, 16, 18, 20 or 24. Default 16 unless inherited differently. |
| theme | system | light | dim | dark | optional | Follow the customer device or use a fixed theme. |
| accent_color / background_color / card_color / button_color | string | optional | #RRGGBB. Background, card and button may be empty for automatic colors. Text contrast is automatic. |
| logo_size / logo_alignment | string | optional | small, medium or large; left or center. |
| images | object | optional | 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_id / show_description / details_expanded | boolean | optional | Show order ID details and a plain-text description below the title. details_expanded opens order ID details initially. Display only, not data redaction. |
| featured_chains | string[] | optional | 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 / default_asset_id | UUID[] / UUID|null | optional | 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. |
| messages | object | optional | en/de objects 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. |
| support_email | string | optional | ASCII email, up to 254 characters. Empty clears. |
| support_url / terms_url / privacy_url | string | optional | HTTPS URLs up to 2,048 characters, without credentials. Empty clears. Links open in a new window. |
| return_button_text | string | optional | Label up to 60 characters. Use top-level redirect_url/cancel_url/redirect_automatically/language for invoice behavior. |
Invoice summary
| Field | Type | Presence | Description |
|---|---|---|---|
| id | UUID | always | Internal invoice UUID. Do not use it in merchant detail or checkout paths. |
| invoice_id | UUID | always | Public invoice UUID used by merchant detail and checkout paths. |
| project_id | UUID | always | Owning project. |
| store_id | UUID | always | Owning store. |
| source | manual | api | always | How the invoice was created. |
| order_id | string | null | always | Merchant order reference. |
| string | null | always | Merchant-only customer email. Never returned by public checkout. | |
| customer_name | string | null | always | Derived display name from private firstname, lastname, and company metadata. |
| customer_address | string | null | always | Derived one-line merchant address from private company, street, street2, zip, city, country, countryiso2, and vatid metadata. |
| description | string | null | always | Customer-facing description. |
| amount | decimal string | always | Canonical invoice amount. |
| currency | string | always | Normalized invoice currency/asset code. |
| exchange_rate_spread_percent | decimal string | always | Locked quote spread: the creation override, or the store default when omitted. Applied before upward rounding; never changes on this invoice. |
| underpayment_tolerance_percent | decimal string | always | Immutable accepted shortfall percentage snapshotted when the invoice was created. |
| status | invoice status | always | new, processing, settled, expired, invalid, or cancelled. |
| amount_status | amount status | always | none, partial, paid, or overpaid. An explicitly allowed zero-amount invoice settles with none and no payment methods. |
| timing_status | timing status | always | on_time or late. |
| resolution | resolution | always | automatic, manually_settled, or manually_invalidated. |
| sequence | integer | always | Monotonic invoice state sequence, starting at 1. |
| winning_payment_intent_id | UUID | null | always | Payment method that resolved the invoice, when selected. |
| expires_at | RFC 3339 timestamp | always | Quote/payment deadline. |
| monitoring_expires_at | RFC 3339 timestamp | always | Latest configured late-monitoring cutoff across payment methods. |
| settled_at | timestamp | null | always | Settlement time when settled. |
| cancelled_at | timestamp | null | always | Cancellation time when cancelled. |
| archived_at | timestamp | null | always | Archival time when archived. |
| created_at | RFC 3339 timestamp | always | Creation time. |
| updated_at | RFC 3339 timestamp | always | Last state update time. |
Invoice detail additions
| Field | Type | Presence | Description |
|---|---|---|---|
| ipn_url | string | null | always | Effective per-invoice IPN target. Merchant response only; omitted from public checkout. |
| redirect_url | string | null | always | Effective success URL used after settlement. |
| cancel_url | string | null | always | Effective return URL used when checkout ends without successful payment. |
| redirect_automatically | boolean | always | Whether checkout should redirect automatically after success. |
| checkout_language | string | always | Effective checkout language tag. |
| metadata | object | always | Merchant metadata. Never returned by public checkout. |
| payment_intents | PaymentIntent[] | always | Quoted payment methods and monitoring state. |
PaymentIntent
| Field | Type | Presence | Description |
|---|---|---|---|
| id | UUID | always | Payment intent identifier; also used as checkout QR intent_id. |
| payment_rail | onchain | lightning | always | 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. |
| bolt11 | string | null | always | Lightning payment request, otherwise null. Pay this request with a Lightning wallet, never send on-chain funds to its payment hash. |
| asset_id | UUID | always | Configured payment asset identifier. |
| asset_key | string | always | Canonical CAIP-style asset key. |
| chain_slug | string | always | Wholly Crypto chain identifier. |
| network | string | always | Configured network, currently mainnet for supported payment assets. |
| caip_network_id | string | always | Canonical CAIP-2 network identifier. |
| caip_asset_id | string | null | always | Canonical CAIP-19 identifier where registered. |
| symbol | string | always | Asset symbol. |
| asset_decimals | integer | always | Atomic-unit precision. Lightning BTC uses 11 (millisatoshis), not on-chain Bitcoin's 8. Quotes are whole satoshis; receipts retain millisatoshi precision. |
| status | intent status | always | pending, partial, paid, overpaid, expired, or invalid. |
| finality_mode | confirmations | finalized | always | Finality policy. |
| required_confirmations | integer | always | Required confirmations when applicable. |
| quote_rate | decimal string | always | Asset units per one invoice currency unit, including the locked spread. For example 1.02 USDC per USD. Not the inverse rate. |
| quote_details | object | null | always | 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_amount | decimal string | always | 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. |
| expected_amount_atomic | integer string | always | Exact amount in the asset's smallest unit. |
| minimum_payment_amount | decimal string | always | Smallest amount accepted as paid after applying the invoice tolerance. |
| minimum_payment_amount_atomic | integer string | always | Exact accepted threshold in the asset's smallest unit. |
| received_amount | decimal string | always | Observed amount. |
| received_amount_atomic | integer string | always | Observed atomic amount. |
| confirmed_amount | decimal string | always | Confirmed/final amount. |
| confirmed_amount_atomic | integer string | always | Confirmed/final atomic amount. |
| destination_address | string | always | On-chain receiving address, or the 64-character payment hash for Lightning. Use bolt11 to pay Lightning; its hash is not a Bitcoin address. |
| destination_tag | string | null | always | Required public payment reference where the rail uses one: XRP destination tag, Stellar memo ID, or TON invoice comment. Null for unique-address rails. |
| derivation_index | integer | always | Reserved wallet child index; merchant detail only. |
| quote_expires_at | RFC 3339 timestamp | always | Quote expiry. |
| monitoring_expires_at | RFC 3339 timestamp | always | Late-monitoring cutoff for this method. |
| next_check_at | timestamp | null | always | Next scheduled chain check. |
| last_checked_at | timestamp | null | always | Last chain check. |
| last_chain_height | integer | null | always | Last trustworthy height observed by the monitor. |
| last_anchor_hash | string | null | always | Last monitor anchor/block hash. |
| last_monitor_error | string | null | always | Safe monitoring diagnostic for operators. |
| first_payment_at | timestamp | null | always | First observed payment time. |
| fully_paid_at | timestamp | null | always | Time the accepted minimum amount was first reached. |
| finalized_at | timestamp | null | always | Time payment met finality policy. |
PaymentMethodIssue
| Field | Type | Presence | Description |
|---|---|---|---|
| chain_slug / asset_id / asset_ticker | string / UUID / string | when known | Identifies the affected chain and asset. Lightning can omit asset_id. |
| reason_code | string | always | 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. |
| message / action | string | when available | 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_role | string | null | on-chain | Scanner-compatible role, for example tron-indexer. A healthy general TRON node is not an indexer. |
| healthy_endpoints | integer | on-chain | Healthy matching endpoints, not the independent-provider count. |
| usable_independent_providers / required_independent_providers | integer | on-chain | Usable independent verification slots, capped at the required two. 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_at | timestamp | null | on-chain | Latest matching endpoint health check, separate from assessment time. |
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 '{
"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))Example response · 201 new invoice; 200 exact idempotent replay
{
"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"
}
}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.
- 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.
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | recommended | application/json |
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Enabled project assigned to the credential. |
| store_id | query UUID | Optional exact store filter. |
| status | query enum | Optional new, processing, settled, expired, invalid, or cancelled. |
| search | query string | Optional case-insensitive public-id, order-id, or email prefix; exact public UUID; or substring across description and recognized customer metadata. Trimmed, at most 100 characters, no controls. |
| limit | query integer | Optional 1–100; defaults to 50. |
| offset | query integer | Optional 0–1,000,000; defaults to 0. |
Invoice summary
| Field | Type | Presence | Description |
|---|---|---|---|
| id | UUID | always | Internal invoice UUID. Do not use it in merchant detail or checkout paths. |
| invoice_id | UUID | always | Public invoice UUID used by merchant detail and checkout paths. |
| project_id | UUID | always | Owning project. |
| store_id | UUID | always | Owning store. |
| source | manual | api | always | How the invoice was created. |
| order_id | string | null | always | Merchant order reference. |
| string | null | always | Merchant-only customer email. Never returned by public checkout. | |
| customer_name | string | null | always | Derived display name from private firstname, lastname, and company metadata. |
| customer_address | string | null | always | Derived one-line merchant address from private company, street, street2, zip, city, country, countryiso2, and vatid metadata. |
| description | string | null | always | Customer-facing description. |
| amount | decimal string | always | Canonical invoice amount. |
| currency | string | always | Normalized invoice currency/asset code. |
| exchange_rate_spread_percent | decimal string | always | Locked quote spread: the creation override, or the store default when omitted. Applied before upward rounding; never changes on this invoice. |
| underpayment_tolerance_percent | decimal string | always | Immutable accepted shortfall percentage snapshotted when the invoice was created. |
| status | invoice status | always | new, processing, settled, expired, invalid, or cancelled. |
| amount_status | amount status | always | none, partial, paid, or overpaid. An explicitly allowed zero-amount invoice settles with none and no payment methods. |
| timing_status | timing status | always | on_time or late. |
| resolution | resolution | always | automatic, manually_settled, or manually_invalidated. |
| sequence | integer | always | Monotonic invoice state sequence, starting at 1. |
| winning_payment_intent_id | UUID | null | always | Payment method that resolved the invoice, when selected. |
| expires_at | RFC 3339 timestamp | always | Quote/payment deadline. |
| monitoring_expires_at | RFC 3339 timestamp | always | Latest configured late-monitoring cutoff across payment methods. |
| settled_at | timestamp | null | always | Settlement time when settled. |
| cancelled_at | timestamp | null | always | Cancellation time when cancelled. |
| archived_at | timestamp | null | always | Archival time when archived. |
| created_at | RFC 3339 timestamp | always | Creation time. |
| updated_at | RFC 3339 timestamp | always | Last state update time. |
Invoice pagination
| Field | Type | Presence | Description |
|---|---|---|---|
| limit | integer | always | Effective page size, 1–100. |
| offset | integer | always | Effective zero-based row offset, 0–1,000,000. |
| total | integer | always | Total rows matching project, store, status, and search filters in the page snapshot. |
| has_more | boolean | always | True when offset plus the returned row count is below total. |
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))Example response · 200 application/json
{
"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
}
}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.
- A scoped lookup deliberately returns invoice_not_found when the public ID is not in the authorized project.
- links.checkout is resolved from the installation's active primary checkout domain at response time.
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | recommended | application/json |
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Enabled project assigned to the credential. |
| invoice_id | path UUID | The invoice_id returned during creation/listing, not internal id. |
Invoice summary
| Field | Type | Presence | Description |
|---|---|---|---|
| id | UUID | always | Internal invoice UUID. Do not use it in merchant detail or checkout paths. |
| invoice_id | UUID | always | Public invoice UUID used by merchant detail and checkout paths. |
| project_id | UUID | always | Owning project. |
| store_id | UUID | always | Owning store. |
| source | manual | api | always | How the invoice was created. |
| order_id | string | null | always | Merchant order reference. |
| string | null | always | Merchant-only customer email. Never returned by public checkout. | |
| customer_name | string | null | always | Derived display name from private firstname, lastname, and company metadata. |
| customer_address | string | null | always | Derived one-line merchant address from private company, street, street2, zip, city, country, countryiso2, and vatid metadata. |
| description | string | null | always | Customer-facing description. |
| amount | decimal string | always | Canonical invoice amount. |
| currency | string | always | Normalized invoice currency/asset code. |
| exchange_rate_spread_percent | decimal string | always | Locked quote spread: the creation override, or the store default when omitted. Applied before upward rounding; never changes on this invoice. |
| underpayment_tolerance_percent | decimal string | always | Immutable accepted shortfall percentage snapshotted when the invoice was created. |
| status | invoice status | always | new, processing, settled, expired, invalid, or cancelled. |
| amount_status | amount status | always | none, partial, paid, or overpaid. An explicitly allowed zero-amount invoice settles with none and no payment methods. |
| timing_status | timing status | always | on_time or late. |
| resolution | resolution | always | automatic, manually_settled, or manually_invalidated. |
| sequence | integer | always | Monotonic invoice state sequence, starting at 1. |
| winning_payment_intent_id | UUID | null | always | Payment method that resolved the invoice, when selected. |
| expires_at | RFC 3339 timestamp | always | Quote/payment deadline. |
| monitoring_expires_at | RFC 3339 timestamp | always | Latest configured late-monitoring cutoff across payment methods. |
| settled_at | timestamp | null | always | Settlement time when settled. |
| cancelled_at | timestamp | null | always | Cancellation time when cancelled. |
| archived_at | timestamp | null | always | Archival time when archived. |
| created_at | RFC 3339 timestamp | always | Creation time. |
| updated_at | RFC 3339 timestamp | always | Last state update time. |
Invoice detail additions
| Field | Type | Presence | Description |
|---|---|---|---|
| ipn_url | string | null | always | Effective per-invoice IPN target. Merchant response only; omitted from public checkout. |
| redirect_url | string | null | always | Effective success URL used after settlement. |
| cancel_url | string | null | always | Effective return URL used when checkout ends without successful payment. |
| redirect_automatically | boolean | always | Whether checkout should redirect automatically after success. |
| checkout_language | string | always | Effective checkout language tag. |
| metadata | object | always | Merchant metadata. Never returned by public checkout. |
| payment_intents | PaymentIntent[] | always | Quoted payment methods and monitoring state. |
PaymentIntent
| Field | Type | Presence | Description |
|---|---|---|---|
| id | UUID | always | Payment intent identifier; also used as checkout QR intent_id. |
| payment_rail | onchain | lightning | always | 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. |
| bolt11 | string | null | always | Lightning payment request, otherwise null. Pay this request with a Lightning wallet, never send on-chain funds to its payment hash. |
| asset_id | UUID | always | Configured payment asset identifier. |
| asset_key | string | always | Canonical CAIP-style asset key. |
| chain_slug | string | always | Wholly Crypto chain identifier. |
| network | string | always | Configured network, currently mainnet for supported payment assets. |
| caip_network_id | string | always | Canonical CAIP-2 network identifier. |
| caip_asset_id | string | null | always | Canonical CAIP-19 identifier where registered. |
| symbol | string | always | Asset symbol. |
| asset_decimals | integer | always | Atomic-unit precision. Lightning BTC uses 11 (millisatoshis), not on-chain Bitcoin's 8. Quotes are whole satoshis; receipts retain millisatoshi precision. |
| status | intent status | always | pending, partial, paid, overpaid, expired, or invalid. |
| finality_mode | confirmations | finalized | always | Finality policy. |
| required_confirmations | integer | always | Required confirmations when applicable. |
| quote_rate | decimal string | always | Asset units per one invoice currency unit, including the locked spread. For example 1.02 USDC per USD. Not the inverse rate. |
| quote_details | object | null | always | 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_amount | decimal string | always | 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. |
| expected_amount_atomic | integer string | always | Exact amount in the asset's smallest unit. |
| minimum_payment_amount | decimal string | always | Smallest amount accepted as paid after applying the invoice tolerance. |
| minimum_payment_amount_atomic | integer string | always | Exact accepted threshold in the asset's smallest unit. |
| received_amount | decimal string | always | Observed amount. |
| received_amount_atomic | integer string | always | Observed atomic amount. |
| confirmed_amount | decimal string | always | Confirmed/final amount. |
| confirmed_amount_atomic | integer string | always | Confirmed/final atomic amount. |
| destination_address | string | always | On-chain receiving address, or the 64-character payment hash for Lightning. Use bolt11 to pay Lightning; its hash is not a Bitcoin address. |
| destination_tag | string | null | always | Required public payment reference where the rail uses one: XRP destination tag, Stellar memo ID, or TON invoice comment. Null for unique-address rails. |
| derivation_index | integer | always | Reserved wallet child index; merchant detail only. |
| quote_expires_at | RFC 3339 timestamp | always | Quote expiry. |
| monitoring_expires_at | RFC 3339 timestamp | always | Late-monitoring cutoff for this method. |
| next_check_at | timestamp | null | always | Next scheduled chain check. |
| last_checked_at | timestamp | null | always | Last chain check. |
| last_chain_height | integer | null | always | Last trustworthy height observed by the monitor. |
| last_anchor_hash | string | null | always | Last monitor anchor/block hash. |
| last_monitor_error | string | null | always | Safe monitoring diagnostic for operators. |
| first_payment_at | timestamp | null | always | First observed payment time. |
| fully_paid_at | timestamp | null | always | Time the accepted minimum amount was first reached. |
| finalized_at | timestamp | null | always | Time payment met finality policy. |
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))Example response · 200 application/json
{
"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"
}
}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.
- 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.
| Header | Presence | Rule |
|---|---|---|
| Authorization | required | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | recommended | application/json |
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Project assigned to this credential. |
| invoice_id | path UUID | Public invoice_id returned at creation. |
| payment_method_id | optional query UUID | Limit to one invoice payment method. |
| limit | query integer | 1–100; default 25. |
| offset | query integer | 0–1,000,000; default 0. |
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))Example response · 200 application/json
{
"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}
}GETCheckout shell/Public
Managed checkout-host root that serves the checkout application without selecting an invoice. Customer integrations should normally use links.checkout instead.
- No bearer token is needed.
- The managed checkout edge allows GET/HEAD and rejects other methods.
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())Example response · 200 text/html
<!doctype html>
<!-- Hosted Wholly Crypto checkout shell -->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.
- 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.
| Parameter | Type / location | Rule |
|---|---|---|
| invoice_id | path UUID | Public invoice UUID returned by the merchant API. |
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())Example response · 200 text/html
<!doctype html>
<!-- Hosted Wholly Crypto checkout application -->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.
- 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.
| Parameter | Type / location | Rule |
|---|---|---|
| invoice_id | path UUID | Public invoice UUID. |
Public checkout invoice
| Field | Type | Presence | Description |
|---|---|---|---|
| invoice_id | UUID | always | Public invoice UUID. |
| order_id | string | null | always | Merchant order reference. |
| description | string | null | always | Customer-facing description. |
| amount | decimal string | always | Invoice amount. |
| currency | string | always | Invoice currency. |
| exchange_rate_spread_percent | decimal string | always | Effective quote spread locked at creation, including a per-invoice override. |
| underpayment_tolerance_percent | decimal string | always | Accepted shortfall percentage for this invoice. |
| status | invoice status | always | Current invoice status. |
| amount_status | amount status | always | none, partial, paid, or overpaid. An explicitly allowed zero-amount invoice settles with none and no payment methods. |
| timing_status | timing status | always | on_time or late. |
| sequence | integer | always | Current state sequence. |
| active_payment_method_id | UUID | null | always | The listed payment method that has received funds. Checkout remains on this method so an underpayment is not continued with an incompatible asset. |
| payment_method_locked | boolean | always | True after a valid payment selects active_payment_method_id. |
| server_time | RFC 3339 timestamp | always | Server clock captured for this response; use it with expires_at to avoid customer-device clock skew. |
| expires_at | RFC 3339 timestamp | always | Invoice deadline. |
| expires_in_seconds | integer | always | Whole seconds remaining at server_time, rounded up and clamped to zero. |
| payment_open | boolean | always | True only while a new or processing invoice is before its deadline and has at least one payable method with an amount remaining. |
| redirect_url | string | null | always | Customer return target after successful settlement. |
| cancel_url | string | null | always | Customer return target when leaving without successful settlement. |
| redirect_automatically | boolean | always | Automatic redirect policy. |
| checkout_language | string | always | Checkout language. |
| project | object | always | name, checkout_title, checkout_description, theme, accent_color, and logo_url. |
| store | object | always | Public store name. |
| appearance | CheckoutAppearance | always | Effective presentation: frozen per-invoice override when supplied, otherwise the store's current design. Never changes financial fields or safety warnings. |
| payment_methods | CheckoutPaymentMethod[] | always | Checkout-safe payment methods. |
CheckoutAppearance
| Field | Type | Presence | Description |
|---|---|---|---|
| inherit_default_store | boolean | always | True when the project's default store supplies this appearance. False for independent stores and frozen invoice overrides. |
| invoice_override | boolean | always | True when checkout_appearance was supplied at invoice creation. Omitted/null keeps this false. |
| title / intro / outro | string | always | Plain merchant heading, top message and bottom message. intro replaces customer_message; old stored copy is preserved. Never evaluate as markup. |
| intro_font_size / outro_font_size | integer | always | Font sizes in pixels: 12, 14, 16, 18, 20 or 24. |
| customer_message | string | always | Deprecated compatibility alias of intro. Use intro for new integrations. |
| theme | system | light | dim | dark | always | Customer-device preference or a fixed theme. |
| accent_color / background_color / card_color / button_color | string | always | Strict #RRGGBB colors. Optional colors are empty for automatic values; foreground contrast is calculated. |
| logo_size / logo_alignment | string | always | small, medium or large; left or center. Images are contained, not cropped. |
| images | object | always | Optional logo_light, logo_dark and favicon URLs: scoped, same-origin normalized PNG images. |
| show_order_id / show_description / details_expanded | boolean | always | Order ID visibility, description below the title and initial order ID expansion. Amount remains visible; these are display controls, not data redaction. |
| featured_chains / featured_asset_ids | array | always | Ordered preferences, applied only to methods already present in the invoice. Missing or disabled methods are ignored. |
| default_asset_id | UUID | null | always | Suggested initial method. A valid remembered customer preference or a method already receiving funds takes priority. |
| messages | object | always | en/de plain text keyed by waiting, confirming, paid, underpaid and expired. English fallback. Supplementary; never replaces actual status. |
| support_email / support_url / terms_url / privacy_url | string | always | Optional contact and HTTPS links, without URL credentials. External links open in a new window. |
| return_button_text | string | always | Optional label only. Success/cancel targets and redirect policy still belong to the invoice. |
CheckoutPaymentMethod
| Field | Type | Presence | Description |
|---|---|---|---|
| payment_rail | onchain | lightning | always | Lightning remains a Bitcoin method, separate from on-chain BTC. Identify the choice by intent id and rail, not only asset_id. |
| bolt11 | string | null | always | Signed Lightning request; null for on-chain methods. Never pay after payable becomes false. |
| payment_hash | string | null | always | Lightning payment hash for reconciliation, not a receiving address. Null for on-chain methods. |
| id | UUID | always | Payment intent identifier. |
| asset_id | UUID | always | Asset UUID used by appearance preferences; distinct from this invoice's payment intent id. |
| asset_key | string | always | Canonical asset key. |
| chain_slug / chain_name | string | always | Machine and display chain names. |
| network | string | always | Payment network. |
| caip_network_id | string | always | Canonical network identity used to disambiguate the selected chain. |
| caip_asset_id | string | null | always | Canonical exact asset identity, including a verified token contract or mint when applicable. |
| asset_name / symbol | string | always | Payment asset display values. |
| asset_icon_url | string | null | always | Same-origin locally cached asset icon, or null when no verified CoinGecko mapping exists. |
| asset_kind | native | token | always | Distinguishes native currency from contract/mint payment. |
| contract_address | string | null | always | Canonical ERC-20 contract or SPL mint for tokens; null for native currency. |
| token_standard | erc20 | spl-token | null | always | Verified token runtime, or null for native currency. |
| asset_decimals | integer | always | Atomic-unit precision: 11 for Lightning BTC millisatoshis, 8 for on-chain BTC satoshis. |
| status | intent status | always | Current payment-method status. |
| payable | boolean | always | True only when this exact method can currently accept payment; false for inactive methods after another asset receives funds. |
| finality_mode / required_confirmations | string / integer | always | Finality policy. |
| expected_amount / expected_amount_atomic | decimal / integer string | always | 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_amount / minimum_payment_amount_atomic | decimal / integer string | always | Accepted settlement threshold after applying underpayment tolerance. |
| received_amount / received_amount_atomic | decimal / integer string | always | Observed amount. |
| remaining_amount | decimal string | always | Exact display amount still needed to reach the accepted threshold, clamped to zero. |
| remaining_amount_atomic | integer string | always | Shortfall to the accepted threshold in atomic units. This is not the requested payment amount: tolerance affects acceptance only. |
| confirmed_amount / confirmed_amount_atomic | decimal / integer string | always | Confirmed/final amount. |
| destination_address / destination_tag | string / string|null | always | On-chain destination and optional reference. For Lightning this is the payment hash with no tag; pay the bolt11/payment_uri instead. |
| quote_expires_at | RFC 3339 timestamp | always | Quote expiry. |
| payment_uri | string | null | always | 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_url | path | null | always | Sequence- and exact-remainder-revisioned same-origin SVG QR path, or null when payable is false. The SVG is no-store. |
| address_explorer_name / address_explorer_url | string|null | always | Validated mainnet explorer fallback where supported. |
| transaction_count | integer | always | Total distinct public, valid transactions observed for this method. |
| transactions_truncated | boolean | always | True when transaction_count exceeds the returned recent transaction list. |
| transactions | CheckoutTransaction[] | always | Up to 10 most recent public, valid transactions. Exact received totals remain independent of this display bound. |
CheckoutTransaction
| Field | Type | Presence | Description |
|---|---|---|---|
| transaction_id | string | always | Observed transaction identifier. |
| status | detected | confirming | final | always | Public observation state. |
| confirmations | integer | always | Observed confirmation count. |
| block_height | integer | null | always | Observed block/ledger height. |
| explorer_name | string | when returned | Validated fixed explorer name. |
| explorer_url | string | when returned | Validated fixed mainnet explorer URL. |
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))Example response · 200 application/json
{
"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": []
}
]
}
}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.
- 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.
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Project UUID copied into the preview link by the authenticated console. |
| store_id | query UUID, optional | Store belonging to this project. Omit to use its first/default store. |
| state | query string, optional | waiting, confirming, paid, underpaid or expired. Browser-only illustration. |
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())Example response · 200 text/html
<!doctype html>
<!-- Hosted branding preview; no invoice is created -->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.
- 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.
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Project UUID from the console preview link. |
| store_id | query UUID, optional | Must belong to this project; mismatched IDs return 404. Unknown query fields are rejected. |
CheckoutAppearance
| Field | Type | Presence | Description |
|---|---|---|---|
| inherit_default_store | boolean | always | True when the project's default store supplies this appearance. False for independent stores and frozen invoice overrides. |
| invoice_override | boolean | always | True when checkout_appearance was supplied at invoice creation. Omitted/null keeps this false. |
| title / intro / outro | string | always | Plain merchant heading, top message and bottom message. intro replaces customer_message; old stored copy is preserved. Never evaluate as markup. |
| intro_font_size / outro_font_size | integer | always | Font sizes in pixels: 12, 14, 16, 18, 20 or 24. |
| customer_message | string | always | Deprecated compatibility alias of intro. Use intro for new integrations. |
| theme | system | light | dim | dark | always | Customer-device preference or a fixed theme. |
| accent_color / background_color / card_color / button_color | string | always | Strict #RRGGBB colors. Optional colors are empty for automatic values; foreground contrast is calculated. |
| logo_size / logo_alignment | string | always | small, medium or large; left or center. Images are contained, not cropped. |
| images | object | always | Optional logo_light, logo_dark and favicon URLs: scoped, same-origin normalized PNG images. |
| show_order_id / show_description / details_expanded | boolean | always | Order ID visibility, description below the title and initial order ID expansion. Amount remains visible; these are display controls, not data redaction. |
| featured_chains / featured_asset_ids | array | always | Ordered preferences, applied only to methods already present in the invoice. Missing or disabled methods are ignored. |
| default_asset_id | UUID | null | always | Suggested initial method. A valid remembered customer preference or a method already receiving funds takes priority. |
| messages | object | always | en/de plain text keyed by waiting, confirming, paid, underpaid and expired. English fallback. Supplementary; never replaces actual status. |
| support_email / support_url / terms_url / privacy_url | string | always | Optional contact and HTTPS links, without URL credentials. External links open in a new window. |
| return_button_text | string | always | Optional label only. Success/cancel targets and redirect policy still belong to the invoice. |
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))Example response · 200 application/json
{
"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": []
}
}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.
- 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.
| Parameter | Type / location | Rule |
|---|---|---|
| invoice_id | path UUID | Public invoice UUID. |
| kind | path enum | logo_light, logo_dark or favicon. |
| revision | path UUID | Current image revision. |
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())Example response · 200 image/png
(binary PNG response)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.
- Use appearance.images from preview data. Unknown or mismatched IDs return 404. No wallet or payment information is exposed.
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Project UUID. |
| store_id | path UUID | Store belonging to the project. |
| kind | path enum | logo_light, logo_dark or favicon. |
| revision | path UUID | Current image revision. |
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())Example response · 200 image/png
(binary PNG response)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.
- 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.
| Parameter | Type / location | Rule |
|---|---|---|
| project_id | path UUID | Project UUID. |
| revision | path UUID | Current checkout logo revision returned in project.logo_url. |
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())Example response · 200 image/png
(binary PNG response)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.
- 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.
| Parameter | Type / location | Rule |
|---|---|---|
| invoice_id | path UUID | Public invoice UUID. |
| intent_id | path UUID | Payment method id from checkout JSON. |
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())Example response · 200 image/svg+xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">…</svg>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.
- 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.
| Parameter | Type / location | Rule |
|---|---|---|
| invoice_id | path UUID | Public invoice UUID. |
| revision | path UUID | Current checkout logo revision embedded in project.logo_url. |
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())Example response · 200 image/png
(binary PNG response)Reference for Wholly Crypto 5.5.0. For your installed version, open Settings → API access → Documentation in your console. View releases.