Taking payments
The hosted checkout
Every payer page is on pay.orrerie.com. This page lists each one with the parameters it reads, says where the payer is sent afterwards, and shows what the payer sees on each of the three ways to pay.
The rule for every address
The path of a payer page carries only a random id. A query parameter may change how the page looks or pre-select a choice. It can never set an amount on a fixed-price object, a destination, an asset you have not enabled, or any personal data. An unknown parameter is ignored, so a page does not break when an email program adds tracking text to a link.
No address or amount is ever in a link
The amount, the destination and the refund wallet of an order are set by Orrerie's server from what you created with your key. Nothing a visitor can edit in the address bar changes where money goes or how much is asked.
Link to the checkout, never copy it
Send the payer to the checkout's own address on pay.orrerie.com. Do not copy the checkout into your own page, and do not show a deposit address anywhere but through Orrerie's page, the API or an official plug-in. A copy of the checkout on another address is what a phishing page looks like: tell your payers to check that the address bar reads pay.orrerie.com before they pay, and check that it reads merchant.orrerie.com before you sign in yourself.
Not for your own app's digital content
Do not take payment through Orrerie for goods or content that your own iPhone or Android app gives access to. The App Store and Google Play require their own in-app purchase for that, and the Merchant Terms forbid it.
The pages and their parameters
| Page | URL | Query parameters |
|---|---|---|
| Hosted checkout | https:// | locale = auto | en. asset = an asset id to pre-select, for example base:usdc (ignored if not offered for this order; the payer can still change it). rail = orrerie_app | relay | direct to open that tab first (ignored if not allowed). Nothing else. |
| Pay with Orrerie hand-off | https:// (fallback orrerie://) | None. The app fetches the order by session id and shows its own review. If the app is not installed the page shows the store links and "Or pay from any wallet" leading to /c/<cs_id>. |
| Payment link | https:// | quantity = integer within the link's limits. amount = decimal string, only for amount_type: "open" links and clamped to min_amount..max_amount. reference = up to 200 of A-Z a-z 0-9 _ -, becomes client_reference_id. locale. No email parameter: an address in a URL ends up in logs and referrers; the payer types it, or your server creates the session with customer_email. |
| Invoice | https:// | locale. download=pdf returns the PDF. |
| Receipt | https:// | locale. download=pdf. |
| Refund claim | https:// (keyed by the refund, because an underpaid order, a returned payment and an unmatched payment have no receipt; a receipt page links to it when one exists) | None. The secret (32 random bytes, stored only as a hash) is in the URL fragment, which browsers never send to a server, so it reaches no log. Single use. Never returned by the API. |
| Subscriber's page | https:// | None. The sub_ id is an ordinary object id seen in API answers, webhooks and exports, so it opens nothing: the page needs a separate 128-bit secret in the fragment, stored only as a hash and given to the customer only on the paid page of the order that started the subscription, in the one browser that chose the way to pay and alone saw it, while Orrerie emails no customer (never on a receipt, and never to anyone who holds only the order's ids; otherwise the customer asks the business). Never returned by the API. |
| Merchant profile | https:// | locale. |
| Point-of-sale display | https:// | None. The device is paired by a signed-in member; the URL alone shows nothing. |
| Test simulation | https:// of a test-mode session | The same page with the test banner and simulate buttons. |
A checkout session's address comes back as url when you create the session. Send the payer's browser there as a top-level navigation. The checkout cannot be shown inside a frame: every payer page is served with a policy that forbids it, so that no site can draw its own content over an address or a pay button.
Return URLs
success_url and cancel_url are given when the session or the payment link is created. Both must be https. Each may contain two placeholders that the checkout replaces:
| Placeholder | Replaced with |
|---|---|
{CHECKOUT_SESSION_ID} | The id of the session, which starts with cs_. |
{ORDER_ID} | The id of its order, which starts with ord_. |
Nothing else is added to the address, and nothing in it is signed. When the payer arrives, look the session up from your own server with your key.
curl
curl https://api.orrerie.com/v1/checkout_sessions/cs_z7ek5wj68fyz80j7bz1eq4e0 \
-H "Authorization: Bearer $ORRERIE_SECRET_KEY"JavaScript
const response = await fetch("https://api.orrerie.com/v1/checkout_sessions/cs_z7ek5wj68fyz80j7bz1eq4e0", {
headers: {
Authorization: `Bearer ${process.env.ORRERIE_SECRET_KEY}`,
},
});
const result = await response.json();
if (!response.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
console.log(result);Python
import os
import requests
response = requests.get(
"https://api.orrerie.com/v1/checkout_sessions/cs_z7ek5wj68fyz80j7bz1eq4e0",
headers={"Authorization": f"Bearer {os.environ['ORRERIE_SECRET_KEY']}"},
timeout=30,
)
result = response.json()
if not response.ok:
raise RuntimeError(f"{result['error']['code']}: {result['error']['message']}")
print(result)PHP
<?php
$request = curl_init('https://api.orrerie.com/v1/checkout_sessions/cs_z7ek5wj68fyz80j7bz1eq4e0');
curl_setopt_array($request, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('ORRERIE_SECRET_KEY'),
],
]);
$body = curl_exec($request);
if ($body === false) {
throw new RuntimeException(curl_error($request));
}
$result = json_decode($body, true);
if (curl_getinfo($request, CURLINFO_HTTP_CODE) >= 400) {
throw new RuntimeException($result['error']['code'] . ': ' . $result['error']['message']);
}
print_r($result);Where the payer is sent depends on the state of the order:
| Order state | What the payer sees |
|---|---|
paid or overpaid | The Paid page, then your success_url. The page goes there by itself after 5 seconds and offers Stay here. |
underpaid | The payer stays on the checkout, which offers Pay the rest. |
processing on Bitcoin, Litecoin or Dogecoin | "Payment seen." and "Waiting for the network to confirm it. Keep this page or save the link: it is your receipt.", with a Return to link that names your site and goes to your success_url, because a payer should not have to keep a tab open for an hour. |
| The payer presses Cancel and return | Your cancel_url. The link is shown only when the order has one. |
The redirect proves nothing
A payer can reach your success_url before the order is paid, and anyone can type the address. Your thank-you page must cope with an order that is not yet paid. Ship on the order.paid webhook, or after reading the order with your key, and never on the browser coming back.
What the payer sees
The checkout shows your logo and name, what is being bought, the amount in your pricing currency and the order reference. It asks for no sign-in and no identity. The payer then chooses how to pay. The way that was used is written on the payment instruction as rail, and it never changes by itself.
Pay with Orrerie
The first and largest button. On a phone it opens the order in the Orrerie app. On a desktop it shows a QR code to scan with the app. The payer reviews "Pay" with your business name and the amount, picks a balance that can pay it, and approves once with Face ID or a fingerprint. No address is shown at any point. The checkout page reads "Waiting for approval in the Orrerie app…" and then follows the same states as any other payment. This is the orrerie_app rail.
The button is shown only when you settle in a token on a network Relay delivers to (networks); for now a business that keeps what was paid, or settles in a network's own coin, gets no button, and its payers use the ways below. When the payer pays from the settlement asset itself, the app sends the order amount plus a few extra digits at the sixth decimal place (always under one cent), as the direct rail does; the figure the app shows holds for 15 minutes, after which the payer opens the link again. An order paid with the app is refunded to the payer's own Orrerie wallet when you give no address (money out).
Any wallet, routed by Relay
The payer picks an asset and a network, and the page shows one address made for this order, the exact amount to send, a QR code and a timer of 15 minutes. The payer can pay in a different asset from the one you settle in: Relay, a separate company, receives the payment and delivers your settlement asset to your own wallet. This is the relay rail, and it is the default for every asset Relay can route.
What the payer pays on this rail: by default the amount to send includes Relay's routing fee and the network cost, so you receive the price in full. The page shows the amount under "Send exactly" and, on its own line under it, the price and that cost: "US$50.00 + about US$0.31 routing and network cost". For a payer on Bitcoin, Solana or TRON, the cost of the address Relay opens for the order is inside that line too: the address is asked for by the amount that must arrive, so it is never taken out of your price and never hidden.
If you absorb the fee instead (fee_bearer: merchant), the payer is shown the price alone under "Send", with no cost line, and the order is paid when at least 98% of it arrives. A route whose fee is above your cap (3% or US$25 unless you change it) is refused with rail_unavailable, whose reason is route_fee_over_cap, and the payer chooses another asset.
The page tells the payer whose address it is: "This address belongs to Relay, a routing service. Relay delivers your payment to your business's wallet. It is for this order only: don't save or reuse it." After the payment is seen, the page shows three steps: "Received by Relay", "Routing to your business", and "Arrived in your business's wallet".
While a payment is being routed
Between the payer's payment and its delivery, which takes seconds to minutes, the value is with Relay. The API says so on every payment instruction of this rail, in in_transit_notice: "While a payment is being routed it is held by Relay, a separate company. Orrerie never holds it." Your order detail in the dashboard carries the same sentence.
If a routed payment cannot be delivered, Relay returns it to your wallet on the payer's network, not to the payer. The checkout says so before the payer sends anything, and you then refund the payer or count the money toward the order.
Any wallet, straight to yours
The address shown is your own wallet's, "This address is your business's own wallet.", where Relay has no route (Litecoin, Dogecoin, POL and TRX), where you set an asset to direct only, for an order below the relay rail's US$1.00 minimum, for Bitcoin when you keep BTC, and when the payer, after Relay refused, chooses to pay straight to your wallet. The instruction's rail_reason says which. This is the direct rail. On Bitcoin, Litecoin and Dogecoin each order gets a fresh address of your wallet. On the other networks the address is your wallet's one address and the amount carries a few extra digits at the end, which is how the payment is matched to the order: "Send this exact amount, to the last digit."
When a way to pay is not available
A way to pay that cannot be used is greyed in the list with its reason: "Not available right now", "Order too small for this network" or "Not accepted by this business". If Relay does not answer when the payer has chosen, the page says so, says "Nothing was charged", and asks the payer to choose another way. Nothing switches to another rail by itself. If you build your own payment page with payment instructions, the same refusal reaches you as the error rail_unavailable with the alternatives.
After the payer has sent
| What happened | Order status | Headline on the page |
|---|---|---|
| A payment was seen on the payer's network | processing | "Payment seen." The address and the QR code are removed. |
| The money is in your wallet, shown by your wallet's own chain read | paid | "Paid." |
| Less than the price arrived | underpaid | "Part paid." with Pay the rest |
| More than the price arrived | overpaid | "Paid, with the extra amount extra." |
| The 15-minute payment details ran out and nothing was seen | open | "These payment details have expired." with Get new details |
| The order's own time ran out and nothing was seen | expired | "This order has expired." |
| You cancelled the order or voided its invoice | cancelled | "This order was cancelled by your business." |
An address cannot be taken back once it has been shown. A payment that arrives after its details expired is still credited to its order at the amount that arrived, and the page reads "Paid late." The states every object uses lists every state with its meaning.
Languages
locale is auto or en. auto reads the browser's language and falls back to English. The parameter exists now so that a language added later changes no address.
Test mode
A session made with an sk_test_ key has the same address shape. Its page carries the banner "Test mode: no real payment is possible here.", and where the address and the QR code would be it has a "Test payment" panel whose buttons simulate a payment. No address is ever shown in test mode. Test mode has the helpers that do the same from your server.