Developers

The API, as it is being designed.

A design document rather than live documentation — no public keys or sandbox yet. Published early because the people who would embed a receiving account in their own billing system are the ones who can tell us what is wrong with the shape.

Payment intents, then payments

The shape follows the model: an account is a persistent identity, an intent says what a payment is for, and a payment is what actually arrived against it.

POST/payment-intents

Declare what you are expecting — invoice, payer, amount, accepted assets, expiry. Returns the instructions to send your customer.

GET/payment-intents/:id

Whether it is still awaiting payment, matched, or expired.

GET/payments/:id

The lifecycle of a real arrival, then the final record with every cost line and the reference rate with its source.

GET/accounts/:id

The account, its status and its verified settlement destination.

Creating an intent

Quotes are indicative unless the underlying provider guarantees execution. A locked rate needs somebody to carry FX risk between quote and execution, which needs pre-funded balances we do not have.

Request

# planned POST /v1/payment-intents { "account_id": "NP-847291", "amount": "40000.00", "currency": "USD", "invoice_reference": "INV-1034", "payer": { "name": "Acme Inc." }, "description": "Software development services", "expires_at": "2026-09-20T23:59:59Z" }

Response

{ "payment_intent_id": "PI-83942", "account_id": "NP-847291", "amount": "40000.00", "currency": "USD", "status": "awaiting_payment", "accepted": [ { "asset": "USDC", "network": "base" }, { "asset": "USDT", "network": "tron" } ] }

The accepted list is dynamic. It reflects what is genuinely supported and available at that moment, so a payment page never offers a rail that would fail. There is no "any" value, and there never will be.

Partner identities are not returned. Custody, liquidity and conversion partners carry opaque ids. You get the economics, the route class and the timing, not the name of the company executing.

A payment, matched

An arrival is reconciled against its intent before anything converts, including when it lands short.

GET /v1/payments/NP-83942 { "account_id": "NP-847291", "payment_intent_id": "PI-83942", "asset": "USDC", "network": "base", "amount": "39950.000000", "tx_hash": "0x7f2a...9c14", "confirmations": 14, "screening_status": "clear", "status": "matched_with_variance", "variance": "-50.00", "tolerance": "100.00" }

Payment states

Named states, and two of them matter more than the rest.

CREATED → AWAITING_PAYMENT → DETECTED → CONFIRMING → SCREENING → MATCHED → ROUTING → NORMALIZING → CONVERTING → SETTLING → SETTLED // branches SCREENING → REVIEW | BLOCKED ROUTING → ROUTE_UNAVAILABLE CONVERTING → FAILED SETTLING → FAILED ANY AMBIGUOUS STATE → UNKNOWN

UNKNOWN never auto-retries. If a provider times out after we send an execution instruction, we do not know whether it executed. Re-sending a $500,000 conversion sends it twice. We query the provider, reconcile, and retry only once the original is definitively confirmed not to have happened.

Webhooks

Signed, at-least-once, replayable. A missed event is never a lost payment.

payment.created payment.routing_started payment.detected payment.conversion_started payment.confirmed payment.settlement_started payment.screening_started payment.settled payment.screening_completed payment.failed payment.matched payment.review_required payment.blocked

Design commitments

Choices being made now because they are painful to retrofit later.

Idempotency on every write

Every external operation takes a key derived from payment, operation and attempt. No conversion or settlement request is ever retried blindly.

Amounts as strings, in minor units

No floats anywhere. Currency precision differs and rounding errors in money are unforgivable.

Asset and network are always a pair

There is no bare "USDT" in this API. An asset without its network is not an asset, and an API that lets you omit it invites the exact mistake the account exists to prevent.

Confirmation policy is explicit and per network

Returned with every payment, not hidden in a boolean. Reorgs are handled rather than assumed away.

Double-entry from transaction one

Never a simple balance field as the source of truth. Every economic event writes ledger entries.

Every external call logged

Request and response to each node, liquidity venue and provider, retained. Without that, reconciliation is guesswork and disputes are unwinnable.

Tell us what is wrong with this.

If you would embed a receiving account in your own billing system, say which part of the shape above would annoy you in production. That is worth more to us right now than a signup.