Ask for an invitation
Menu

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

Table: The hosted pages and their URL parameters
PageURLQuery parameters
Hosted checkouthttps://pay.orrerie.com/c/<cs_id>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-offhttps://pay.orrerie.com/o/<cs_id> (fallback orrerie://pay?session=<cs_id>)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 linkhttps://pay.orrerie.com/l/<plink_id>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.
Invoicehttps://pay.orrerie.com/i/<inv_id>locale. download=pdf returns the PDF.
Receipthttps://pay.orrerie.com/r/<rcpt_id>locale. download=pdf.
Refund claimhttps://pay.orrerie.com/rf/<rf_id>#<claim secret> (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 pagehttps://pay.orrerie.com/s/<sub_id>#<secret>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 profilehttps://pay.orrerie.com/m/<slug>locale.
Point-of-sale displayhttps://pay.orrerie.com/pos/<pos_id>None. The device is paired by a signed-in member; the URL alone shows nothing.
Test simulationhttps://pay.orrerie.com/c/<cs_id> of a test-mode sessionThe 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:

Table: Return URLs: Return URLs (1)
PlaceholderReplaced 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:

Table: Return URLs: Return URLs (2)
Order stateWhat the payer sees
paid or overpaidThe Paid page, then your success_url. The page goes there by itself after 5 seconds and offers Stay here.
underpaidThe 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 returnYour 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

Table: After the payer has sent
What happenedOrder statusHeadline on the page
A payment was seen on the payer's networkprocessing"Payment seen." The address and the QR code are removed.
The money is in your wallet, shown by your wallet's own chain readpaid"Paid."
Less than the price arrivedunderpaid"Part paid." with Pay the rest
More than the price arrivedoverpaid"Paid, with the extra amount extra."
The 15-minute payment details ran out and nothing was seenopen"These payment details have expired." with Get new details
The order's own time ran out and nothing was seenexpired"This order has expired."
You cancelled the order or voided its invoicecancelled"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.