Ask for an invitation
Menu

Guides

Subscriptions

A subscription is a schedule of orders that your customer pays one by one. Nothing is ever pulled from a payer's wallet. Orrerie creates each period's order and tells your server whether it was paid; you send the customer its pay link.

Nobody is charged automatically

With a card, a shop can take the next payment by itself. In crypto nobody can: neither you nor Orrerie can move money out of a customer's wallet, and Orrerie never asks for a permission that would allow it. So each period works like this:

  1. Before the period ends, an order is createdBy default 7 days before. Orrerie does not email customers yet: you send the customer the order's pay link, from your server or from the dashboard.
  2. The customer pays itOn the hosted checkout, in any asset you accept. A customer who paid with the Orrerie app can switch on a reminder there and renew with one approval.
  3. The period moves onThe next period starts where the last one ended, so paying early loses nothing.
  4. If it is not paid, you are toldFirst past_due, then after your grace time unpaid. Your own system decides what to switch off.

Plan for a renewal that is not paid

A subscription paid in crypto renews only when the customer pays. Build your product so that a period can end unpaid: listen for subscription.unpaid and withdraw access then.

Three ways a customer can renew

Table: Three ways a customer can renew
WayWhat the customer doesWho can use it
Remind and payGets the next period's pay link from you before each period ends. This is the default.Every payer, on every network.
Pay aheadPays for several periods at once at checkout, with a discount if you offer one.Every payer.
One-tap renewal in the Orrerie appSwitches on Remind me in the Orrerie app. At renewal the app shows a notification that opens the review for the new order, and one approval renews.Payers who use the Orrerie app.

Set up a plan

A plan is a recurring price on a product. In the dashboard open Products, choose a product, then Add price and Recurring. Set the amount, how often it repeats, the pay-ahead choices, the grace time and the reminder days.

Add price with Recurring chosen: US$20.00 every month, the pay-ahead choices, a grace time of 3 days and reminders 7, 3 and 1 days before the period ends.
Add price, with Recurring chosen. Open the full picture

With the API, create the price with recurring:

curl

curl https://api.orrerie.com/v1/prices \
  -H "Authorization: Bearer $ORRERIE_SECRET_KEY" \
  -H "Idempotency-Key: 8f14e45f-ceea-467f-a9d2-6f1c2b3d4e5a" \
  -H "Content-Type: application/json" \
  -d '{
  "product": "prod_9cct573e5ewmrj7f6hyjsqap",
  "amount": "18.00",
  "currency": "USD",
  "type": "recurring",
  "recurring": {
    "interval": "month",
    "interval_count": 1
  },
  "nickname": "Monthly bag"
}'

JavaScript

const response = await fetch("https://api.orrerie.com/v1/prices", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ORRERIE_SECRET_KEY}`,
    "Idempotency-Key": "8f14e45f-ceea-467f-a9d2-6f1c2b3d4e5a",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "product": "prod_9cct573e5ewmrj7f6hyjsqap",
    "amount": "18.00",
    "currency": "USD",
    "type": "recurring",
    "recurring": {
      "interval": "month",
      "interval_count": 1
    },
    "nickname": "Monthly bag"
  }),
});
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/prices",
    headers={"Authorization": f"Bearer {os.environ['ORRERIE_SECRET_KEY']}", "Idempotency-Key": "8f14e45f-ceea-467f-a9d2-6f1c2b3d4e5a"},
    json={
        "product": "prod_9cct573e5ewmrj7f6hyjsqap",
        "amount": "18.00",
        "currency": "USD",
        "type": "recurring",
        "recurring": {
            "interval": "month",
            "interval_count": 1,
        },
        "nickname": "Monthly bag",
    },
    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/prices');
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([
        'product' => 'prod_9cct573e5ewmrj7f6hyjsqap',
        'amount' => '18.00',
        'currency' => 'USD',
        'type' => 'recurring',
        'recurring' => [
            'interval' => 'month',
            'interval_count' => 1,
        ],
        'nickname' => 'Monthly bag',
    ]),
]);
$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);

Start a subscription

A subscription starts in one of two ways:

  • A member presses Create subscription on a customer in the dashboard.
  • Your server creates it. The customer must have an email address.

A payment link cannot carry a recurring price in this release: a link takes one-time prices only.

curl

curl https://api.orrerie.com/v1/subscriptions \
  -H "Authorization: Bearer $ORRERIE_SECRET_KEY" \
  -H "Idempotency-Key: 8f14e45f-ceea-467f-a9d2-6f1c2b3d4e5a" \
  -H "Content-Type: application/json" \
  -d '{
  "customer": "cus_8ea07c40qsvv19f7rgmeb8ha",
  "items": [
    {
      "price": "price_s5n6hhtmyv5erqb81efqgjy6",
      "quantity": 1
    }
  ]
}'

JavaScript

const response = await fetch("https://api.orrerie.com/v1/subscriptions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ORRERIE_SECRET_KEY}`,
    "Idempotency-Key": "8f14e45f-ceea-467f-a9d2-6f1c2b3d4e5a",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "customer": "cus_8ea07c40qsvv19f7rgmeb8ha",
    "items": [
      {
        "price": "price_s5n6hhtmyv5erqb81efqgjy6",
        "quantity": 1
      }
    ]
  }),
});
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/subscriptions",
    headers={"Authorization": f"Bearer {os.environ['ORRERIE_SECRET_KEY']}", "Idempotency-Key": "8f14e45f-ceea-467f-a9d2-6f1c2b3d4e5a"},
    json={
        "customer": "cus_8ea07c40qsvv19f7rgmeb8ha",
        "items": [
            {
                "price": "price_s5n6hhtmyv5erqb81efqgjy6",
                "quantity": 1,
            },
        ],
    },
    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/subscriptions');
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([
        'customer' => 'cus_8ea07c40qsvv19f7rgmeb8ha',
        'items' => [
            [
                'price' => 'price_s5n6hhtmyv5erqb81efqgjy6',
                'quantity' => 1,
            ],
        ],
    ]),
]);
$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 subscription starts incomplete, with its first order in latest_order. Send the customer that order's pay link (below). It becomes active when that order is paid. Create a subscription lists reminder_days, grace_days, prepaid_periods_allowed and proration.

Subscription created: Coffee club for Jonas Wirth, which starts when the first order is paid, with the first order's pay link and a message to send from your own email.
A subscription made from a customer in the dashboard, with its first pay link to send. Open the full picture
A subscription in the dashboard, Active, with its current period, what has been paid so far and its first order paid.A subscription in the dashboard, Active, with its current period, what has been paid so far and its first order paid.
The subscription once its first order is paid. Open the full picture

Orrerie does not email customers yet, so each period's pay link comes from you. When the subscription is made, and again on each subscription.renewal_due, create a checkout session for the subscription's latest_order and send the customer the session's url:

curl

curl https://api.orrerie.com/v1/checkout_sessions \
  -H "Authorization: Bearer $ORRERIE_SECRET_KEY" \
  -H "Idempotency-Key: 8f14e45f-ceea-467f-a9d2-6f1c2b3d4e5a" \
  -H "Content-Type: application/json" \
  -d '{
  "order": "ord_56zb2faf1zrt24zjgrrqxfh4",
  "success_url": "https://acme.example/account"
}'

JavaScript

const response = await fetch("https://api.orrerie.com/v1/checkout_sessions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ORRERIE_SECRET_KEY}`,
    "Idempotency-Key": "8f14e45f-ceea-467f-a9d2-6f1c2b3d4e5a",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "order": "ord_56zb2faf1zrt24zjgrrqxfh4",
    "success_url": "https://acme.example/account"
  }),
});
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/checkout_sessions",
    headers={"Authorization": f"Bearer {os.environ['ORRERIE_SECRET_KEY']}", "Idempotency-Key": "8f14e45f-ceea-467f-a9d2-6f1c2b3d4e5a"},
    json={
        "order": "ord_56zb2faf1zrt24zjgrrqxfh4",
        "success_url": "https://acme.example/account",
    },
    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');
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([
        'order' => 'ord_56zb2faf1zrt24zjgrrqxfh4',
        'success_url' => 'https://acme.example/account',
    ]),
]);
$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 session pays that order and nothing else. In the dashboard, Send renewal link now on the subscription gives you the same link with a message ready to send from your own email.

A subscription due for renewal, with Send renewal link now's message ready to send from your own email.
Send renewal link now, with the message to send. Open the full picture

The cycle

Take a monthly plan of US$20.00 with the default reminders of 7, 3 and 1 days and a grace time of 3 days, first paid on 11 October.

Table: The cycle
WhenWhat Orrerie doesEvent
11 October, first order paidThe subscription is active and runs until 11 November. The receipt says it does not renew by itself.subscription.activated
4 November, 7 days before the endCreates the renewal order, payable until 14 November. Send the customer its pay link.subscription.renewal_due
8 and 10 NovemberReminder days, if the order is still unpaid: the dashboard offers you a reminder to send, and the Orrerie app reminds a payer who switched its reminder on.None
Paid on any day up to 11 NovemberThe next period runs from 11 November to 11 December.subscription.renewed
11 November, unpaidpast_due. Your service should still work during the grace time.subscription.past_due
14 November, unpaidunpaid, and the renewal order expires. This is when to withdraw access.subscription.unpaid
30 days unpaidended.subscription.ended

A period that ends on the 31st ends on the last day of a shorter month. A customer whose subscription is unpaid can restart it: a renewal order paid then makes it active again.

States

Table: States
statusWhat it means
incompleteMade, first order not yet paid.
incomplete_expiredThe first order expired unpaid. Final.
activePaid up.
past_dueThe period ended unpaid and the grace time is running.
unpaidThe grace time is over. People see "Lapsed".
pausedYou paused it. No orders and no reminders are made. Time already paid for keeps running.
cancelledCancelled by you or by the customer.
endedOver, after 30 days unpaid or after its last period.
A subscription whose renewal was not paid after the grace time: Lapsed, with its renewal order expired.A subscription whose renewal was not paid after the grace time: Lapsed, with its renewal order expired.
A subscription whose renewal went unpaid past the grace time. Open the full picture

Part payment of a period

proration decides what a renewal paid in part does:

  • none, the default: the order is underpaid, the period does not move, and the customer is asked for the rest.
  • proportional: time is given in proportion to what was paid, rounded down to whole days. Half the price buys half the days.

A price change applies from the next order that is created. An order that already exists keeps its amount, and there is no charge for part of a period.

Pause, cancel, change

curl

curl https://api.orrerie.com/v1/subscriptions/sub_9q5v9rq2naessbxsrec2tsw0/pause \
  -H "Authorization: Bearer $ORRERIE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

JavaScript

const response = await fetch("https://api.orrerie.com/v1/subscriptions/sub_9q5v9rq2naessbxsrec2tsw0/pause", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ORRERIE_SECRET_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({}),
});
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/subscriptions/sub_9q5v9rq2naessbxsrec2tsw0/pause",
    headers={"Authorization": f"Bearer {os.environ['ORRERIE_SECRET_KEY']}"},
    json={},
    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/subscriptions/sub_9q5v9rq2naessbxsrec2tsw0/pause');
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(new stdClass()),
]);
$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);

curl

curl https://api.orrerie.com/v1/subscriptions/sub_9q5v9rq2naessbxsrec2tsw0/cancel \
  -H "Authorization: Bearer $ORRERIE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "Customer asked by email"
}'

JavaScript

const response = await fetch("https://api.orrerie.com/v1/subscriptions/sub_9q5v9rq2naessbxsrec2tsw0/cancel", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ORRERIE_SECRET_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "reason": "Customer asked by email"
  }),
});
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/subscriptions/sub_9q5v9rq2naessbxsrec2tsw0/cancel",
    headers={"Authorization": f"Bearer {os.environ['ORRERIE_SECRET_KEY']}"},
    json={
        "reason": "Customer asked by email",
    },
    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/subscriptions/sub_9q5v9rq2naessbxsrec2tsw0/cancel');
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([
        'reason' => 'Customer asked by email',
    ]),
]);
$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);

Cancelling ends the subscription now and cancels its open renewal order. It is refused with instruction_in_flight while a payment for that order is on its way. To stop at the end of the paid period instead, update the subscription with cancel_at_period_end: true.

Cancelling refunds nothing

No money moves when a subscription is cancelled. If you owe the customer for unused time, request a refund of the order.

What your customer has

The customer's own page is https://pay.orrerie.com/s/<sub_id>#<secret>. It shows your business, the product, "Paid until the date" and past receipts, with Pay next period, Pay ahead, Remind me in the Orrerie app and Stop reminders and cancel. While Orrerie emails no customer, the link is shown on the paid page of the order that started the subscription, in the one browser that chose the way to pay and was the only one to see it. It is never on a receipt and never shown to anyone who holds only the order's ids. If the payment details were seen anywhere else before the order was paid (a second browser, your server through the API, the order's page in your dashboard) or the Orrerie app paid, the paid page asks the customer to get the link from you instead. A customer who lost it asks you too, and you change or cancel the subscription from the dashboard.

The page opens only with the secret after the #, which is never returned by the API. The sub_ id by itself opens nothing.

Try it in test mode

Test mode has a clock you can move, so a month takes a second. Create the subscription with an sk_test_ key, pay its first order with Pay in full on its test checkout, then advance the clock and watch the events arrive:

curl

curl https://api.orrerie.com/v1/test/subscriptions/sub_9q5v9rq2naessbxsrec2tsw0/advance \
  -H "Authorization: Bearer $ORRERIE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "to": "renewal_opens"
}'

JavaScript

const response = await fetch("https://api.orrerie.com/v1/test/subscriptions/sub_9q5v9rq2naessbxsrec2tsw0/advance", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ORRERIE_SECRET_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "to": "renewal_opens"
  }),
});
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/subscriptions/sub_9q5v9rq2naessbxsrec2tsw0/advance",
    headers={"Authorization": f"Bearer {os.environ['ORRERIE_SECRET_KEY']}"},
    json={
        "to": "renewal_opens",
    },
    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/subscriptions/sub_9q5v9rq2naessbxsrec2tsw0/advance');
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([
        'to' => 'renewal_opens',
    ]),
]);
$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);

to names the moment to jump to:

Table: Try it in test mode
toWhat happens now
renewal_opensThe renewal order is created and subscription.renewal_due is sent.
period_endThe subscription becomes past_due.
grace_endIt becomes unpaid.
unpaid_endIt becomes ended.

Pay the renewal order after renewal_opens to receive subscription.renewed instead.

The same in brief, without code: Crypto subscriptions, paid one by one.