Using the API
Money out
The API can ask for money to leave a wallet. It can never make it leave. A person at your business approves every refund, payout, withdrawal and conversion in the dashboard with their passkey.
The rule
Orrerie is non-custodial. Each of your wallets needs two of three keys: your browser key, the recovery key in your Orrerie app, and Orrerie's co-signer. The browser key exists only encrypted, and only a passkey of an Owner or Finance member can open it, in a page on merchant.orrerie.com. An API key opens nothing. So an API call can never produce the first signature, and the co-signer never signs a transaction that lacks it.
| The API can | The API cannot, under any key |
|---|---|
| Read wallets, addresses, balances, the fee account, the ledger | Create, delete or change a wallet, an owner, a threshold |
| Create orders, sessions, links, invoices, subscriptions: everything that brings money in | Change where takings land (the settlement asset, rails, fee bearer): dashboard only |
| Request a refund, a payout, a withdrawal, a batch, a conversion | Approve, sign, co-sign or broadcast any of them |
| Propose a saved address | Make a saved address active, or shorten its 24-hour wait |
Cancel its own request while it is awaiting_approval | Cancel anything that is signed or later |
| Read every state of a request and receive its webhooks | Read or export any key, backup or passkey |
| Remove a saved address (this only narrows where money can go) | Invite or change an Owner or Finance member |
Money out can only be requested
There is no automatic approval, no approval by API key, no "trusted integration" switch, and no approval by email link. A business that wants unattended payouts cannot have them on this platform. That is the price of Orrerie never being able to move your money.
From request to money sent
The same sequence serves refunds, withdrawals, batches and conversions; only the wording on the approval screen differs.
- Your server asks.
POST /v1/payoutswith anIdempotency-Key. The server checks, in this order: the key's scope; that the destination is a saved address that isactive; that the destination still screens clear against sanctions lists; the tier's daily limit for payouts to others; the available balance; and that the fee account on the sending network can pay the network fee. Any failure answers at once with its error code and nothing is created. - The request exists. The answer is
202 Acceptedwith the payoutawaiting_approval, anapproval_urland anapproval_expires_atseven days ahead. The amount is reserved andpayout.createdis sent. Every Owner and Finance member gets an email and a badge in the dashboard. The email has no approve link that works without signing in, and never contains the full address. - A person approves. An Owner or Finance member opens the
approval_url, signs in, and sees who asked, the amount, the source wallet and its balance after, the saved address with its label and its age, the network fee and who pays it, and the limit used today. They press Approve and confirm with their passkey. They can instead press Reject: the API then shows the request ascancelledwithrejected_by_member. - Orrerie co-signs. The co-signer rebuilds the same transaction from its own record of the request, checks the first signature, and checks policy again at that moment. If it agrees, the payout is
signedandpayout.signedis sent. If it refuses, the payout isfailedwithfailure_code: "policy_refused"and nothing was broadcast. - It is sent. The transaction is broadcast:
sent, withtransaction_hash. - It ends.
confirmedwhen the network's confirmation count is reached;failedwhen the chain rejected it, and the reserve is released; orunknownwhen the outcome cannot yet be established.
Steps 1 and 2 take under a second. Step 3 takes as long as a person takes. If nobody approves within 7 days the request lapses back to draft with pending_, its reserve is released, and payout.approval_lapsed is sent; POST /v1/payouts/{id}/submit sends it for approval again. Steps 4 to 6 take seconds to minutes, by network.
unknown is a real state
A broadcast can time out. The transaction may be on its way or may never have left. The platform does not guess and does not retry blind. The payout shows unknown with failure_, keeps its reserve, and only evidence moves it: the transaction is found (sent, then confirmed), or it is proved never to have been broadcast (failed). No timer ends it.
Do not create a replacement payout while one is unknown
Wait for payout.confirmed or payout.failed. A replacement could pay twice.
Failure codes of refunds, payouts and conversions
failure_code | Meaning |
|---|---|
transaction_reverted | The transaction was mined and failed. Funds did not leave (the network fee was spent). |
insufficient_fee | The fee account ran short between request and send. |
balance_changed | The wallet's balance fell below the amount between request and approval. |
policy_refused | The co-signer refused: destination not an active saved address, or over a limit at signing time. |
destination_blocked | The destination screened as blocked at signing time. |
slippage_exceeded | Conversion or cross-asset delivery: the fresh quote at approval was below the minimum. |
relay_fill_below_minimum | The venue paid out less than the signed minimum; the conversion is not treated as confirmed. |
provider_route_failed | The venue could not complete and returned the funds to the merchant wallet. |
provider_refunded_underpayment | The provider returned a deposit that was too small for a strict address. |
deployment_failed | The wallet's first-send deployment failed; the send was not attempted. |
broadcast_outcome_unknown | State unknown only: the broadcast timed out and the chain does not yet show the transaction. |
dropped_from_mempool | Bitcoin family: the transaction was not mined within 14 days and its inputs are free again. |
Refunds, in particular
- By default you send no address. Orrerie makes a single-use link for the payer to give one, and the refund is
awaiting_addressuntil they do. Orrerie sends payers no email, so the dashboard shows the link and you give it to the payer yourself. It then waits for approval like any other request. - An order paid with the Orrerie app needs no address either. With no
destination, the refund goes to the payer's own Orrerie wallet in the order's settlement asset, with no claim link, and waits for approval. This holds only while all of the order's money came from that one wallet, in that asset on that network, and never for more than it sent: an order paid in part another way, from another wallet, or through a route from another network uses the claim link. - A refund goes without the 24-hour wait only when its address came from the payer through that claim link, or is the payer's own Orrerie wallet on an order paid with the Orrerie app; the order was paid less than 180 days ago; and the total stays within what the order was credited automatically.
- Allowance. No-wait refunds are limited to US$1,000 on the Unverified tier or US$100,000 for a Verified business per rolling 24 hours, counted separately from payouts. Above it the refund is not refused: it carries
approvable_at, 24 hours later. - An address typed by the merchant or sent by the API, and the address the payment came from, always fall under the saved-address rule. The refund carries
approvable_at, cannot be approved before it, and counts toward the tier's limit on payouts to others. - Rate. The refund amount is converted at the order's own locked rate by default, so you return what you received.
- Orrerie emails no payer in this release, so tell the payer yourself when the refund is
sent:refund.sentcarries itstransaction_hash. - Say "refund started", not "refunded". The call asks and a person approves, so tell your support agent "refund started", and mark the refund done on
refund.confirmed.
The JavaScript and Python samples use the small orrerie function of the quick start. They send the same Idempotency-Key on every retry of one refund, and branch on the error's code, never on its message.
curl
curl https://api.orrerie.com/v1/refunds \
-H "Authorization: Bearer $ORRERIE_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: refund-1042-1" \
-d '{
"order": "ord_0zymngngvf12b8b2cra7tb8e",
"amount": "5.00",
"reason": "requested_by_customer",
"note": "One bag arrived damaged"
}'
# 202 Accepted, "status": "awaiting_address"JavaScript
async function requestRefund(orderId, amount, ticket) {
try {
const refund = await orrerie('POST', '/refunds', {
order: orderId,
amount, // "5.00", in the order's pricing currency
reason: 'requested_by_customer',
note: `Support ticket ${ticket}`,
}, `refund-${orderId}-${ticket}`); // the same key on every retry of this refund
return { state: refund.status }; // "awaiting_address", later "awaiting_approval"
} catch (err) {
if (err.code === 'refund_amount_exceeds_refundable') return { state: 'too_much' };
if (err.code === 'customer_email_required') return { state: 'no_email_use_dashboard_qr' };
throw err; // 5xx or 504: retry later with the SAME key
}
}Python
def request_refund(order_id, amount, ticket):
try:
refund = orrerie("POST", "/refunds", {
"order": order_id,
"amount": amount, # "5.00"
"reason": "requested_by_customer",
"note": f"Support ticket {ticket}",
}, idempotency_key=f"refund-{order_id}-{ticket}") # the same key on every retry
return refund["status"] # "awaiting_address", later "awaiting_approval"
except OrrerieError as e:
if e.code == "refund_amount_exceeds_refundable":
return "too_much"
if e.code == "customer_email_required":
return "no_email_use_dashboard_qr"
raise # 5xx / 504: retry later with the SAME keyTo send a refund to an address your business has already saved and approved, name it as the destination. There is no claim link and no wait for the payer, and the refund starts as awaiting_approval:
JavaScript
await orrerie('POST', '/refunds', {
order: orderId, amount: '5.00', reason: 'requested_by_customer',
destination: { saved_address: 'dst_92jw0br3z1bzkk45ddjqcm6q' },
}, `refund-${orderId}-${ticket}`); // status "awaiting_approval"Network fees: what you must hold
When the fee account cannot pay for a send, the request fails with fee_account_low, whose needed field names the coin and the amount.
| Sending from | Network fee paid by | What fee_account_low asks for |
|---|---|---|
| Bitcoin, Litecoin, Dogecoin | Taken from the amount sent | Never raised. |
| Ethereum, BNB Smart Chain, Polygon, Arbitrum, Optimism, Base | The fee account's balance of that network's own coin, filled from takings by default | The coin and amount for this send, for example {"asset": "base:. |
| Solana | The fee account's SOL | The SOL for this send, plus rent the first time. |
| TRON | The TRON wallet account itself: TRON has no separate fee payer, so TRX and energy are charged to the sending account | The TRX for this send. needed.account is the wallet address to fund. |
A fee account fills itself only from orders between US$1 and US$10 on the settlement network. A business with no orders that small must send the coin once; GET /v1/fee_account shows topup so that an integration can tell.