Connect to your workspace.
Explore the current identity, merchant workspace, ledger and invoice API. Use protected browser sessions, mobile device tokens or scoped merchant keys for the appropriate integration.
One API. Clear boundaries.
Laravel controls identity, workspace membership and permissions. The API never grants financial access because an account has been created.
https://api.satodock.com/api/v1Examples below show illustrative responses, not account data. Hosting availability is tracked separately from this reference.
Sessions for web. Tokens for devices.
Browser sessions
Include credentials on requests. Before a write, request /sanctum/csrf-cookie, URL-decode the XSRF-TOKEN cookie and send its value as X-XSRF-TOKEN. Use the approved web origin with shared session-cookie configuration.
Keep browser credentials out of local storage. Production connections require HTTPS.
Mobile device tokens
Sign in through /auth/device-token, then send Authorization: Bearer <token>. Keep the token in OS-backed secure storage. It expires after 24 hours and can be revoked from the Security page.
Device tokens are account access tokens. Merchant keys are issued separately and cannot substitute for a device token.
const response = await fetch("https://api.satodock.com/api/v1/workspaces", {
credentials: "include",
headers: { Accept: "application/json" },
cache: "no-store",
});
if (!response.ok) throw new Error("Workspace request failed");
const { data } = await response.json();Scoped keys for your merchant server.
Create a key from API keys after verifying your email and enabling MFA. Store its one-time secret in your server secret manager, never in browser code, public environment variables or a mobile bundle.
Send Authorization: Bearer <merchant-key> to /api/v1/merchant/*. The key determines the workspace. Each endpoint enforces its own scope; keys cannot manage human identity or account security.
Payment integration has two steps: create a persistent invoice draft with a stable idempotency key, then explicitly activate that invoice when its provider and account eligibility gates permit. An uncertain response must be reviewed before another creation request.
const response = await fetch("https://api.satodock.com/api/v1/merchant/invoices", {
headers: {
Authorization: "Bearer " + process.env.SATO_DOCK_API_KEY,
Accept: "application/json",
},
});
if (!response.ok) throw new Error("Invoice request failed");
const { data, meta } = await response.json();Endpoints
All paths below are relative to /api/v1. Successful responses wrap the result in data; errors use message and optional field errors.
89 endpoints
GET/operator/custody-networksRead ledger-derived custody exposureCustody reviewer
Read ledger-derived custody exposure
Exact integer-string amounts by network: gross_custody_units, custody_shortfall_units, ledger_custody_net_units (may be negative), reorg_receivable_units and customer_liability_units, plus outflows_blocked. These are accounting records, not an external wallet audit or proof of reserves. Never subtract native gas funding from USDT balances.
{
"data": [
{
"network_id": "btc-bitcoin",
"asset": "BTC",
"decimals": 8,
"gross_custody_units": "0",
"custody_shortfall_units": "0",
"ledger_custody_net_units": "0",
"reorg_receivable_units": "0",
"customer_liability_units": "0",
"outflows_blocked": false
}
]
}GET/operator/custody-bindingsList invoice custody bindingsCustody reviewer
List invoice custody bindings
Requires explicit custody_reviewer role, verified email and MFA. Paginated binding records include id, invoice_id, workspace_id, network_id, chain, bound address, expected_amount_units, invoice_status and created_at. A bound address shown in operations is not a public deposit instruction.
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 0
}
}GET/operator/custody-bindings/{bindingId}Read custody receipts and review versionCustody reviewer
Read custody receipts and review version
Binding details add receipts, current_batch_hash, can_review and review history. Reconciliation attempt/success timestamps and sanitized reconciliation_error_code show freshness; stale available receipts can pause outflows until a successful scan. Receipts expose transaction/output, exact amount, observed/pending/available/reversed/shortfall/recovery_review state, credit cycle, confirmations and verified finality. A null batch hash is not reviewable evidence.
{
"message": "Not Found"
}POST/operator/custody-bindings/{bindingId}/reviewReview an exact custody receipt batchIndependent custody reviewer + fresh MFA
Review an exact custody receipt batch
Two independent reviews and verified chain/custody backing are required before pending funds become available. The backend rescans current chain evidence and rejects stale batch versions with HTTP 409. Reload and independently assess the changed evidence before another approval. No manual credit endpoint exists.
| Field | Requirement |
|---|---|
expected_batch_hash | Exact loaded current_batch_hash, 64 lowercase hexadecimal characters. |
reason / evidence_reference | Independent rationale 10–2000 characters and custody-control/risk evidence reference 8–500 characters. |
password / second factor | Current password and unused two_factor_code or recovery_code. |
{
"message": "Custody evidence changed. Reload before reviewing."
}GET/operator/custody-incidentsList unresolved custody incidentsCustody reviewer
List unresolved custody incidents
Optional include_resolved=1 includes resolved records. Paginated unresolved incidents expose binding_id, receipt_id, network_id, exact affected amount, open/backing_restored/resolved state, episode and timestamps. Network outflow holds protect all downstream balances until verified resolution.
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 0
}
}GET/operator/custody-incidents/{incidentId}Read incident evidence and review historyCustody reviewer
Read incident evidence and review history
Adds can_review and reviewer decisions bound to an incident episode. Actual restored backing and two independent reviews are required for resolution. Resolving one incident does not by itself prove every network hold has cleared.
{
"message": "Not Found"
}POST/operator/custody-incidents/{incidentId}/reviewReview a restored-backing incident episodeIndependent custody reviewer + fresh MFA
Review a restored-backing incident episode
Requires expected_episode from the exact loaded record. A changed episode returns HTTP 409 before a decision is recorded. No manual override bypasses backing verification or the second independent review.
| Field | Requirement |
|---|---|
expected_episode | Loaded incident episode, integer>=1. |
reason / evidence_reference | Rationale 10–2000 characters and independently checked custody evidence reference 8–500 characters. |
password / second factor | Current password and unused two_factor_code or recovery_code. |
{
"message": "The custody incident episode changed. Reload before reviewing."
}GET/operator/p2p-disputesList disputed P2P ordersP2P reviewer
List disputed P2P orders
Requires explicit p2p_reviewer role, verified email and MFA. Returns paginated disputed orders with side reviewer and participant action flags false. Ordinary merchant or consumer accounts cannot access this queue.
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 0
}
}GET/operator/p2p-disputes/{orderId}Read independent dispute evidenceP2P reviewer
Read independent dispute evidence
Order details plus can_review and reviews. Server can_review requires disputed state, no participant conflict and no earlier decision by this reviewer. Review records include reviewer_user_id, decision release/refund, reason, evidence_reference and created_at. Private participant identities are not exposed for client authorization.
{
"message": "Not Found"
}GET/operator/p2p-disputes/{orderId}/messagesRead dispute conversationP2P reviewer
Read dispute conversation
Newest-first paginated immutable messages. Each includes sender_role buyer/seller so evidence can be attributed without exposing private identities. Text claims are not automatic proof of fiat receipt.
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 0
}
}POST/operator/p2p-disputes/{orderId}/reviewRecord independent escrow resolutionP2P reviewer + fresh MFA
Record independent escrow resolution
Two distinct independent reviewers must record matching decisions. First leaves escrow held; second releases to buyer or refunds crypto to seller. Participants cannot review their own order. Eligibility and network safety are rechecked by the backend. An uncertain response must be reconciled by reading current reviews before another write.
| Field | Requirement |
|---|---|
decision | release to buyer or refund crypto to seller. |
reason | Required independent rationale, 10–2000 characters. |
evidence_reference | Required evidence reference, 8–500 characters. |
password / second factor | Current password and unused two_factor_code or recovery_code. |
{
"message": "Not Found"
}GET/consumer/p2p/adsList P2P offersPersonal account
List P2P offers
Filters: network_id, side buy/sell, fiat_currency USD/EUR, mine=1, page and per_page (maximum 50). Sell offers let a taker buy crypto; buy offers let a taker sell. Top-level enabled is the server eligibility flag. Public results exclude private payment instructions. Prices are fiat minor units per whole crypto unit.
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 0
},
"enabled": false
}GET/consumer/p2p/ads/{adId}Read an offerPersonal account
Read an offer
Returns the offer terms, exact amount limits, remaining units, price, payment method and ownership. Only the owner receives private payment instructions. Closed offers are visible only to their owner.
{
"message": "Not Found"
}POST/consumer/p2p/adsCreate an eligible P2P offerApproved personal account + fresh MFA
Create an eligible P2P offer
Requires verified email, enabled MFA, current password, unused second factor and Idempotency-Key. Global and country eligibility gates apply. An offer reserves no funds; escrow is reserved when an order is created. Prices are immutable. Identical retries use the same key; changed payloads conflict.
| Field | Requirement |
|---|---|
side / network_id | buy or sell; one supported ledger network. |
fiat_currency / price_minor_per_unit | USD or EUR; canonical positive integer price, maximum 18 digits. |
amount_units / min_order_units / max_order_units | Canonical positive crypto units, maximum 28 digits, min <= max <= amount. |
payment_method / terms | bank_transfer or mobile_wallet; public terms up to 1000 characters. |
payment_instructions | Required for sell offers, prohibited for buy offers; private instructions up to 2000 characters. |
{
"message": "P2P trading is not enabled for this account."
}POST/consumer/p2p/ads/{adId}/closeClose your offerOwner + fresh authentication
Close your offer
Current password and second factor if currently enabled. Stops new orders without cancelling existing orders or returning their escrow. Read the updated offer response; do not infer an order outcome from offer closure.
{
"message": "Not Found"
}GET/consumer/p2p/ordersList your escrow ordersOrder participant
List your escrow orders
Paginated personal orders. Each includes exact crypto amount and fiat total, side buyer/seller, terms, private payment instructions, status, deadline and authoritative can_mark_paid/can_release/can_cancel/can_dispute flags.
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 0
}
}GET/consumer/p2p/orders/{orderId}Read a participant orderOrder participant
Read a participant order
Only the buyer or seller can read this order. server_time provides the UTC snapshot for an informational countdown without trusting the device clock. Refresh its server state before acting on an uncertain previous outcome. Fiat total uses integer ceiling(amount_units * price_minor_per_unit / 10^decimals).
{
"message": "Not Found"
}POST/consumer/p2p/ordersCreate an escrow orderApproved personal account + fresh MFA
Create an escrow order
Requires Idempotency-Key, password and unused factor. The server reserves seller crypto atomically and applies remaining amount, sufficient funds, eligibility and network hold checks. Same key and identical business payload safely return the existing order.
| Field | Requirement |
|---|---|
ad_id / amount_units | Offer ULID and canonical positive crypto units, maximum 28 digits. |
payment_instructions | Required when taking a buy offer as the seller; up to 2000 characters. |
{
"message": "P2P trading is not enabled for this account."
}POST/consumer/p2p/orders/{orderId}/paidBuyer marks fiat payment sentBuyer + verified email + fresh MFA
Buyer marks fiat payment sent
Only before the payment deadline. This is a buyer statement and never releases crypto. Existing orders can be marked paid even while new-trade creation is disabled.
{
"message": "Not Found"
}POST/consumer/p2p/orders/{orderId}/releaseSeller confirms fiat receiptSeller + verified email + fresh MFA
Seller confirms fiat receipt
Requires received_fiat:true and a payment-marked order. The seller must independently verify full fiat receipt. Both parties must remain eligible; network holds and service gates apply. Crypto is then released to the buyer.
| Field | Requirement |
|---|---|
received_fiat | Must be true after independent fiat receipt confirmation. |
{
"message": "Not Found"
}POST/consumer/p2p/orders/{orderId}/cancelBuyer cancels an unpaid orderBuyer + fresh authentication
Buyer cancels an unpaid order
Buyer only while awaiting payment, following can_cancel. A seller cannot cancel and unlock escrow while a fiat transfer may be in flight. Current password and second factor if enabled. Seller may close their offer for future trades or dispute an existing order.
{
"message": "Not Found"
}POST/consumer/p2p/orders/{orderId}/disputeOpen an escrow disputeParticipant + fresh authentication
Open an escrow dispute
Available for an awaiting-payment or payment-marked order according to can_dispute. Requires password, factor if enabled and reason of 10–2000 characters. The reason is appended to the immutable participant conversation. Escrow remains held pending independent review.
{
"message": "Not Found"
}GET/consumer/p2p/orders/{orderId}/messagesRead private order messagesOrder participant
Read private order messages
Newest-first paginated messages: id, body, is_mine and created_at. Only order participants can read; text is stored encrypted and immutable.
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 0
}
}POST/consumer/p2p/orders/{orderId}/messagesSend an order messageOrder participant
Send an order message
Body text up to 2000 characters. Closed orders are read-only. Attachments and automatic fiat proof verification are unavailable. On an uncertain response, reload the conversation before sending another message.
| Field | Requirement |
|---|---|
body | Required nonempty text, maximum 2000 characters. |
{
"data": {
"id": "example-message-id",
"body": "Example order message",
"is_mine": true,
"created_at": "<UTC timestamp>"
}
}GET/operator/consumer-applicationsList personal identity applicationsConsumer reviewer
List personal identity applications
Requires an explicit consumer_reviewer role, verified email and MFA. Standard status/page/per_page filters return application id, personal account_id, revision, status and submitted_at. Merchant or finance review roles do not grant this access.
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 0
}
}GET/operator/consumer-applications/{applicationId}Read personal verification evidenceConsumer reviewer
Read personal verification evidence
Returns the personal profile fields and review state, with id identifying the application and account_id identifying the separate personal account. Also includes profile_hash, review_evidence and immutable reviews. Operator can_edit/can_submit are false; the applicant cannot review their own identity.
{
"data": {
"id": "example-application-id",
"account_id": "example-personal-account-id",
"status": "submitted",
"revision": 1,
"profile": {
"full_name": "Example Person",
"residence_country": "US",
"birth_date": "1990-01-01"
},
"submitted_at": "<UTC timestamp>",
"reviewed_at": null,
"review_reason": null,
"can_edit": false,
"can_submit": false,
"trading_enabled": false,
"deposit_available": false,
"withdrawal_available": false,
"profile_hash": "<sha256>",
"review_evidence": null,
"reviews": []
}
}POST/operator/consumer-applications/{applicationId}/reviewRecord an independent identity reviewConsumer reviewer + fresh MFA
Record an independent identity review
Requires current password and unused second factor. Only submitted-to-approved/rejected and approved-to-suspended transitions are permitted. The reviewer cannot be the applicant, and approval requires the configured country eligibility policy. Verification approval alone does not enable deposits, withdrawals or trading.
| Field | Requirement |
|---|---|
decision | approved, rejected or suspended, subject to current state. |
reason | Required explanation, 10–2000 characters. |
evidence_reference | Required verification evidence reference, 8–500 characters. |
password | Current reviewer password. |
two_factor_code OR recovery_code | Exactly one valid, unused second factor. |
{
"message": "The operating country is not eligible for consumer approval."
}GET/consumer/accountRead your personal accountAuthenticated identity
Read your personal account
Returns null before a personal profile is created. An existing account includes its own id, profile, status, revision, review timestamps and authoritative edit/submit/eligibility flags. Personal ledger ownership is separate from merchant workspace membership and balances must never be combined.
{
"data": null
}PUT/consumer/accountSave personal identity detailsAuthenticated identity
Save personal identity details
Creates or updates the user's personal profile. Only draft or rejected profiles are editable; a rejected revision is preserved when a new revision is created. Saving does not complete identity verification or country eligibility checks. The API rejects applicants under eighteen.
| Field | Requirement |
|---|---|
full_name | Required legal name, up to 160 characters. |
residence_country | Required two uppercase country-code letters. |
birth_date | Required YYYY-MM-DD date; age must be at least 18. |
{
"data": {
"id": "example-personal-account-id",
"status": "draft",
"revision": 1,
"profile": {
"full_name": "Example Person",
"residence_country": "US",
"birth_date": "1990-01-01"
},
"submitted_at": null,
"reviewed_at": null,
"review_reason": null,
"can_edit": true,
"can_submit": false,
"trading_enabled": false,
"deposit_available": false,
"withdrawal_available": false
}
}POST/consumer/account/submitSubmit a personal profile for reviewVerified email + MFA
Submit a personal profile for review
Requires complete saved identity details, verified email, enabled MFA and fresh authentication. Locks the submitted revision for review. This submission alone does not authorize deposits, withdrawals, trading or access to merchant funds.
| Field | Requirement |
|---|---|
password | Current password. |
two_factor_code OR recovery_code | Exactly one unused second factor. |
{
"data": {
"id": "example-personal-account-id",
"status": "submitted",
"revision": 1,
"profile": {
"full_name": "Example Person",
"residence_country": "US",
"birth_date": "1990-01-01"
},
"submitted_at": "<UTC timestamp>",
"reviewed_at": null,
"review_reason": null,
"can_edit": false,
"can_submit": false,
"trading_enabled": false,
"deposit_available": false,
"withdrawal_available": false
}
}GET/consumer/balancesRead personal ledger balancesAuthenticated identity
Read personal ledger balances
Same exact integer-string network balance contract as merchant balances, bound exclusively to the current user's personal account. Returns 404 before that account exists. The private ledger is not a merchant membership and never appears in the merchant workspace selector.
{
"data": [
{
"network_id": "btc-bitcoin",
"asset": "BTC",
"decimals": 8,
"pending_units": "0",
"available_units": "0",
"held_units": "0",
"total_units": "0",
"outflows_blocked": false
}
]
}GET/consumer/ledgerRead personal ledger historyAuthenticated identity
Read personal ledger history
Standard paginated ledger entries for the current user's personal account only. Credits/debits and exact minor units follow the existing ledger contract. Returns 404 before personal account creation. Never aggregate these entries or balances with merchant workspace records.
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 0
}
}GET/workspaces/{workspaceId}/webhook-endpointsList callback endpoint metadataWorkspace owner
List callback endpoint metadata
Standard paginated list of id, name, url, subscribed events, created_at, revoked_at and delivery_available. Signing secrets are never listed. Registration does not imply delivery is enabled.
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 0
}
}POST/workspaces/{workspaceId}/webhook-endpointsRegister a signed callback endpointOwner + verified email + MFA
Register a signed callback endpoint
Requires fresh authentication and Idempotency-Key of 16–128 characters. Returns the exact signing secret once with HTTP 201; duplicate request keys return 409 without secret recovery. At most five active endpoints. To rotate a secret or change URL, register a replacement and revoke the old endpoint. Delivery resolves only public addresses, pins the resolved address, blocks private/rebound DNS destinations and never follows redirects.
| Field | Requirement |
|---|---|
name | Required name, up to 80 characters. |
url | HTTPS on port 443 public ASCII domain, up to 2048 characters; no IP literals, credentials or fragment. |
events | Distinct nonempty subset of invoice.created and invoice.updated. |
password | Current password. |
two_factor_code OR recovery_code | Unused second factor. |
{
"data": {
"id": "example-endpoint-id",
"name": "Orders",
"url": "https://merchant.example.com/callbacks",
"events": [
"invoice.created",
"invoice.updated"
],
"created_at": "<UTC timestamp>",
"revoked_at": null,
"delivery_available": false,
"secret": "<shown-once-signing-secret>"
}
}DELETE/workspaces/{workspaceId}/webhook-endpoints/{endpointId}Revoke a callback endpointOwner + fresh authentication
Revoke a callback endpoint
Requires current password and a second factor when MFA is enabled. Stops future delivery to the endpoint and preserves the audit history. Email verification is not required to reduce access.
{
"data": {
"revoked": true
}
}GET/workspaces/{workspaceId}/webhook-deliveriesRead callback delivery historyWorkspace owner
Read callback delivery history
Standard pagination with optional endpoint_id and status filters. Statuses are pending, processing, retry, delivered, failed or disabled. Each record includes stable event_id, endpoint_id, type, attempts, last_http_status, safe last_error_code, next_attempt_at, delivered_at and created_at. Automatic retry uses up to eight attempts per cycle with backoff.
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 0
}
}POST/workspaces/{workspaceId}/webhook-deliveries/{deliveryId}/retryQueue a failed callback retryOwner + verified email + MFA
Queue a failed callback retry
Requires fresh password and unused second factor. Only failed or retry deliveries are eligible; at most three manual cycles are allowed. Delivered events cannot be manually replayed. HTTP 202 queued means accepted for retry, not delivered. Event ID remains stable so the receiving server can deduplicate.
| Field | Requirement |
|---|---|
password | Current password. |
two_factor_code OR recovery_code | Unused second factor. |
{
"data": {
"queued": true
}
}GET/operator/transfersList transfer review requestsFinance reviewer
List transfer review requests
Requires an explicit finance_reviewer role, verified email and MFA. Query status is awaiting_approval (default), approved, cancelled or rejected. Standard pagination returns transfer summaries with exact amounts and approval_count; approved does not mean sent.
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 0
}
}GET/operator/transfers/{transferId}Read transfer review evidenceFinance reviewer
Read transfer review evidence
Returns transfer details plus requester_user_id and reviews containing id, reviewer_user_id, decision, reason and created_at. The requester and all members of the merchant workspace are ineligible to review. An operator may decide only once per request.
{
"data": {
"id": "example-transfer-id",
"workspace_id": "example-workspace-id",
"kind": "payout",
"network_id": "btc-bitcoin",
"asset": "BTC",
"decimals": 8,
"amount_units": "1000",
"fee_limit_units": "100",
"reserved_units": "1100",
"fee_asset": "BTC",
"fee_type": "merchant_fee_cap",
"network_fee_asset": "BTC",
"network_fee_quote": null,
"destination": {
"id": "example-destination-id",
"label": "Example destination",
"address": "<validated-network-address>"
},
"invoice_id": null,
"reference": "Example payout",
"status": "awaiting_approval",
"approval_count": 0,
"created_at": "<UTC timestamp>",
"updated_at": "<UTC timestamp>",
"can_cancel": false,
"signing_available": false,
"requester_user_id": "example-requester-id",
"reviews": []
}
}POST/operator/transfers/{transferId}/reviewRecord a finance review decisionIndependent finance reviewer + MFA
Record a finance review decision
Only awaiting_approval requests can be reviewed. Requires current password and unused second factor. Two distinct independent approve decisions change status to approved; one reject releases the held reservation. Repeated decisions by the same reviewer are rejected. Approval rechecks merchant/destination eligibility and never signs or broadcasts a transaction.
| Field | Requirement |
|---|---|
decision | approve or reject. |
reason | Required explanation, 10–2000 characters. |
password | Current reviewer password. |
two_factor_code OR recovery_code | Exactly one valid, unused second factor. |
{
"data": {
"id": "example-transfer-id",
"workspace_id": "example-workspace-id",
"kind": "payout",
"network_id": "btc-bitcoin",
"asset": "BTC",
"decimals": 8,
"amount_units": "1000",
"fee_limit_units": "100",
"reserved_units": "1100",
"fee_asset": "BTC",
"fee_type": "merchant_fee_cap",
"network_fee_asset": "BTC",
"network_fee_quote": null,
"destination": {
"id": "example-destination-id",
"label": "Example destination",
"address": "<validated-network-address>"
},
"invoice_id": null,
"reference": "Example payout",
"status": "awaiting_approval",
"approval_count": 1,
"created_at": "<UTC timestamp>",
"updated_at": "<UTC timestamp>",
"can_cancel": false,
"signing_available": false,
"requester_user_id": "example-requester-id",
"reviews": [
{
"id": "example-review-id",
"reviewer_user_id": "example-reviewer-id",
"decision": "approve",
"reason": "Illustrative independent review decision.",
"created_at": "<UTC timestamp>"
}
]
}
}GET/workspaces/{workspaceId}/payout-destinationsList saved destinationsWorkspace owner
List saved destinations
Standard paginated list of destination id, network_id, label, address, created_at, usable_at, revoked_at and can_use. New addresses must wait at least 24 hours. Refresh to recheck can_use; a locally elapsed timer is not authorization.
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 0
}
}POST/workspaces/{workspaceId}/payout-destinationsSave a payout destinationOwner + verified email + MFA
Save a payout destination
Requires fresh password and unused second factor. Validates BTC mainnet checksum, TRON Base58Check or Ethereum/Polygon EIP-55 checksum. No BEP20 destination is enabled. Duplicate active network/address pairs are rejected; at most 20 active destinations are allowed. Saving starts a 24-hour cooldown and does not send funds.
| Field | Requirement |
|---|---|
network_id | One of the four ledger networks. |
label | Required label, up to 80 characters. |
address | Exact checksummed network address, up to 120 characters. |
password | Current password. |
two_factor_code OR recovery_code | Exactly one unused second factor. |
{
"data": {
"id": "example-destination-id",
"network_id": "btc-bitcoin",
"label": "Example destination",
"address": "<validated-network-address>",
"created_at": "<UTC timestamp>",
"usable_at": "<at-least-24-hours-later>",
"revoked_at": null,
"can_use": false
}
}DELETE/workspaces/{workspaceId}/payout-destinations/{destinationId}Revoke a saved destinationOwner + fresh authentication
Revoke a saved destination
Prevents new requests using the destination. Requires the current password and second factor when MFA is enabled; account email verification is not required to reduce access. Existing transfer reservations have their own cancellation workflow.
{
"data": {
"revoked": true
}
}GET/workspaces/{workspaceId}/transfersList payout and refund requestsWorkspace member
List payout and refund requests
Returns paginated transfer requests with exact amount, fee-cap and reservation strings. approved means review approval, not a broadcast or completed blockchain transfer. Signing remains unavailable. Merchant integration keys cannot access these first-party routes.
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 0
}
}GET/workspaces/{workspaceId}/transfers/{transferId}Read a transfer reservationWorkspace member
Read a transfer reservation
Reads authoritative state and owner can_cancel permission. reserved_units records the original amount-plus-fee-cap reservation; cancelled or rejected reservations have been released. fee_limit_units is a merchant fee cap in the transfer asset, not a native-network fee quote or funding allocation.
{
"data": {
"id": "example-transfer-id",
"workspace_id": "example-workspace-id",
"kind": "payout",
"network_id": "btc-bitcoin",
"asset": "BTC",
"decimals": 8,
"amount_units": "1000",
"fee_limit_units": "100",
"reserved_units": "1100",
"fee_asset": "BTC",
"fee_type": "merchant_fee_cap",
"network_fee_asset": "BTC",
"network_fee_quote": null,
"destination": {
"id": "example-destination-id",
"label": "Example destination",
"address": "<validated-network-address>"
},
"invoice_id": null,
"reference": "Example payout",
"status": "awaiting_approval",
"approval_count": 0,
"created_at": "<UTC timestamp>",
"updated_at": "<UTC timestamp>",
"can_cancel": true,
"signing_available": false
}
}POST/workspaces/{workspaceId}/transfersReserve funds for a transfer requestApproved owner + verified email + MFA
Reserve funds for a transfer request
Requires a matured active destination and sufficient available balance. Atomically moves amount plus merchant fee cap from available to held; it does not sign or send. A stable Idempotency-Key identifies the operation. Same key and business payload returns the original request after a password check; changed payload returns 409. Preserve the key through uncertain outcomes and failed retry authentication. Native-chain fee quote/funding remains separate and unavailable.
| Field | Requirement |
|---|---|
kind | payout or refund. |
destination_id | Saved destination ULID. |
amount_units | Positive canonical integer string, at most 28 digits. |
fee_limit_units | Nonnegative canonical integer string, at most 28 digits. Merchant fee cap in the transfer asset. |
reference | Required, up to 120 characters. |
invoice_id | Required only for refunds; confirmed invoice on the same network/workspace. Cumulative refunds cannot exceed the original amount. |
password | Current owner password. |
two_factor_code OR recovery_code | Unused second factor required for new requests. |
{
"data": {
"id": "example-transfer-id",
"workspace_id": "example-workspace-id",
"kind": "payout",
"network_id": "btc-bitcoin",
"asset": "BTC",
"decimals": 8,
"amount_units": "1000",
"fee_limit_units": "100",
"reserved_units": "1100",
"fee_asset": "BTC",
"fee_type": "merchant_fee_cap",
"network_fee_asset": "BTC",
"network_fee_quote": null,
"destination": {
"id": "example-destination-id",
"label": "Example destination",
"address": "<validated-network-address>"
},
"invoice_id": null,
"reference": "Example payout",
"status": "awaiting_approval",
"approval_count": 0,
"created_at": "<UTC timestamp>",
"updated_at": "<UTC timestamp>",
"can_cancel": true,
"signing_available": false
}
}POST/workspaces/{workspaceId}/transfers/{transferId}/cancelCancel a transfer reservationOwner + fresh authentication
Cancel a transfer reservation
Cancels an awaiting_approval or approved request and releases its held reservation. Requires password and second factor when MFA is enabled. A terminal cancellation retry returns the existing record. Refresh authoritative state after an uncertain response; no blockchain send action is available.
{
"data": {
"id": "example-transfer-id",
"workspace_id": "example-workspace-id",
"kind": "payout",
"network_id": "btc-bitcoin",
"asset": "BTC",
"decimals": 8,
"amount_units": "1000",
"fee_limit_units": "100",
"reserved_units": "1100",
"fee_asset": "BTC",
"fee_type": "merchant_fee_cap",
"network_fee_asset": "BTC",
"network_fee_quote": null,
"destination": {
"id": "example-destination-id",
"label": "Example destination",
"address": "<validated-network-address>"
},
"invoice_id": null,
"reference": "Example payout",
"status": "cancelled",
"approval_count": 0,
"created_at": "<UTC timestamp>",
"updated_at": "<UTC timestamp>",
"can_cancel": false,
"signing_available": false
}
}GET/operator/accessDiscover assigned operator rolesBrowser session
Discover assigned operator roles
Returns explicitly assigned operator roles. Ordinary merchant users receive an empty list; merchant ownership never grants operator privileges. Review endpoints additionally require verified email and enabled MFA. This endpoint cannot grant roles.
{
"data": {
"roles": []
}
}GET/operator/merchant-applicationsList business review applicationsCompliance reviewer
List business review applications
Requires the explicitly assigned compliance_reviewer role, verified email and enabled MFA. Query status is submitted (default), approved, rejected or suspended. Standard page/per_page pagination defaults to 20 entries with a maximum of 50. Returns application ID, workspace ID, revision, status and submission timestamp.
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 0
}
}GET/operator/merchant-applications/{applicationId}Read submitted business evidenceCompliance reviewer
Read submitted business evidence
Returns the owner onboarding fields plus id, profile_hash, review_evidence and reviews containing decision, reason, evidence_reference, reviewer_user_id and created_at. can_edit and can_submit are false in this operator view. Access is independently enforced by the API.
{
"data": {
"id": "example-application-id",
"workspace_id": "example-workspace-id",
"status": "submitted",
"revision": 1,
"profile": {
"legal_name": "Example Merchant",
"registration_country": "US",
"registration_number": "EXAMPLE-001",
"website": "https://merchant.example",
"business_description": "Illustrative business profile."
},
"submitted_at": "<UTC timestamp>",
"reviewed_at": null,
"review_reason": null,
"can_edit": false,
"can_submit": false,
"profile_hash": "<sha256>",
"review_evidence": null,
"reviews": []
}
}POST/operator/merchant-applications/{applicationId}/reviewRecord an independent reviewCompliance reviewer + fresh MFA
Record an independent review
Requires current password and unused second factor. Reviewer must not belong to the applicant workspace. Only submitted-to-approved/rejected and approved-to-suspended transitions are permitted; approval additionally requires the configured country eligibility allowlist. Stores an audit trail and returns the updated application detail. Review approval does not enable payment collection.
| Field | Requirement |
|---|---|
decision | approved, rejected or suspended, subject to the current state. |
reason | Required explanation, 10–2000 characters. |
evidence_reference | Required reference to reviewed evidence, 8–500 characters. |
password | Current reviewer password. |
two_factor_code OR recovery_code | Exactly one valid, unused second factor. |
{
"message": "The operating country is not eligible for merchant approval."
}GET/workspaces/{workspaceId}/onboardingRead business review stateWorkspace owner
Read business review state
Returns the current business profile revision, review status and authoritative can_edit/can_submit permissions. A new workspace has draft status, revision 0 and no profile. Submission is a review step, not completed business verification or live payment approval.
{
"data": {
"workspace_id": "example-workspace-id",
"status": "draft",
"revision": 0,
"profile": null,
"submitted_at": null,
"reviewed_at": null,
"review_reason": null,
"can_edit": true,
"can_submit": false
}
}PUT/workspaces/{workspaceId}/onboardingSave registered business detailsBrowser owner + CSRF
Save registered business details
Saves the complete five-field business profile encrypted at rest. Only draft or rejected profiles are editable; revising a rejected profile creates a new revision and preserves prior submitted evidence. No document upload or self-approval is provided. A country code is registration data, not a service eligibility decision.
| Field | Requirement |
|---|---|
legal_name | Required registered name, up to 160 characters. |
registration_country | Required two uppercase letters. |
registration_number | Required registration identifier, up to 120 characters. |
website | Required HTTPS URL without embedded credentials, up to 2048 characters. |
business_description | Required description, up to 2000 characters. |
{
"data": {
"workspace_id": "example-workspace-id",
"status": "draft",
"revision": 1,
"profile": {
"legal_name": "Example Merchant",
"registration_country": "US",
"registration_number": "EXAMPLE-001",
"website": "https://merchant.example",
"business_description": "Illustrative business profile."
},
"submitted_at": null,
"reviewed_at": null,
"review_reason": null,
"can_edit": true,
"can_submit": false
}
}POST/workspaces/{workspaceId}/onboarding/submitSubmit saved business detailsOwner + verified email + MFA
Submit saved business details
Requires a complete saved draft, verified email, enabled MFA and fresh authentication. Freezes the submitted revision for independent review. Submitted, approved and suspended records cannot be edited by the applicant. Approval and country eligibility are separate operator decisions.
| Field | Requirement |
|---|---|
password | Current account password. |
two_factor_code OR recovery_code | Exactly one valid, unused second factor. |
{
"data": {
"workspace_id": "example-workspace-id",
"status": "submitted",
"revision": 1,
"profile": {
"legal_name": "Example Merchant",
"registration_country": "US",
"registration_number": "EXAMPLE-001",
"website": "https://merchant.example",
"business_description": "Illustrative business profile."
},
"submitted_at": "<UTC timestamp>",
"reviewed_at": null,
"review_reason": null,
"can_edit": false,
"can_submit": false
}
}GET/workspaces/{workspaceId}/api-keysList integration key metadataWorkspace owner
List integration key metadata
Lists key names, prefixes, permissions, expiry and use/revocation timestamps. Secrets are never listed. Query page starts at 1; per_page defaults to 20 and allows up to 50.
{
"data": [
{
"id": "example-key-id",
"name": "Checkout service",
"prefix": "sd_example",
"scopes": [
"invoices:read"
],
"created_at": "<UTC timestamp>",
"expires_at": "<UTC timestamp>",
"last_used_at": null,
"revoked_at": null
}
],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 1
}
}POST/workspaces/{workspaceId}/api-keysIssue a scoped integration keyOwner + verified email + MFA
Issue a scoped integration key
Requires a fresh password and unused second factor. Send a 16–128 character Idempotency-Key. The token is returned once with HTTP 201; duplicate request keys return 409 without redisplaying the secret. An uncertain result must be reviewed in key metadata and revoked if its secret was lost before issuing a replacement. At most ten active keys are allowed per workspace.
| Field | Requirement |
|---|---|
name | Required name, up to 80 characters. |
scopes | One or more of invoices:read, invoices:create, invoices:activate, balances:read or ledger:read. No wildcard. |
expires_in_days | Integer from 1 to 90; default 30. |
password | Current account password. |
two_factor_code OR recovery_code | Exactly one valid, unused second factor. |
{
"data": {
"id": "example-key-id",
"name": "Checkout service",
"prefix": "sd_example",
"scopes": [
"invoices:read"
],
"created_at": "<UTC timestamp>",
"expires_at": "<UTC timestamp>",
"last_used_at": null,
"revoked_at": null,
"token": "<shown-once-secret>"
}
}DELETE/workspaces/{workspaceId}/api-keys/{keyId}Revoke an integration keyOwner + fresh authentication
Revoke an integration key
Requires the owner's current password and a second factor only when MFA is enabled. Email verification is not required to reduce access. Password resets and MFA confirm/recovery-code replacement/disable also revoke existing merchant keys automatically.
| Field | Requirement |
|---|---|
password | Current account password. |
two_factor_code OR recovery_code | Required when MFA is enabled. |
{
"data": {
"revoked": true
}
}GET/merchant/invoicesList invoices from your serverMerchant key · invoices:read
List invoices from your server
Uses Authorization: Bearer <merchant-key>. The workspace is derived exclusively from the key; no workspace ID is accepted. Human sessions and mobile tokens cannot substitute for a merchant key. Pagination follows the workspace invoice endpoint.
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 0
}
}GET/merchant/invoices/{invoiceId}Read a merchant invoiceMerchant key · invoices:read
Read a merchant invoice
Returns an invoice belonging to the key's workspace. Other workspaces' records remain inaccessible. A confirmed payment state is separate from merchant balance availability or payout completion.
{
"data": {
"id": "example-invoice-id",
"workspace_id": "example-workspace-id",
"network_id": "btc-bitcoin",
"asset": "BTC",
"decimals": 8,
"amount_units": "1000",
"reference": "Order-1001",
"description": null,
"status": "draft",
"checkout_url": null,
"provider_invoice_id": null,
"provider_status": null,
"failure_code": null,
"created_at": "<UTC timestamp>",
"expires_at": null,
"can_activate": false
}
}POST/merchant/invoicesCreate an invoice draft from your serverMerchant key · invoices:create
Create an invoice draft from your server
Step one of collection: persist a draft with the same network_id, amount_units, reference and optional description contract as the workspace endpoint. Send a stable Idempotency-Key per logical creation request and reuse it with an unchanged body for retries. Creating a draft does not create a provider checkout.
| Field | Requirement |
|---|---|
network_id | btc-bitcoin, usdt-tron, usdt-ethereum or usdt-polygon. |
amount_units | Canonical positive minor-unit integer string, 1–28 digits. |
reference | Required string, up to 120 characters. |
description | Optional string, up to 240 characters. |
{
"data": {
"id": "example-invoice-id",
"workspace_id": "example-workspace-id",
"network_id": "btc-bitcoin",
"asset": "BTC",
"decimals": 8,
"amount_units": "1000",
"reference": "Order-1001",
"description": null,
"status": "draft",
"checkout_url": null,
"provider_invoice_id": null,
"provider_status": null,
"failure_code": null,
"created_at": "<UTC timestamp>",
"expires_at": null,
"can_activate": false
}
}POST/merchant/invoices/{invoiceId}/activateActivate an eligible merchant invoiceMerchant key · invoices:activate
Activate an eligible merchant invoice
Step two is a separate action after draft creation. Activation follows provider, release and merchant eligibility gates. Repeated requests inspect the existing invoice state rather than blindly creating another provider invoice. Review an uncertain outcome before taking another action.
{
"message": "Payment collection is unavailable until provider and merchant eligibility are verified."
}GET/merchant/balancesRead balances from your serverMerchant key · balances:read
Read balances from your server
Returns the same exact minor-unit balance contract as workspace balances, limited to the key's workspace. Keep BTC and each USDT network separate and use decimal-safe arithmetic.
{
"data": [
{
"network_id": "btc-bitcoin",
"asset": "BTC",
"decimals": 8,
"pending_units": "0",
"available_units": "0",
"held_units": "0",
"total_units": "0",
"outflows_blocked": false
}
]
}GET/merchant/ledgerRead ledger history from your serverMerchant key · ledger:read
Read ledger history from your server
Returns paginated ledger entries for the key's workspace using exact integer-string amounts and the same credit/debit bucket semantics as workspace history.
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 0
}
}GET/auth/mfaRead account security stateSession or device
Read account security state
Returns enabled and pending authenticator state, the number of unused recovery codes and whether account email delivery is configured. Secrets and recovery-code values are never returned by this read endpoint.
{
"data": {
"enabled": false,
"pending_setup": false,
"recovery_codes_remaining": 0,
"email_delivery_available": false
}
}POST/auth/mfa/setupStart authenticator enrollmentBrowser + fresh password
Start authenticator enrollment
Checks the current password and returns a pending secret and otpauth URI once. Pending setup expires after ten minutes. Render its QR locally; never send the secret to an external QR service. Setup does not enable MFA until confirmation and does not revoke existing sessions.
| Field | Requirement |
|---|---|
password | Current account password. |
{
"data": {
"secret": "<shown-once-setup-key>",
"otpauth_uri": "<shown-once-otpauth-uri>",
"expires_at": "<UTC timestamp>"
}
}POST/auth/mfa/confirmConfirm and enable MFABrowser + fresh password
Confirm and enable MFA
Confirms the six-digit TOTP for the pending setup and returns ten single-use recovery codes once. Enabling MFA revokes merchant API keys, other browser sessions and all mobile tokens while preserving the current browser session.
| Field | Requirement |
|---|---|
password | Current account password. |
two_factor_code | Current six-digit authenticator code. |
{
"data": {
"enabled": true,
"recovery_codes": [
"<ten-shown-once-recovery-codes>"
]
}
}POST/auth/mfa/recovery-codesReplace recovery codesBrowser + password + factor
Replace recovery codes
Replaces all previous recovery codes with ten new codes, shown once. Requires the current password and an unused authenticator or recovery code. Merchant API keys, other sessions and all mobile tokens are revoked; the current browser session remains active.
| Field | Requirement |
|---|---|
password | Current account password. |
two_factor_code OR recovery_code | Exactly one second factor. TOTP is six digits; recovery code is at most 40 characters. |
{
"data": {
"recovery_codes": [
"<ten-shown-once-recovery-codes>"
]
}
}DELETE/auth/mfaDisable MFABrowser + password + factor
Disable MFA
Requires the current password and one valid unused second factor. Clears the enrolled authenticator and recovery codes and revokes merchant API keys, other sessions and all mobile tokens. Browser requests require CSRF protection.
| Field | Requirement |
|---|---|
password | Current account password. |
two_factor_code OR recovery_code | Exactly one valid, unused second factor. |
{
"data": {
"enabled": false
}
}POST/auth/forgot-passwordRequest password recoveryPublic
Request password recovery
Returns the same generic HTTP 202 result for known and unknown valid email addresses. delivery_available=false means no email is sent; never claim delivery from accepted alone. Reset tokens are never returned over this API.
| Field | Requirement |
|---|---|
email | Account email address, at most 254 characters. |
{
"data": {
"accepted": true,
"delivery_available": false
}
}POST/auth/reset-passwordReset a passwordReset link token
Reset a password
Uses the token delivered through the reset link. Invalid, expired or reused tokens return 422. A successful reset revokes merchant API keys, signs out every browser session and mobile token, and preserves MFA enrollment. The client must sign in again.
| Field | Requirement |
|---|---|
email | Account email associated with the reset link. |
token | Reset-link token, at most 200 characters. |
password | At least 12 characters with letters and numbers; at most 72 UTF-8 bytes. |
password_confirmation | Must match the new password. |
{
"data": {
"password_reset": true
}
}POST/auth/email/resendRequest email verificationSession or device
Request email verification
Requests a signed verification email for the authenticated account. HTTP 202 accepts the request; check delivery_available before reporting that an email was requested for delivery. Browser writes require CSRF protection.
{
"data": {
"accepted": true,
"delivery_available": false
}
}GET/auth/email/verify/{id}/{hash}Verify a signed email linkSession + signed link
Verify a signed email link
Forward expires and signature query parameters from the verification link unchanged. Only the authenticated user identified by the valid signed link can verify that email. An invalid signature, expired link or another user's identity returns 403.
| Field | Requirement |
|---|---|
expires | Signed expiry query parameter from the email link. |
signature | Signed query parameter from the email link; preserve it unchanged. |
{
"data": {
"email_verified": true
}
}POST/workspaces/{workspaceId}/invoicesSave an invoice draftBrowser owner + CSRF
Save an invoice draft
Creates a persistent invoice draft without requesting funds. Send a required Idempotency-Key header of 16–128 letters, digits, colons, underscores or hyphens. Reuse the same key and unchanged body when retrying an uncertain request; a changed payload with the same key returns 409. Browser requests require CSRF. Current mobile device tokens do not have invoice write permission.
| Field | Requirement |
|---|---|
network_id | btc-bitcoin, usdt-tron, usdt-ethereum or usdt-polygon. |
amount_units | Positive canonical integer string with 1–28 digits; no leading zero, decimal point, sign or exponent. |
reference | Required nonempty string, at most 120 characters. References are not unique; the idempotency key identifies the request. |
description | Optional string, at most 240 characters. |
{
"data": {
"id": "example-invoice-id",
"workspace_id": "example-workspace-id",
"network_id": "btc-bitcoin",
"asset": "BTC",
"decimals": 8,
"amount_units": "1000",
"reference": "Order-1001",
"description": null,
"status": "draft",
"checkout_url": null,
"provider_invoice_id": null,
"provider_status": null,
"failure_code": null,
"created_at": "<UTC timestamp>",
"expires_at": null,
"can_activate": false
}
}GET/workspaces/{workspaceId}/invoicesList workspace invoicesSession or device
List workspace invoices
Returns paginated drafts and payment states for the authenticated workspace. Query page starts at 1; per_page defaults to 20 and allows up to 50. Requires workspace membership and workspace:read for device tokens.
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 0
}
}GET/workspaces/{workspaceId}/invoices/{invoiceId}Read invoice stateSession or device
Read invoice state
Returns the authoritative invoice state: draft, creating, awaiting_payment, confirming, confirmed, expired or review_required. Unknown invoices and invoices outside the workspace return 404. A checkout URL, when present, is validated by the backend against the configured HTTPS provider host. Payment confirmation and payout availability are separate records.
{
"data": {
"id": "example-invoice-id",
"workspace_id": "example-workspace-id",
"network_id": "btc-bitcoin",
"asset": "BTC",
"decimals": 8,
"amount_units": "1000",
"reference": "Order-1001",
"description": null,
"status": "draft",
"checkout_url": null,
"provider_invoice_id": null,
"provider_status": null,
"failure_code": null,
"created_at": "<UTC timestamp>",
"expires_at": null,
"can_activate": false
}
}POST/workspaces/{workspaceId}/invoices/{invoiceId}/activateActivate a provider checkoutBrowser owner + CSRF
Activate a provider checkout
A separate action guarded by the backend release flag, workspace approval and verified provider/network configuration. Check can_activate before offering the action. The invoice is claimed atomically; retries return its existing state and do not blindly create another provider invoice. An uncertain provider result enters review_required. Activation is currently unavailable while collection gates remain closed. This endpoint does not require an additional idempotency header.
{
"message": "Payment collection is unavailable until provider and merchant eligibility are verified."
}GET/workspaces/{workspaceId}/balancesRead ledger balancesSession or device
Read ledger balances
Returns pending, available, held and total balances separately for Bitcoin and USDT on TRON, Ethereum and Polygon. All *_units values are exact integer strings; divide by 10^decimals using decimal-safe arithmetic. No balance is combined across networks, and these records do not enable withdrawals. Requires workspace membership and workspace:read for devices.
{
"data": [
{
"network_id": "btc-bitcoin",
"asset": "BTC",
"decimals": 8,
"pending_units": "0",
"available_units": "0",
"held_units": "0",
"total_units": "0",
"outflows_blocked": false
}
]
}GET/workspaces/{workspaceId}/ledgerRead paginated ledger historySession or device
Read paginated ledger history
Returns immutable financial entries for the workspace. Query parameters page and per_page control pagination. A credit increases the named balance bucket; a debit decreases it. Exact amounts are integer strings in the network asset's minor units. There is no public endpoint for writing journal entries.
| Field | Requirement |
|---|---|
page | Optional query parameter, page number starting at 1. |
per_page | Optional query parameter; default 20 entries. |
{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 1,
"total": 0
}
}GET/statusService statusPublic
Service status
Returns application liveness and the current money movement flag. This response does not verify database or provider readiness.
{
"data": {
"service": "Sato Dock API",
"version": "v1",
"money_movement_enabled": false
}
}POST/auth/registerCreate a personal or merchant accountBrowser + CSRF
Create a personal or merchant account
Creates a user, starts a browser session and returns HTTP 201. account_type defaults to merchant for existing clients; personal creates no merchant workspace. Use GET /workspaces after sign-in: an empty list leads to the personal wallet, where the identity profile is created separately. Sign out of an existing account first. Account creation does not activate money movement.
| Field | Requirement |
|---|---|
account_type | Optional personal or merchant; default merchant. |
name | Required string, up to 120 characters. |
workspace_name | Required for merchant, prohibited for personal; up to 120 characters. |
email | Required unique email, up to 254 characters. |
password | At least 12 characters including letters and numbers; at most 72 UTF-8 bytes. |
password_confirmation | Must match password. |
{
"data": {
"id": "example-user-id",
"name": "Example Merchant",
"email": "[email protected]",
"email_verified": false,
"mfa_enabled": false
}
}POST/auth/loginStart a browser sessionBrowser + CSRF
Start a browser session
Validates credentials, rotates the session ID and returns the authenticated identity. Incorrect credentials produce a 422 validation error. For an MFA-enabled account, send either two_factor_code or recovery_code with the same credentials. Missing or invalid factors return 422 with code mfa_required or mfa_invalid; no session is issued until both factors pass.
| Field | Requirement |
|---|---|
email | Registered email address. |
password | Account password, at most 72 UTF-8 bytes. |
{
"data": {
"id": "example-user-id",
"name": "Example Merchant",
"email": "[email protected]",
"email_verified": false,
"mfa_enabled": false
}
}POST/auth/logoutEnd a browser sessionBrowser + CSRF
End a browser session
Invalidates the authenticated browser session and rotates the CSRF token. Mobile clients revoke their bearer token instead.
{
"data": {
"signed_out": true
}
}GET/auth/meRead your identitySession or device
Read your identity
Returns the current identity. A bearer device requires the workspace:read ability.
{
"data": {
"id": "example-user-id",
"name": "Example Merchant",
"email": "[email protected]",
"email_verified": false,
"mfa_enabled": false
}
}POST/auth/device-tokenSign in a mobile deviceCredentials
Sign in a mobile device
Returns a bearer token once, an expiry timestamp and the user identity. Tokens expire after 24 hours; at most 10 active devices are allowed. Store the token in OS-backed secure storage. Current abilities are workspace:read and devices:manage. If MFA is enabled, include two_factor_code or recovery_code alongside the credentials; the same 422 challenge codes apply.
| Field | Requirement |
|---|---|
email | Registered email address. |
password | Account password, at most 72 UTF-8 bytes. |
device_name | Required descriptive string, up to 80 characters. |
{
"data": {
"token": "<returned-once-token>",
"expires_at": "<UTC timestamp>",
"user": {
"id": "example-user-id",
"name": "Example Merchant",
"email": "[email protected]",
"email_verified": false,
"mfa_enabled": false
}
}
}GET/auth/devicesList your active devicesSession or device
List your active devices
Lists the current user's active mobile tokens. Browser sessions and token secrets are not included. A bearer device requires devices:manage.
{
"data": [
{
"id": 1,
"name": "Example phone",
"last_used_at": null,
"created_at": "<UTC timestamp>",
"expires_at": "<UTC timestamp>"
}
]
}DELETE/auth/devices/{tokenId}Revoke an owned deviceSession or device
Revoke an owned device
Immediately revokes the selected device's API access. A device owned by another user returns 404. Browser requests require CSRF protection; bearer devices require devices:manage.
{
"data": {
"revoked": true
}
}DELETE/auth/device-tokenSign out the current deviceBearer device
Sign out the current device
Revokes the bearer token used by this request. A browser session without a bearer device token receives 400; browser clients use /auth/logout.
{
"data": {
"revoked": true
}
}GET/workspacesList your workspacesSession or device
List your workspaces
Returns only workspaces the current user belongs to. A bearer device requires workspace:read. Use a returned ID for workspace detail requests.
{
"data": [
{
"id": "example-workspace-id",
"name": "Example Store",
"role": "owner",
"verification_status": "unverified"
}
]
}GET/workspaces/{workspaceId}Read workspace readinessSession or device
Read workspace readiness
Returns membership, configured network readiness and capability flags. Network entries are informational and never constitute a deposit destination. Unknown or inaccessible workspaces return 404.
{
"data": {
"id": "example-workspace-id",
"name": "Example Store",
"role": "owner",
"verification_status": "unverified",
"networks": [
{
"id": "btc-bitcoin",
"asset": "BTC",
"network": "Bitcoin",
"standard": "On-chain",
"status": "setup_required"
}
],
"capabilities": {
"collect_payments": false,
"withdraw": false,
"trade": false,
"p2p": false
}
}
}Verify before applying an invoice event.
Register an endpoint from Callbacks and store the one-time signing secret on your receiving server. Use that exact secret string, including its prefix, as the HMAC key; do not hex- or base64-decode it.
The SatoDock-Signature header has the form t=<unix-seconds>,v1=<hex-hmac>. The signed message is the timestamp string, a period and the exact raw request body. Verify with constant-time comparison and reject timestamps outside a five-minute window.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody is a Buffer captured before JSON parsing.
export function validSignature(rawBody, signature, signingSecret) {
const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(signature ?? "");
if (!match) return false;
const timestamp = Number(match[1]);
if (!Number.isSafeInteger(timestamp) ||
Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 300) return false;
const expected = createHmac("sha256", signingSecret)
.update(match[1] + ".")
.update(rawBody)
.digest();
return timingSafeEqual(expected, Buffer.from(match[2], "hex"));
}After verifying the signature, parse the JSON body and require its id to match SatoDock-Event-ID. The body includes id, type, created_at, workspace_id and an invoice snapshot in data. Persist the event ID and apply business effects in an atomic, idempotent workflow. Acknowledge duplicate events without repeating their effects.
Retries keep the same event ID. Delivery can be duplicated or arrive out of order; retrieve the authoritative invoice if current state matters. An invoice-created event represents a draft, and review approval or a browser redirect does not prove a completed payment. Return a successful HTTP response only after durably accepting the event.
The destination must use HTTPS on port 443 with a public domain. Private addresses and DNS rebinding are blocked during delivery, and redirects are never followed. Delivery availability is shown separately for each endpoint.
Errors with a next step.
Responses include an X-Request-ID header for server-log correlation. Keep the request ID when investigating an error; never include passwords or tokens in logs.
| Status | Meaning | Action |
|---|---|---|
400 | Invalid request | Check endpoint-specific requirements, such as bearer authentication for device sign-out. |
401 | Authentication required | Sign in again. Device tokens may have expired or been revoked. |
403 | Access denied | The current identity or token lacks the required permission. |
404 | Resource unavailable | Check the ID and workspace membership. Hidden resources also return 404. |
409 | State conflict | Resolve the stated conflict, such as an active browser account or the 10-device limit. |
419 | Session / CSRF mismatch | Refresh the CSRF cookie and sign in if the browser session has expired. |
422 | Validation failed | Display the field errors and correct the request before retrying. |
429 | Rate limited | Wait before retrying. Respect Retry-After when returned by the server. |
5xx | Service unavailable | Do not assume a write failed safely. Check its outcome before repeating it. |
{
"message": "The email or password is incorrect.",
"errors": {
"email": ["The email or password is incorrect."]
}
}