Sato Dock
Sato Dock/Developers/API reference
DEVELOPER REFERENCE

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.

REST / JSONVersion 1Development availability

One API. Clear boundaries.

Laravel controls identity, workspace membership and permissions. The API never grants financial access because an account has been created.

CONFIGURED API BASEhttps://api.satodock.com/api/v1

Examples below show illustrative responses, not account data. Hosting availability is tracked separately from this reference.

AUTHENTICATION

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.

Browser read example · after sign-in
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();
SERVER INTEGRATIONS

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.

Server-side read example
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();
API V1

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.

JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Review an exact custody receipt batch
FieldRequirement
expected_batch_hashExact loaded current_batch_hash, 64 lowercase hexadecimal characters.
reason / evidence_referenceIndependent rationale 10–2000 characters and custody-control/risk evidence reference 8–500 characters.
password / second factorCurrent password and unused two_factor_code or recovery_code.
JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Review a restored-backing incident episode
FieldRequirement
expected_episodeLoaded incident episode, integer>=1.
reason / evidence_referenceRationale 10–2000 characters and independently checked custody evidence reference 8–500 characters.
password / second factorCurrent password and unused two_factor_code or recovery_code.
JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Record independent escrow resolution
FieldRequirement
decisionrelease to buyer or refund crypto to seller.
reasonRequired independent rationale, 10–2000 characters.
evidence_referenceRequired evidence reference, 8–500 characters.
password / second factorCurrent password and unused two_factor_code or recovery_code.
JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Create an eligible P2P offer
FieldRequirement
side / network_idbuy or sell; one supported ledger network.
fiat_currency / price_minor_per_unitUSD or EUR; canonical positive integer price, maximum 18 digits.
amount_units / min_order_units / max_order_unitsCanonical positive crypto units, maximum 28 digits, min <= max <= amount.
payment_method / termsbank_transfer or mobile_wallet; public terms up to 1000 characters.
payment_instructionsRequired for sell offers, prohibited for buy offers; private instructions up to 2000 characters.
JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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).

JSON response example
{
  "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.

Request fields for Create an escrow order
FieldRequirement
ad_id / amount_unitsOffer ULID and canonical positive crypto units, maximum 28 digits.
payment_instructionsRequired when taking a buy offer as the seller; up to 2000 characters.
JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Seller confirms fiat receipt
FieldRequirement
received_fiatMust be true after independent fiat receipt confirmation.
JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Send an order message
FieldRequirement
bodyRequired nonempty text, maximum 2000 characters.
JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Record an independent identity review
FieldRequirement
decisionapproved, rejected or suspended, subject to current state.
reasonRequired explanation, 10–2000 characters.
evidence_referenceRequired verification evidence reference, 8–500 characters.
passwordCurrent reviewer password.
two_factor_code OR recovery_codeExactly one valid, unused second factor.
JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Save personal identity details
FieldRequirement
full_nameRequired legal name, up to 160 characters.
residence_countryRequired two uppercase country-code letters.
birth_dateRequired YYYY-MM-DD date; age must be at least 18.
JSON response example
{
  "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.

Request fields for Submit a personal profile for review
FieldRequirement
passwordCurrent password.
two_factor_code OR recovery_codeExactly one unused second factor.
JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Register a signed callback endpoint
FieldRequirement
nameRequired name, up to 80 characters.
urlHTTPS on port 443 public ASCII domain, up to 2048 characters; no IP literals, credentials or fragment.
eventsDistinct nonempty subset of invoice.created and invoice.updated.
passwordCurrent password.
two_factor_code OR recovery_codeUnused second factor.
JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Queue a failed callback retry
FieldRequirement
passwordCurrent password.
two_factor_code OR recovery_codeUnused second factor.
JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Record a finance review decision
FieldRequirement
decisionapprove or reject.
reasonRequired explanation, 10–2000 characters.
passwordCurrent reviewer password.
two_factor_code OR recovery_codeExactly one valid, unused second factor.
JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Save a payout destination
FieldRequirement
network_idOne of the four ledger networks.
labelRequired label, up to 80 characters.
addressExact checksummed network address, up to 120 characters.
passwordCurrent password.
two_factor_code OR recovery_codeExactly one unused second factor.
JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Reserve funds for a transfer request
FieldRequirement
kindpayout or refund.
destination_idSaved destination ULID.
amount_unitsPositive canonical integer string, at most 28 digits.
fee_limit_unitsNonnegative canonical integer string, at most 28 digits. Merchant fee cap in the transfer asset.
referenceRequired, up to 120 characters.
invoice_idRequired only for refunds; confirmed invoice on the same network/workspace. Cumulative refunds cannot exceed the original amount.
passwordCurrent owner password.
two_factor_code OR recovery_codeUnused second factor required for new requests.
JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Record an independent review
FieldRequirement
decisionapproved, rejected or suspended, subject to the current state.
reasonRequired explanation, 10–2000 characters.
evidence_referenceRequired reference to reviewed evidence, 8–500 characters.
passwordCurrent reviewer password.
two_factor_code OR recovery_codeExactly one valid, unused second factor.
JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Save registered business details
FieldRequirement
legal_nameRequired registered name, up to 160 characters.
registration_countryRequired two uppercase letters.
registration_numberRequired registration identifier, up to 120 characters.
websiteRequired HTTPS URL without embedded credentials, up to 2048 characters.
business_descriptionRequired description, up to 2000 characters.
JSON response example
{
  "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.

Request fields for Submit saved business details
FieldRequirement
passwordCurrent account password.
two_factor_code OR recovery_codeExactly one valid, unused second factor.
JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Issue a scoped integration key
FieldRequirement
nameRequired name, up to 80 characters.
scopesOne or more of invoices:read, invoices:create, invoices:activate, balances:read or ledger:read. No wildcard.
expires_in_daysInteger from 1 to 90; default 30.
passwordCurrent account password.
two_factor_code OR recovery_codeExactly one valid, unused second factor.
JSON response example
{
  "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.

Request fields for Revoke an integration key
FieldRequirement
passwordCurrent account password.
two_factor_code OR recovery_codeRequired when MFA is enabled.
JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Create an invoice draft from your server
FieldRequirement
network_idbtc-bitcoin, usdt-tron, usdt-ethereum or usdt-polygon.
amount_unitsCanonical positive minor-unit integer string, 1–28 digits.
referenceRequired string, up to 120 characters.
descriptionOptional string, up to 240 characters.
JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Start authenticator enrollment
FieldRequirement
passwordCurrent account password.
JSON response example
{
  "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.

Request fields for Confirm and enable MFA
FieldRequirement
passwordCurrent account password.
two_factor_codeCurrent six-digit authenticator code.
JSON response example
{
  "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.

Request fields for Replace recovery codes
FieldRequirement
passwordCurrent account password.
two_factor_code OR recovery_codeExactly one second factor. TOTP is six digits; recovery code is at most 40 characters.
JSON response example
{
  "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.

Request fields for Disable MFA
FieldRequirement
passwordCurrent account password.
two_factor_code OR recovery_codeExactly one valid, unused second factor.
JSON response example
{
  "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.

Request fields for Request password recovery
FieldRequirement
emailAccount email address, at most 254 characters.
JSON response example
{
  "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.

Request fields for Reset a password
FieldRequirement
emailAccount email associated with the reset link.
tokenReset-link token, at most 200 characters.
passwordAt least 12 characters with letters and numbers; at most 72 UTF-8 bytes.
password_confirmationMust match the new password.
JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Verify a signed email link
FieldRequirement
expiresSigned expiry query parameter from the email link.
signatureSigned query parameter from the email link; preserve it unchanged.
JSON response example
{
  "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.

Request fields for Save an invoice draft
FieldRequirement
network_idbtc-bitcoin, usdt-tron, usdt-ethereum or usdt-polygon.
amount_unitsPositive canonical integer string with 1–28 digits; no leading zero, decimal point, sign or exponent.
referenceRequired nonempty string, at most 120 characters. References are not unique; the idempotency key identifies the request.
descriptionOptional string, at most 240 characters.
JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Read paginated ledger history
FieldRequirement
pageOptional query parameter, page number starting at 1.
per_pageOptional query parameter; default 20 entries.
JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Create a personal or merchant account
FieldRequirement
account_typeOptional personal or merchant; default merchant.
nameRequired string, up to 120 characters.
workspace_nameRequired for merchant, prohibited for personal; up to 120 characters.
emailRequired unique email, up to 254 characters.
passwordAt least 12 characters including letters and numbers; at most 72 UTF-8 bytes.
password_confirmationMust match password.
JSON response example
{
  "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.

Request fields for Start a browser session
FieldRequirement
emailRegistered email address.
passwordAccount password, at most 72 UTF-8 bytes.
JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

Request fields for Sign in a mobile device
FieldRequirement
emailRegistered email address.
passwordAccount password, at most 72 UTF-8 bytes.
device_nameRequired descriptive string, up to 80 characters.
JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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.

JSON response example
{
  "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
    }
  }
}
SIGNED CALLBACKS

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.

Node.js signature verification example
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.

REQUEST HANDLING

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.

API error statuses and suggested actions
StatusMeaningAction
400Invalid requestCheck endpoint-specific requirements, such as bearer authentication for device sign-out.
401Authentication requiredSign in again. Device tokens may have expired or been revoked.
403Access deniedThe current identity or token lacks the required permission.
404Resource unavailableCheck the ID and workspace membership. Hidden resources also return 404.
409State conflictResolve the stated conflict, such as an active browser account or the 10-device limit.
419Session / CSRF mismatchRefresh the CSRF cookie and sign in if the browser session has expired.
422Validation failedDisplay the field errors and correct the request before retrying.
429Rate limitedWait before retrying. Respect Retry-After when returned by the server.
5xxService unavailableDo not assume a write failed safely. Check its outcome before repeating it.
Example validation error · HTTP 422
{
  "message": "The email or password is incorrect.",
  "errors": {
    "email": ["The email or password is incorrect."]
  }
}