- Keys and conventions
- Subscriptions
- GET /api/v1/subs/subscriptions
- GET /api/v1/subs/subscriptions/{id}
- POST /api/v1/subs/subscriptions/{id}/pause
- POST /api/v1/subs/subscriptions/{id}/resume
- POST /api/v1/subs/subscriptions/{id}/skip
- POST /api/v1/subs/subscriptions/{id}/cancel
- POST /api/v1/subs/subscriptions/{id}/bill-now
- GET /api/v1/subs/subscriptions/{id}/options
- POST /api/v1/subs/subscriptions/{id}/change-plan
- POST /api/v1/subs/subscriptions/{id}/swap
- POST /api/v1/subs/subscriptions/{id}/one-offs
- POST /api/v1/subs/subscriptions
- Plans
- Memberships and store credit
- Rental queue and loans
- GET /api/v1/subs/queue
- POST /api/v1/subs/queue
- GET /api/v1/subs/queue/{id} and DELETE /api/v1/subs/queue/{id}
- POST /api/v1/subs/queue/{id}/move
- POST /api/v1/subs/queue/{id}/send and POST /api/v1/subs/queue/send-next
- GET /api/v1/subs/loans and /api/v1/subs/loans/{id}
- POST /api/v1/subs/loans/{id}/return and POST /api/v1/subs/loans/return
- POST /api/v1/subs/loans/{id}/lost
- Gift subscriptions
- Analytics and the cancel flow report
- Errors
The REST API v1 endpoints of SI Subscriptions & Memberships: read and manage subscriptions, plans, memberships and their store credit, the rental queue and its loans, gift subscriptions, and the analytics. The key, limits, pagination and error format are the same as for all our apps: see the API overview. The same operations are in GraphQL: GraphQL API reference: Subscriptions & Memberships.
Needs the SI Subscriptions & Memberships app. On a store without it, every endpoint here answers 403 with the code rentals_rest_app_not_installed and data.app: "subscriptions". If the store’s plan doesn’t include the API, the answer is 403 rentals_rest_plan_required.
Keys and conventions #
The Subscriptions app has no API page of its own: make the key in another of our apps on the same store, in Rentals & Bookings under Settings › API, or on the API access page of E-Sign Contracts & Waivers, Refundable Security Deposits, Custom Fields or SI Request a Quote, or in Delivery Routing under Settings › API. Keys belong to the store, so that key works here. GET needs a read key; everything else needs Allow changes (write). A read key gets:
{"code": "rentals_rest_forbidden", "message": "This API key can only read. Create a key that allows changes to do this.",
"data": {"required_scope": "write", "status": 403}}
The examples use two shell variables, API="https://YOUR-API-ADDRESS/api/v1" and KEY="sirb_your_secret_key". Every path starts with /api/v1/subs.
{id}takes our id (42) or the global id (gid://sirentals/SubsSubscription/42). A subscription also takes Shopify’s contract id or gid (gid://shopify/SubscriptionContract/…).- Shopify ids (
customer_id,variant_id,selling_plan_id) take the number or the gid. - Amounts are decimal strings (
"14.40") with acurrency. Timestamps are ISO 8601 in UTC;fromandtoof the reports are days (YYYY-MM-DD) in the store’s time zone. - Lists are paged with
pageandper_page(at most 200), with the totals inX-TotalandX-Total-Pages. Plans and tiers come whole. - Changes go to Shopify’s subscription contract as well; the answer is the subscription after the change.
Subscriptions #
status | Meaning |
|---|---|
pending | Being set up. |
trial | In a free trial. |
active | Billing on schedule. |
past_due | A charge failed and is being retried. |
paused | Paused: no charges until it resumes (pause_until when it resumes by itself). |
pending_cancel | Cancelled at the end of the paid period (cancel_at). |
cancelled, expired, failed | Ended: cancelled, reached its last cycle, or set-up failed. |
kind is subscription (subscribe and save), membership or rental.
GET /api/v1/subs/subscriptions #
Newest first. Paged. Parameters: status (one or several, comma-separated), customer_id, plan_id, search, updated_since (ISO 8601).
curl -s "$API/subs/subscriptions?status=active,past_due&per_page=50" -H "Authorization: Bearer $KEY"
# 200, X-Total: 5
[{"id": 1, "contract_id": 36400000001, "contract_gid": "gid://shopify/SubscriptionContract/36400000001", "status": "active",
"kind": "subscription", "customer_id": 7001234, "origin_order_id": 5550001,
"plan": {"name": "Delivery every 2 weeks", "template_id": 1, "selling_plan_id": 2844000001},
"billing": {"interval": "WEEK", "count": 2}, "next_billing_at": "2026-11-25T14:00:00Z", "cycles_billed": 2, "max_cycles": null,
"trial_ends_at": null, "pause_until": null, "cancel_at": null, "cancel_reason": null, "currency": "USD", "amount": "20.40",
"lines": [{"title": "Coffee beans 12 oz", "variant_title": "Whole bean", "quantity": 1, "price": "14.40"}],
"rental": null, "prepaid": null, "billing_day": null, "gift": null,
"created_at": "2026-11-05T17:38:45Z", "updated_at": "2026-11-06T04:36:52Z"}]
# 400, an unknown status:
{"code": "rentals_rest_invalid", "message": "Unknown status: bogus. Use pending, trial, active, past_due, paused, pending_cancel, cancelled, expired, failed.",
"data": {"field": "status", "status": 400}}
GET /api/v1/subs/subscriptions/{id} #
One subscription: the summary above, with the full lines (ids, product and variant, selling plan, one_time for an added one-off), plus customer_gid, payment_method_gid, shopify_status, last_synced_at, dunning (the retry state of a failed charge), delivery, next_cycle, changes (one_offs, skipped, min_cycles) and cycles, the billing cycles, newest first. rental describes a rental subscription: its items, return_by, and whether it is ending. 404 rentals_rest_not_found (“No subscription with that id in this store.”) otherwise.
curl -s "$API/subs/subscriptions/1" -H "Authorization: Bearer $KEY"
# 200 (trimmed)
{"id": 1, "status": "active", "…": "…",
"delivery": {"type": "shipping", "title": "Standard shipping", "price": "6.00",
"address": {"firstName": "Jordan", "lastName": "Lee", "address1": "18 Elm Street", "city": "Columbus", "zip": "43215",
"provinceCode": "OH", "countryCode": "US", "address2": null, "phone": null}},
"next_cycle": {"index": 2, "due_at": "2026-11-25T14:00:00Z", "status": "scheduled", "amount": null, "order_id": null, "attempts": 0, "next_retry_at": null},
"changes": {"one_offs": [], "skipped": null, "min_cycles": null},
"cycles": [{"index": 2, "due_at": "2026-11-25T14:00:00Z", "status": "scheduled", "amount": null, "order_id": null, "attempts": 0, "next_retry_at": null},
{"index": 1, "due_at": "2026-11-11T14:00:00Z", "status": "paid", "amount": null, "order_id": 5550002, "attempts": 0, "next_retry_at": null}]}
A cycle’s status is scheduled, claimed, billing, paid, failed, skipped, waived or cancelled.
POST /api/v1/subs/subscriptions/{id}/pause #
Pauses an active subscription. Body (optional): cycles (1 to 12) moves the next charge that many cycles on; without it, it stays paused until resumed. Answers the subscription.
curl -s -X POST "$API/subs/subscriptions/1/pause" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"cycles": 2}'
POST /api/v1/subs/subscriptions/{id}/resume #
Resumes a paused subscription. A next charge date already past moves to now.
POST /api/v1/subs/subscriptions/{id}/skip #
Skips the next charge; the cycle after it is billed as usual. POST …/unskip brings it back.
POST /api/v1/subs/subscriptions/{id}/cancel #
Body (optional): when, period_end (the default: it ends when the paid period ends, pending_cancel until then) or now, and reason (up to 255 characters).
curl -s -X POST "$API/subs/subscriptions/1/cancel" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"when": "period_end", "reason": "Moving abroad"}'
POST /api/v1/subs/subscriptions/{id}/bill-now #
Charges the next cycle now with the card on file. Answers {"result": {state, code, message, cycle, attempt}, "subscription": {…}}. state can still be pending: the charge result comes from Shopify a moment later, and GET shows it.
GET /api/v1/subs/subscriptions/{id}/options #
What the subscription can change to: plans (the frequencies with their price and price per month; current marks the one it has), variants it can swap to, and one_offs, the items that can be added to the next order. Parameter: line_id for a subscription with several lines.
curl -s "$API/subs/subscriptions/1/options" -H "Authorization: Bearer $KEY"
# 200 (trimmed)
{"line_id": "gid://shopify/SubscriptionLine/e04b…",
"plans": [{"selling_plan_id": 2844000001, "name": "Delivery every 2 weeks", "interval": "WEEK", "count": 2, "price": "14.40", "per_month": "30.86", "current": true},
{"selling_plan_id": 2844000002, "name": "Delivery every month", "interval": "MONTH", "count": 1, "price": "14.40", "per_month": "14.40", "current": false}],
"variants": [{"variant_id": 49280000001, "product_title": "Coffee beans 12 oz", "title": "Ground", "price": "14.40", "current": false}],
"one_offs": [{"product_id": 9110000001, "…": "…"}]}
POST /api/v1/subs/subscriptions/{id}/change-plan #
Moves a line to another plan (frequency). Body: selling_plan_id (required), line_id. The price and the next charge date follow the new plan. Answers the subscription.
POST /api/v1/subs/subscriptions/{id}/swap #
Swaps a line’s variant for another one the plan allows, priced by the plan. Body: variant_id (required), line_id.
POST /api/v1/subs/subscriptions/{id}/one-offs #
Adds an item to the next order only. Body: variant_id (required), quantity (1 to 10, default 1). Up to 5 items an order. POST …/one-offs/{oneOffId}/remove takes it off again.
curl -s -X POST "$API/subs/subscriptions/1/one-offs" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"variant_id": 49280000003, "quantity": 1}'
POST /api/v1/subs/subscriptions #
Starts a subscription for a customer, as staff do from the app. Rental plans are refused (409 rentals_rest_rental_staff, with data.booking_url).
| Body field | Notes |
|---|---|
customer_id | Required. |
selling_plan_id | The plan (frequency). Required. |
variant_id | Required. |
quantity | Default 1. |
first_billing_date | YYYY-MM-DD in the store’s time zone, today up to a year ahead (default today). |
delivery_address | {first_name, last_name, address1, address2, city, province_code, zip, country_code, phone}; default: the customer’s address. |
When the customer has a card that can be charged, the subscription is made at once: 201 {"subscription": {…}}. Otherwise the answer is 202 with a request that waits for a card: Shopify emails the customer to add one (email_sent), or you send them checkout_url. The subscription is made when the card arrives.
curl -s -X POST "$API/subs/subscriptions" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"customer_id": 7001234, "selling_plan_id": 2844000001, "variant_id": 49280000001, "quantity": 2}'
# 202
{"subscription": null,
"request": {"id": 12, "status": "waiting_payment", "customer_id": 7001234, "selling_plan_id": 2844000001, "variant_id": 49280000001,
"quantity": 2, "first_billing_date": null, "checkout_url": null, "email_sent_at": "2026-11-11T01:40:00Z", "expires_at": "…"},
"checkout_url": null, "email_sent": true}
Plans #
GET /api/v1/subs/plans #
The plans in your order. Parameters: status (active, the default, archived or all), kind (subscription, membership or rental). X-Total holds the count.
GET /api/v1/subs/plans/{id} #
curl -s "$API/subs/plans/1" -H "Authorization: Bearer $KEY"
# 200
{"id": 1, "name": "Coffee subscription", "kind": "subscription", "status": "active", "position": 1, "description": null,
"frequencies": [{"interval": "WEEK", "count": 2}, {"interval": "MONTH", "count": 1}], "billing": {"interval": "WEEK", "count": 2},
"pricing": {"type": "PERCENTAGE", "value": "10"}, "trial_days": 0, "signup_fee": null, "min_cycles": null, "max_cycles": null,
"subscription_only": false,
"selling_plans": [{"id": 2844000001, "gid": "gid://shopify/SellingPlan/2844000001", "name": "Delivery every 2 weeks", "interval": "WEEK", "count": 2},
{"id": 2844000002, "gid": "gid://shopify/SellingPlan/2844000002", "name": "Delivery every month", "interval": "MONTH", "count": 1}],
"prepaid_deliveries": null, "prepaid_renews": true, "billing_day": null, "gifting": false,
"created_at": "2026-11-05T17:08:53+00:00", "updated_at": "2026-11-05T17:08:53+00:00"}
selling_plans are the Shopify selling plans the plan made: their ids go in selling_plan_id.
Memberships and store credit #
A membership gives a customer a tier’s benefits: discounts, free shipping, store credit every period, access to products and pages, and rental queue slots. It comes from a subscription (source: "contract"), an order, your team or the API. status is active, grace (a payment is overdue: benefits continue until grace_until), suspended (benefits off: the subscription is paused, or grace ran out) or ended.
GET /api/v1/subs/memberships #
Newest first. Paged. Parameters: status (comma-separated: open, the default, is active, grace and suspended; also live, all, active, grace, suspended, ended), customer_id, tier_id, source.
GET /api/v1/subs/memberships/{id} #
One membership. credit.balance (the member’s store credit, read from Shopify) is on this answer only.
curl -s "$API/subs/memberships/1" -H "Authorization: Bearer $KEY"
# 200
{"id": 1, "status": "active", "status_reason": null, "source": "staff", "tier": {"id": 1, "name": "Coffee Club", "handle": "coffee-club"},
"customer_id": 7001234, "subscription_id": null, "contract_id": null, "source_order_id": null,
"starts_at": "2026-11-05T17:13:42+00:00", "ends_at": null, "grace_until": null, "ended_at": null, "benefits": true,
"credit": {"per_period": "5.00", "currency": "USD", "balance": "5.00"}, "queue": {"slots": 2, "out": 1},
"created_at": "2026-11-05T17:13:42+00:00"}
POST /api/v1/subs/memberships #
Makes a customer a member of a tier now. Body: customer_id, tier_id, ends_at (optional: no end without it). Answers 201 with the membership (source: "api").
POST /api/v1/subs/memberships/{id}/end #
Ends a membership made by an order, your team or the API, now. Body (optional): reason. A subscription’s membership ends with its subscription: 409 rentals_rest_contract_membership.
POST /api/v1/subs/memberships/{id}/credit #
Adds store credit (a positive amount) or takes it back (negative) on the member’s Shopify account, recorded on the membership. Body: amount (required), note, expires_at, request_key. Send the same request_key on a retry and you get the same entry back: nothing is added twice. Answers {membership, entry, credit}.
curl -s -X POST "$API/subs/memberships/1/credit" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"amount": "10.00", "note": "Sorry about the late box", "request_key": "ticket-4821"}'
GET /api/v1/subs/tiers and /api/v1/subs/tiers/{id} #
The tiers, highest rank first. Parameter: status (active, the default, deleted or all).
curl -s "$API/subs/tiers/1" -H "Authorization: Bearer $KEY"
# 200
{"id": 1, "name": "Coffee Club", "handle": "coffee-club", "status": "active", "position": 1, "slots": 2, "swaps_per_period": 2,
"credit": {"amount": "5.00", "expiry_days": null, "rollover_cap": null},
"discount": {"type": "percent", "value": "10", "scope": "all", "collections": [], "tags": []},
"shipping": {"type": "free", "value": "100"}, "member_tag": "member-coffee-club",
"access": {"products": ["gid://shopify/Product/9110000001"], "collections": [], "pages": ["brew-guides"]},
"one_time": null, "priced": true, "members": {"active": 2, "grace": 0, "suspended": 0},
"created_at": "2026-11-05T16:38:06+00:00", "updated_at": "2026-11-06T07:15:40+00:00"}
Rental queue and loans #
Members with queue slots keep a list of products they want to borrow. You send the next item (a loan), the member sends it back, and the next one goes out. A loan is picking until its loan order is fulfilled, then out, then returned (or lost, cancelled).
GET /api/v1/subs/queue #
A member’s queue in send order. Parameter: customer_id (required). The answer says how many slots the member has, how many items are out, how many are free, and whether you can send now (can_send, or the reason why not).
curl -s "$API/subs/queue?customer_id=7001234" -H "Authorization: Bearer $KEY"
# 200
{"customer_id": 7001234, "slots": 2, "out": 0, "free": 2, "can_send": true, "reason": null,
"items": [{"id": 4, "position": 1, "starred": false, "customer_id": 7001234, "product_id": 9110000004, "product_title": "Siphon brewer",
"variant_id": null, "variant_title": null, "membership_id": 2, "mode": "only", "added_at": "2026-11-06T07:41:50+00:00",
"removed_at": null, "removed_reason": null}]}
POST /api/v1/subs/queue #
Adds a product (any option) or one option to a member’s queue. Body: customer_id, product_id, variant_id (optional), starred (optional: starred items go first). Answers 201 with the item; an item already queued answers that item.
GET /api/v1/subs/queue/{id} and DELETE /api/v1/subs/queue/{id} #
One item, live or removed. DELETE takes it out of the queue and answers the queue.
POST /api/v1/subs/queue/{id}/move #
Body: position (1 = next; a starred item stays among the starred ones). Answers the queue.
POST /api/v1/subs/queue/{id}/send and POST /api/v1/subs/queue/send-next #
Sends one item now (body: variant_id, serial, handover – all optional), or the member’s next item in stock, as the pick list would (body: customer_id, handover). Answers 201 with the loan. With handover: true (handed over in person) the loan is out at once; otherwise it is picking until its loan order is fulfilled.
GET /api/v1/subs/loans and /api/v1/subs/loans/{id} #
Newest first. Paged. Parameters: status (all, the default, open, closed, picking, out, returned, lost, cancelled), customer_id, product_id, membership_id.
curl -s "$API/subs/loans?status=out" -H "Authorization: Bearer $KEY"
# 200, X-Total: 1
[{"id": 3, "status": "out", "status_reason": null, "mode": "inventory", "customer_id": 7001234, "membership_id": 2, "queue_item_id": 5,
"product_id": 9110000005, "product_title": "Pour-over brewer", "variant_id": 49280000005, "variant_title": "Default Title",
"sku": null, "serial": null, "booking_id": null, "order": {"state": "created", "id": 5550009, "name": "#1049", "error": null},
"invoice": null, "sent_at": "2026-11-06T07:43:55+00:00", "due_back_at": null, "returned_at": null, "lost_at": null,
"cancelled_at": null, "created_at": "2026-11-06T07:42:51+00:00"}]
POST /api/v1/subs/loans/{id}/return and POST /api/v1/subs/loans/return #
A loan came back (body: at, ISO 8601, default now). The batch form takes loan_ids, serials (as scanned) or order_name (the loan order, #1049) and answers one result per loan or serial.
curl -s -X POST "$API/subs/loans/return" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"serials": ["SB-0042", "SB-0043"]}'
POST /api/v1/subs/loans/{id}/lost #
A loan that won’t come back. The member’s slot is free again. The replacement invoice follows your queue settings; body amount overrides it ("0" = no invoice), note is optional.
Gift subscriptions #
GET /api/v1/subs/gifts and /api/v1/subs/gifts/{id} #
Newest first. Paged. Parameters: status (comma-separated: scheduled, sent, ended, cancelled), subscription_id. One gift includes its recipient.
POST /api/v1/subs/gifts/{id}/renewal #
Body: renews. true keeps the gift going after its prepaid deliveries (the giver is charged from then on); false lets it end after them.
Analytics and the cancel flow report #
GET /api/v1/subs/analytics #
The analytics of a range of days (default: the last 30, at most 366): monthly recurring revenue per currency, active, paused, new and ended subscriptions, churn rate, failed payment recovery (dunning), store credit owed, the rental queue and rental subscriptions. tiles hold the totals, days the same per day, by_plan per plan. Parameters: from, to.
curl -s "$API/subs/analytics?from=2026-11-01&to=2026-11-10" -H "Authorization: Bearer $KEY"
# 200 (trimmed)
{"range": {"from": "2026-11-01", "to": "2026-11-10", "days": 10, "timezone": "America/New_York"},
"tiles": {"mrr": [{"currency": "USD", "amount": "448.00"}], "active": 5, "paused": 0, "new": 5, "churned": 0, "churn_rate": null,
"dunning": {"failed": 0, "recovered": 0, "lost": 0, "rate": null}, "credit": [{"currency": "USD", "amount": "15.00"}],
"queue": {"out": 1, "overdue": 0, "returned": 2, "median_days_out": 0}, "rentals": {"active": 3, "overdue": 0}},
"days": [{"day": "2026-11-01", "active": 0, "…": "…"}], "by_plan": [{"…": "…"}], "computed_at": "2026-11-11T01:30:00+00:00"}
GET /api/v1/subs/retention/report #
The cancel flow’s report for a range of days (default: the last 30, at most 366): how many customers started cancelling, cancelled, were kept by an offer or left the flow, by reason, and how each offer did. Parameters: from, to.
curl -s "$API/subs/retention/report?from=2026-11-01&to=2026-11-10" -H "Authorization: Bearer $KEY"
# 200
{"from": "2026-11-01", "to": "2026-11-10", "totals": {"started": 6, "cancelled": 2, "saved": 0, "left": 4},
"reasons": [{"key": "too_expensive", "label": "It's too expensive", "started": 5, "cancelled": 2, "saved": 0, "left": 3}],
"offers": [{"type": "pause", "label": "Pause", "shown": 2, "accepted": 0, "rate": 0}], "notes": []}
Errors #
Errors have the shape of every endpoint of the API: {"code": "rentals_rest_<code>", "message": "…", "data": {"status": …}}. The message says what to do in plain words.
| Status | Code | When |
|---|---|---|
| 400 | invalid | A parameter or body field is missing or wrong; data.field names it. |
| 403 | forbidden | A read key tried to change something (data.required_scope: "write"). |
| 403 | plan_required | The store’s plan doesn’t include this. |
| 403 | disabled | The setting this needs is off in the app (for example, customers may not make that change). |
| 403 | app_not_installed | The store doesn’t have SI Subscriptions & Memberships (data.app: "subscriptions"). |
| 404 | not_found | No such subscription, plan, membership, tier, queue item, loan or gift in this store. |
| 409 | not_allowed, terminal, conflict | The subscription can’t do that now (the message says why), it has ended, or someone else just changed it: reload and try again. |
| 409 | min_cycles, one_off_pending, renew_once | The plan’s minimum number of cycles isn’t reached yet; items added to the next order must be removed first; the next delivery was already ordered early. |
| 409 | contract_membership, already_ended | The membership belongs to a subscription, or has ended. |
| 409 | item_out, not_available, already_returned, already_lost | The queue item or loan can’t do that now. |
| 409 | rental_staff | Rental plans start from a booking, not here (data.booking_url). |
| 429 | too_soon | The email to the customer went out less than an hour ago: try again later. |
| 502 | shopify_error | Shopify refused the change; nothing was saved. Try again later. |
