Start
Quick start
A support email, a test key, a checkout session, a simulated payment and a verified webhook. Ten minutes, and no real money moves.
1. Add a support email
In the dashboard (sign up first if you have no account: it is free and takes about five minutes in a browser), open Settings, then Business profile, and fill in Support email, then enter the 6-digit code Orrerie emails to that address. Payers are shown it once it is confirmed, and no order can be created until the business has a confirmed one, test orders included. Without it, the call in step 4 is answered 409 with support_email_required, and the dashboard says "Add a support email so customers can reach you." That answer is kept with its Idempotency-Key for 24 hours, like every answer to a request that ran: once the email is in, send the call again with a new key, such as order-1042-attempt-2.
2. Get a test key
Create a test secret key in the dashboard. It starts with sk_test_. An Owner, an Admin or a Developer of the business can create one. Put it in the environment of your server, never in code:
Shell
export ORRERIE_SECRET_KEY=sk_test_…A test key cannot touch real money
With an sk_test_ key every payment is simulated and no payer is ever shown an address. The key in these samples is read from the environment; no page of these docs shows a real one.
3. Add your webhook endpoint
Tell Orrerie where your server listens before you take a payment: an event is sent to the endpoints that exist when it happens, and an endpoint added later does not receive the events before it. The signing secret, which starts with whsec_, is returned once: keep it beside your API key.
curl
curl https://api.orrerie.com/v1/webhook_endpoints \
-H "Authorization: Bearer $ORRERIE_SECRET_KEY" \
-H "Idempotency-Key: 8f14e45f-ceea-467f-a9d2-6f1c2b3d4e5a" \
-H "Content-Type: application/json" \
-d '{
"url": "https://acme.example/webhooks/orrerie",
"description": "Shop backend",
"enabled_events": [
"order.paid",
"order.underpaid",
"order.overpaid",
"order.unpaid",
"order.expired",
"refund.confirmed"
]
}'JavaScript
const response = await fetch("https://api.orrerie.com/v1/webhook_endpoints", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ORRERIE_SECRET_KEY}`,
"Idempotency-Key": "8f14e45f-ceea-467f-a9d2-6f1c2b3d4e5a",
"Content-Type": "application/json",
},
body: JSON.stringify({
"url": "https://acme.example/webhooks/orrerie",
"description": "Shop backend",
"enabled_events": [
"order.paid",
"order.underpaid",
"order.overpaid",
"order.unpaid",
"order.expired",
"refund.confirmed"
]
}),
});
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.post(
"https://api.orrerie.com/v1/webhook_endpoints",
headers={"Authorization": f"Bearer {os.environ['ORRERIE_SECRET_KEY']}", "Idempotency-Key": "8f14e45f-ceea-467f-a9d2-6f1c2b3d4e5a"},
json={
"url": "https://acme.example/webhooks/orrerie",
"description": "Shop backend",
"enabled_events": [
"order.paid",
"order.underpaid",
"order.overpaid",
"order.unpaid",
"order.expired",
"refund.confirmed",
],
},
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/webhook_endpoints');
curl_setopt_array($request, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('ORRERIE_SECRET_KEY'),
'Idempotency-Key: 8f14e45f-ceea-467f-a9d2-6f1c2b3d4e5a',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://acme.example/webhooks/orrerie',
'description' => 'Shop backend',
'enabled_events' => [
'order.paid',
'order.underpaid',
'order.overpaid',
'order.unpaid',
'order.expired',
'refund.confirmed',
],
]),
]);
$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);The list holds order.unpaid: the handler in step 6 stops fulfilment on it when a payment is undone on its chain.
Working on your own computer
A test endpoint still needs a public https address on port 443: Orrerie refuses localhost, a private or loopback address, plain http and any other port, with url_invalid. To receive deliveries on your own computer, put your local server behind an HTTPS tunnel of your choice and add the tunnel's address as the endpoint.
You can also build without an endpoint at all:
- Read the events. List events with
created[gte]set to the time you last read, and Retrieve an event, with your test key. Every event is kept for 30 days, whether or not an endpoint received it. In the dashboard, Developers, then Events, shows the same list. - Send one again. Once an endpoint exists, Resend an event queues a new delivery of a stored event to it, with the same body and a new signature timestamp.
- Check your signature code. The test vector and the checker on Receiving webhooks work before you have an endpoint, and the checker sends nothing anywhere.
4. Create a checkout session
One call creates the session and its order. Send the payer's browser to the url in the answer.
curl
curl https://api.orrerie.com/v1/checkout_sessions \
-H "Authorization: Bearer $ORRERIE_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-attempt-1" \
-d '{
"amount": "21.99",
"currency": "USD",
"description": "Order #1042",
"reference": "1042",
"customer_email": "jane@example.com",
"success_url": "https://acme.example/thanks?session={CHECKOUT_SESSION_ID}",
"cancel_url": "https://acme.example/cart"
}'
# Then send the payer's browser to the "url" in the answer:
# HTTP/1.1 303 See Other
# Location: https://pay.orrerie.com/c/cs_z7ek5wj68fyz80j7bz1eq4e0JavaScript
const API = 'https://api.orrerie.com/v1';
async function orrerie(method, path, body, idempotencyKey) {
const res = await fetch(API + path, {
method,
headers: {
Authorization: `Bearer ${process.env.ORRERIE_SECRET_KEY}`,
...(body ? { 'Content-Type': 'application/json' } : {}),
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json();
if (!res.ok) {
const err = new Error(json.error.message);
err.code = json.error.code; // branch on this, never on the message
err.status = res.status;
err.requestId = json.error.request_id;
throw err;
}
return json;
}
app.post('/checkout', async (req, res) => {
const order = await loadCart(req); // your own order: { number, total }
const session = await orrerie('POST', '/checkout_sessions', {
amount: order.total, // a string such as "21.99"
currency: 'USD',
description: `Order #${order.number}`,
reference: String(order.number),
success_url: 'https://acme.example/thanks?session={CHECKOUT_SESSION_ID}',
cancel_url: 'https://acme.example/cart',
}, `order-${order.number}-attempt-1`);
await saveSessionId(order.number, session.id, session.order);
res.redirect(303, session.url);
});Python
import os
import requests
from flask import redirect
API = "https://api.orrerie.com/v1"
class OrrerieError(Exception):
def __init__(self, status, error):
super().__init__(error["message"])
self.status, self.code, self.request_id = status, error["code"], error["request_id"]
def orrerie(method, path, body=None, idempotency_key=None, params=None):
headers = {"Authorization": "Bearer " + os.environ["ORRERIE_SECRET_KEY"]}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
r = requests.request(method, API + path, json=body, params=params, headers=headers, timeout=20)
data = r.json()
if not r.ok:
raise OrrerieError(r.status_code, data["error"]) # branch on .code, never on the message
return data
@app.post("/checkout")
def checkout():
order = load_cart() # your own order
session = orrerie("POST", "/checkout_sessions", {
"amount": order.total, # a string such as "21.99"
"currency": "USD",
"description": f"Order #{order.number}",
"reference": str(order.number),
"success_url": "https://acme.example/thanks?session={CHECKOUT_SESSION_ID}",
"cancel_url": "https://acme.example/cart",
}, idempotency_key=f"order-{order.number}-attempt-1")
save_session_id(order.number, session["id"], session["order"])
return redirect(session["url"], code=303)The JavaScript sample is an Express handler and the Python sample a Flask handler; the call inside each is the same as the curl command.
The answer is a checkout_session. Its order is the id you will see again in the webhook. The full list of fields is on Create a checkout session.
On your success_url page, show "Thank you, we are confirming your payment" and read your own order's state. Mark the order paid only from the webhook, or from Retrieve an order.
5. Simulate a payment
Open the session's url. In test mode the hosted page carries the banner "Test mode: no real payment is possible here.", and where the address and QR code would be it has a "Test payment" panel with Pay in full, Underpay by 10%, Overpay by 10%, Pay late, Let it expire and Fail routing. Press Pay in full.
Or do the same from your server, with the order's id:
curl
curl https://api.orrerie.com/v1/test/orders/ord_0zymngngvf12b8b2cra7tb8e/simulate_payment \
-H "Authorization: Bearer $ORRERIE_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"scenario": "exact",
"asset": "ethereum:usdc"
}'JavaScript
const response = await fetch("https://api.orrerie.com/v1/test/orders/ord_0zymngngvf12b8b2cra7tb8e/simulate_payment", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ORRERIE_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"scenario": "exact",
"asset": "ethereum:usdc"
}),
});
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.post(
"https://api.orrerie.com/v1/test/orders/ord_0zymngngvf12b8b2cra7tb8e/simulate_payment",
headers={"Authorization": f"Bearer {os.environ['ORRERIE_SECRET_KEY']}"},
json={
"scenario": "exact",
"asset": "ethereum:usdc",
},
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/test/orders/ord_0zymngngvf12b8b2cra7tb8e/simulate_payment');
curl_setopt_array($request, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('ORRERIE_SECRET_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'scenario' => 'exact',
'asset' => 'ethereum:usdc',
]),
]);
$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);The simulated payment walks the real states: seen, confirmed, then settled, over about 6 seconds. The order becomes paid and order.paid is sent with livemode: false.
6. Receive the webhook
order.paid now reaches the endpoint you added in step 3. Your endpoint must read the raw body, check the signature, and only then act. The four functions on checking a signature do the check; this is what handling the verified event looks like:
JavaScript
// after verifyOrrerieSignature(...) returned true
switch (event.type) {
case 'order.paid': // sent once; status is "paid" or "overpaid"
await markPaid(event.data.object.reference, event.data.object.id);
break;
case 'order.underpaid':
await flagShort(event.data.object.reference, event.data.object.amount_due);
break;
case 'order.unpaid': // a payment was undone by the chain
await holdFulfilment(event.data.object.reference);
break;
default:
break; // ignore types you do not use
}Python
# after verify_orrerie_signature(...) returned True
obj = event["data"]["object"]
if event["type"] == "order.paid": # sent once; status is "paid" or "overpaid"
mark_paid(obj["reference"], obj["id"])
elif event["type"] == "order.underpaid":
flag_short(obj["reference"], obj["amount_due"])
elif event["type"] == "order.unpaid": # a payment was undone by the chain
hold_fulfilment(obj["reference"])You can send your own endpoint a correctly signed test delivery from a shell before any event exists, and ask Orrerie to send a real ping:
curl
SECRET='whsec_…' # your endpoint's signing secret
BODY='{"id":"evt_local_test","object":"event","type":"ping","livemode":false,"data":{"object":{"object":"ping"}}}'
T=$(date +%s)
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
curl -i https://acme.example/webhooks/orrerie \
-H "Content-Type: application/json" \
-H "Orrerie-Signature: t=$T,v1=$SIG" \
-H "Orrerie-Event-Id: evt_local_test" \
-H "Orrerie-Event-Type: ping" \
-d "$BODY"
# and ask Orrerie to send a real one:
curl -X POST https://api.orrerie.com/v1/webhook_endpoints/we_tx7j2vmn3r2px2vpwbtz5h7b/ping \
-H "Authorization: Bearer $ORRERIE_SECRET_KEY"7. Fulfil on order.paid
When order.paid arrives and data.object.status is paid or overpaid, ship the goods or switch the service on. That event is sent once for an order. The payer's browser reaching your success_url proves nothing, so never fulfil from the redirect. To be sure before you ship, read the order with Retrieve an order and your secret key: its status is the one the event carried.
8. Go live
Going live is changing the key, once your business has made its wallets, saved its browser key backup and confirmed its recovery key in the Orrerie app: the dashboard asks for both before the first real payment. Objects do not move between modes: create your products, prices and payment links again with the live key, and add your live webhook endpoint in the dashboard, with a passkey (a live key cannot add one), then copy its signing secret to your server.
Limits until your business is verified
Until your business is verified it is on the Unverified tier, with these limits:
- Each of your first three paid orders can be up to US$250, and any later order up to US$1,000.
- Paid orders and unmatched payments you keep may total US$5,000 in any 30 days and US$20,000 in all. Once that total is reached, new orders are refused until you verify the business in the dashboard.
- At most 50 unpaid orders can be open at once.
- Payouts to others are limited to US$1,000 in any 24 hours; withdrawals to your own saved addresses have no tier limit.
A Verified business can take orders of up to US$50,000, and US$250,000 in any 30 days with no total limit, and pay out up to US$100,000 in 24 hours. Over a limit the API answers 403 with tier_order_limit, tier_volume_limit, tier_open_orders_limit or payout_daily_limit_reached (see the limit errors). Test mode does not enforce these limits, so plan for them before your first real sale.
Before the first real payment, read keeping funds safe and money out.
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 same in brief, without code: Crypto payments API and webhooks.