The Subby API is a REST API. It accepts JSON request bodies, returns JSON, and uses standard HTTP methods and status codes.
Base URLs
| Environment | Base URL |
|---|---|
| Live | https://api.mysubbyapp.com/v1 |
| Sandbox | https://sandbox-api.mysubbyapp.com/v1 |
All requests must use HTTPS and include Content-Type: application/json when they have a body.
HTTP methods
| Method | Used for | Metered |
|---|---|---|
GET |
Retrieve one object or list objects | Never |
POST |
Create objects and perform actions (/pause, /cancel, /retry) |
Yes, when successful in live |
PATCH |
Update some fields of an object | Yes, when successful in live |
DELETE |
Delete or archive an object | Yes, when successful in live |
The API does not use PUT. See API usage & metering.
Response envelope
Successful responses wrap the result in data:
{
"success": true,
"data": {
"id": "sub_9fK3mQ2xLp",
"object": "subscription",
"status": "active",
"livemode": true
}
}
List responses return an array in data and pagination details in meta:
{
"success": true,
"data": [ { "id": "cus_1", "object": "customer" }, { "id": "cus_2", "object": "customer" } ],
"meta": { "has_more": true, "next_cursor": "cus_2", "limit": 2 }
}
Errors return success: false and an error object. See Errors.
Object IDs
Every object has a string ID with a prefix that tells you its type:
| Prefix | Object |
|---|---|
acct_ |
Account |
plan_ |
Plan |
cus_ |
Customer |
sub_ |
Subscription |
chg_ |
Charge |
cs_ |
Checkout session |
pm_ |
Payment method |
ndg_ |
Nudge |
we_ |
Webhook endpoint |
evt_ |
Event |
inv_ |
Subby invoice |
clock_ |
Test clock (sandbox only) |
req_ |
Request ID |
Treat IDs as opaque strings of up to 64 characters. Don't parse them.
Amounts and currency
- Amounts are integers in the smallest currency unit. For NGN that's kobo:
500000means ₦5,000.00. currencyis a three-letter ISO 4217 code in upper case.NGNis supported today.- Never send decimals.
5000.00is rejected withparameter_invalid.
Dates and times
All timestamps are ISO 8601 strings in UTC, for example 2026-09-17T14:02:11Z. Send timestamps in the same format.
Metadata
Plans, customers, subscriptions, charges and checkout sessions accept a metadata object for your own reference, such as your internal user ID.
- Up to 20 keys.
- Keys up to 40 characters, values up to 500 characters, strings only.
- Set a key to
""in aPATCHto remove it. - Don't store sensitive data like card numbers or passwords in metadata.
You can filter list endpoints by one metadata key: GET /customers?metadata[your_user_id]=u_1024.
Pagination
List endpoints use cursor pagination.
| Parameter | Default | Description |
|---|---|---|
limit |
20 |
Number of objects to return, from 1 to 100 |
starting_after |
— | An object ID. Returns objects after this one |
ending_before |
— | An object ID. Returns objects before this one |
Lists are sorted newest first. To fetch everything, pass meta.next_cursor as starting_after until has_more is false:
let cursor;
do {
const url = new URL(`${process.env.SUBBY_BASE_URL}/subscriptions`);
url.searchParams.set('limit', '100');
if (cursor) url.searchParams.set('starting_after', cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.SUBBY_SECRET_KEY}` } });
const { data, meta } = await res.json();
for (const sub of data) handle(sub);
cursor = meta.has_more ? meta.next_cursor : undefined;
} while (cursor);
Expanding related objects
Some fields hold the ID of a related object. Add expand[] to get the full object instead, which saves a request:
curl "https://api.mysubbyapp.com/v1/subscriptions/sub_9fK3mQ2xLp?expand[]=customer&expand[]=plan" \
-H "Authorization: Bearer $SUBBY_SECRET_KEY"
You can expand up to 4 fields per request, one level deep.
Idempotency
Networks fail. If a POST times out, you can't tell whether Subby received it. Idempotency keys make retrying safe: send the same key again and you get the original response back instead of creating a duplicate.
curl "https://api.mysubbyapp.com/v1/subscriptions" \
-H "Authorization: Bearer $SUBBY_SECRET_KEY" \
-H "Idempotency-Key: 3b7f0d8e-1c2a-4e7b-9a55-0f5b3e1d9c21" \
-H "Content-Type: application/json" \
-d '{ "customer": "cus_4kQ", "plan": "plan_8Zp", "payment_method": "pm_2Hx" }'
How it works:
- Send
Idempotency-Keyon anyPOST. A V4 UUID is a good choice. Maximum 255 characters. - Keys are stored for 24 hours per account per environment.
- A replay returns the original status code and body, plus the header
Idempotent-Replayed: true. - Replays are not metered.
- Reusing a key with a different body returns
409withidempotency_key_reused. - If the first request is still processing, a replay returns
409withidempotency_request_in_progress. Retry after a moment. - Requests that failed validation (
4xx) are not stored, so you can fix the body and retry with the same key.
PATCH and DELETE are naturally idempotent and don't need a key, though sending one does no harm.
Versioning
The current version is v1, in the URL path (/v1).
- Backwards-compatible changes ship without a new version: new endpoints, new optional parameters, new fields in responses, new event types and new enum values. Build your integration to ignore fields and values it doesn't recognise.
- Breaking changes only ship in a new version (
/v2). We announce them at least 90 days in advance on the changelog and by email, and keep the old version running for at least 12 months after that.
Response headers
| Header | Description |
|---|---|
X-Request-Id |
Unique ID for this request. Include it in support requests |
X-Subby-Billable |
Whether this request counted toward your write allowance |
X-Subby-Write-Usage / X-Subby-Write-Limit |
Current period write usage and allowance (write requests only) |
RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset |
See Rate limits |
Idempotent-Replayed |
true when the response is a replay |