Ask for an invitation
Menu

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.

Table: The rule
The API canThe API cannot, under any key
Read wallets, addresses, balances, the fee account, the ledgerCreate, delete or change a wallet, an owner, a threshold
Create orders, sessions, links, invoices, subscriptions: everything that brings money inChange where takings land (the settlement asset, rails, fee bearer): dashboard only
Request a refund, a payout, a withdrawal, a batch, a conversionApprove, sign, co-sign or broadcast any of them
Propose a saved addressMake a saved address active, or shorten its 24-hour wait
Cancel its own request while it is awaiting_approvalCancel anything that is signed or later
Read every state of a request and receive its webhooksRead 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.

  1. Your server asks. POST /v1/payouts with an Idempotency-Key. The server checks, in this order: the key's scope; that the destination is a saved address that is active; 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.
  2. The request exists. The answer is 202 Accepted with the payout awaiting_approval, an approval_url and an approval_expires_at seven days ahead. The amount is reserved and payout.created is 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.
  3. 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 as cancelled with rejected_by_member.
  4. 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 signed and payout.signed is sent. If it refuses, the payout is failed with failure_code: "policy_refused" and nothing was broadcast.
  5. It is sent. The transaction is broadcast: sent, with transaction_hash.
  6. It ends. confirmed when the network's confirmation count is reached; failed when the chain rejected it, and the reserve is released; or unknown when 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_reason: "approval_lapsed", 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_code: "broadcast_outcome_unknown", 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

Table: Failure codes of refunds, payouts and conversions
failure_codeMeaning
transaction_revertedThe transaction was mined and failed. Funds did not leave (the network fee was spent).
insufficient_feeThe fee account ran short between request and send.
balance_changedThe wallet's balance fell below the amount between request and approval.
policy_refusedThe co-signer refused: destination not an active saved address, or over a limit at signing time.
destination_blockedThe destination screened as blocked at signing time.
slippage_exceededConversion or cross-asset delivery: the fresh quote at approval was below the minimum.
relay_fill_below_minimumThe venue paid out less than the signed minimum; the conversion is not treated as confirmed.
provider_route_failedThe venue could not complete and returned the funds to the merchant wallet.
provider_refunded_underpaymentThe provider returned a deposit that was too small for a strict address.
deployment_failedThe wallet's first-send deployment failed; the send was not attempted.
broadcast_outcome_unknownState unknown only: the broadcast timed out and the chain does not yet show the transaction.
dropped_from_mempoolBitcoin 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_address until 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.sent carries its transaction_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 key

To 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.

Table: Network fees: what you must hold
Sending fromNetwork fee paid byWhat fee_account_low asks for
Bitcoin, Litecoin, DogecoinTaken from the amount sentNever raised.
Ethereum, BNB Smart Chain, Polygon, Arbitrum, Optimism, BaseThe fee account's balance of that network's own coin, filled from takings by defaultThe coin and amount for this send, for example {"asset": "base:eth", "amount": "0.00002"}.
SolanaThe fee account's SOLThe SOL for this send, plus rent the first time.
TRONThe TRON wallet account itself: TRON has no separate fee payer, so TRX and energy are charged to the sending accountThe 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.