DocsAPI ReferenceSubscriptions
Subscriptions
Create recurring subscriptions from your own backend — not just the dashboard. Stripe-style: your server creates a subscription checkout with a secret key, your customer enters their card on the hosted page, and webhooks keep you in sync for every step of the subscription's life.
How it works
Card data never touches your servers or this API: creating a subscription returns a hosted checkout url, served by Saleonix, where your customer enters their card. When the customer completes it, Saleonix creates the recurring contract at the payment gateway, the checkout's subscription field points at the new subscription, and the subscription.created webhook fires. From then on the platform bills the customer automatically every cycle and emits a webhook for every charge, failure, cancellation and expiry.
| Step | What happens | You receive |
|---|---|---|
| 1. Create | POST /api/v1/subscriptions with a plan or price | A subscription.checkout with a hosted url |
| 2. Customer pays | Card entered on the hosted checkout page | Redirect to your success_url + subscription.created webhook |
| 3. Recurring billing | Saleonix charges the card every cycle | subscription.payment_succeeded / payment_failed webhooks |
| 4. End of life | You cancel via API/dashboard, or the schedule completes | subscription.canceled / subscription.expired webhooks |
Create a subscription
/api/v1/subscriptionsSecret key (sk_)Define the plan inline (amount + interval + plan_name), or reference a recurring catalog price (price_id) so the amount and interval derive server-side from your catalog. Provide exactly one of price_id or amount.
| Field | Type | Required | Description |
|---|---|---|---|
| price_id | string | Price mode | A RECURRING price of your store. Amount, interval and plan name derive from the catalog; interval/interval_count/plan_name must not be sent. |
| amount | integer | Plan mode | Charged EVERY cycle, in integer minor units (cents): 999 = EUR 9.99. Minimum 50. All subscriptions bill in EUR. |
| interval | string | Plan mode | One of "day", "week", "month", "year". |
| interval_count | integer | No | Bill every N intervals (default 1, max 365). E.g. month + 3 = quarterly. |
| plan_name | string | Plan mode | Shown to the customer on the hosted checkout page. |
| total_payments | integer | Yes | Total number of charges before the schedule ends (1–10000). The gateway requires a fixed count — use e.g. 12 for a one-year monthly plan. |
| customer_email | string | Yes | The subscriber. Prefilled read-only on the hosted checkout page. |
| customer_name | string | No | Prefilled on the hosted checkout page. |
| success_url | string | No | Your page the customer lands on after paying; checkout_id and subscription_id query params are appended. https required in live mode. |
| metadata | object | No | Up to 20 string key/value pairs. Copied onto the subscription and echoed in every webhook. |
| expires_at | integer | No | Epoch seconds. Clamped to [now + 30 minutes, now + 24 hours]. Defaults to 24 hours; the hosted link dies with it. |
curl -X POST https://saleonix.com/api/v1/subscriptions \
-H "Authorization: Bearer sk_test_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sub-order-4912" \
-d '{
"amount": 999,
"interval": "month",
"plan_name": "Pro Monthly",
"total_payments": 12,
"customer_email": "buyer@example.com",
"success_url": "https://example.com/thanks"
}'Response
json
{
"id": "subc_test_9fJk3W2v1XyZ8Qw7Tt6Rr5Ee4Uu3Ii2",
"object": "subscription.checkout",
"status": "open",
"livemode": false,
"url": "https://saleonix.com/checkout?draft=subc_test_9fJk...&exp=1751616000&sig=...",
"subscription": null,
"amount": 999,
"currency": "eur",
"interval": "month",
"interval_count": 1,
"total_payments": 12,
"plan_name": "Pro Monthly",
"price_id": null,
"product_id": null,
"customer_email": "buyer@example.com",
"customer_name": null,
"success_url": "https://example.com/thanks",
"metadata": { "internal_ref": "acct-88" },
"created": 1751529600,
"expires_at": 1751616000
}Retries are safe
Idempotency-Key header: a repeated key replays the original checkout instead of creating a second payment link, and reusing a key with a different payload is rejected with 409.One link, one payment attempt
incomplete and you create a fresh checkout to retry.Check a subscription checkout
/api/v1/subscriptions/checkout/{id}Secret key (sk_)Poll or reconcile a checkout you created. status is open (link live, url re-signed on every read), complete (consumed by a payment attempt — follow subscription for the outcome) or expired. Prefer webhooks over polling.
json
{
"id": "subc_test_9fJk3W2v1XyZ8Qw7Tt6Rr5Ee4Uu3Ii2",
"object": "subscription.checkout",
"status": "complete",
"url": null,
"subscription": "cmcsub7h2k0001abcd1234efgh",
...
}Retrieve a subscription
/api/v1/subscriptions/{id}Secret key (sk_)The subscription id comes from the checkout's subscription field, the subscription.created webhook, or the redirect's subscription_id query param. Amounts are integer minor units; timestamps are epoch seconds.
json
{
"id": "cmcsub7h2k0001abcd1234efgh",
"object": "subscription",
"status": "active",
"livemode": false,
"currency": "eur",
"amount": 999,
"interval": "month",
"interval_count": 1,
"total_payments": 12,
"recurring_id": "4f2a9c1e-...",
"customer_id": null,
"customer_email": "buyer@example.com",
"customer_name": null,
"product_id": null,
"price_id": null,
"failure_count": 0,
"current_period_start": 1751529600,
"current_period_end": 1754208000,
"next_billing_at": 1754208000,
"canceled_at": null,
"ended_at": null,
"metadata": { "internal_ref": "acct-88" },
"created": 1751529600
}Subscription statuses
| Status | Meaning |
|---|---|
| active | Billing normally; next_billing_at is the next charge |
| incomplete | Created but the first charge was declined |
| past_due | A recurring charge failed; retries are in progress |
| canceled | Stopped by you (API/dashboard) or after exhausted retries |
| expired | The final scheduled payment completed (total_payments reached) |
| pending / suspended | Transitional / administratively paused |
List subscriptions
/api/v1/subscriptionsSecret key (sk_)| Field | Type | Required | Description |
|---|---|---|---|
| limit | integer | No | Page size, 1–100 (default 10). |
| starting_after | string | No | Cursor: the last subscription id of the previous page. Newest first. |
| status | string | No | Filter: active, past_due, incomplete, canceled, expired, … |
| customer_email | string | No | All subscriptions of one customer. |
bash
curl "https://saleonix.com/api/v1/subscriptions?status=active&limit=25" \
-H "Authorization: Bearer sk_live_xxx"
# Next page: pass the last id of the previous page
curl "https://saleonix.com/api/v1/subscriptions?limit=25&starting_after=cmcsub7h2k..." \
-H "Authorization: Bearer sk_live_xxx"Returns { "object": "list", "data": [...], "has_more": true } — pass the last id as starting_after while has_more is true.
Update a subscription
/api/v1/subscriptions/{id}Secret key (sk_)metadata is the only mutable field and the object you send replaces the stored one. Billing amount and schedule are fixed at the gateway once the recurring contract exists — to change the plan, cancel and create a new subscription. Emits subscription.updated.
bash
curl -X POST https://saleonix.com/api/v1/subscriptions/cmcsub7h2k... \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "metadata": { "internal_ref": "acct-88", "tier": "pro" } }'Cancel a subscription
/api/v1/subscriptions/{id}Secret key (sk_)Cancels the recurring contract at the payment gateway first, then marks the subscription canceled and stops all future charges — effective immediately, no proration. The optional reason is stored for your audit trail. Canceling an already-canceled subscription is a safe no-op that returns the subscription unchanged. Emits subscription.canceled.
bash
curl -X DELETE https://saleonix.com/api/v1/subscriptions/cmcsub7h2k... \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "reason": "Customer requested downgrade" }'Webhook events
Every lifecycle step emits a signed webhook — configure endpoints under Developers → Webhooks and see the Webhooks guide for signature verification.
| Event | Fires when |
|---|---|
| subscription.created | The customer completed the hosted checkout |
| subscription.updated | State changed (status, next billing, metadata) |
| subscription.payment_succeeded | A recurring charge settled |
| subscription.payment_failed | A recurring charge was declined |
| subscription.past_due | Retries started after a failed charge |
| subscription.canceled | Canceled via API, dashboard, or exhausted retries |
| subscription.expired | The final scheduled payment completed |
ts
// The webhook payload's data.object is the same subscription object the
// REST API returns, so one deserializer covers both.
switch (event.type) {
case "subscription.created":
// Created after the customer completed the hosted checkout. status is
// "active" when the first charge succeeded, "incomplete" when it declined.
break;
case "subscription.payment_succeeded":
// A recurring charge settled — extend the customer's access.
break;
case "subscription.payment_failed":
case "subscription.past_due":
// A charge was declined / retries are running — warn the customer.
break;
case "subscription.canceled":
case "subscription.expired":
// Billing stopped (canceled, or the final scheduled payment completed).
break;
}Security model
| Property | Behaviour |
|---|---|
| Card data | Entered on the Saleonix-hosted checkout page only — never sent to this API or to your servers. Card data is transmitted server-to-server from Saleonix to the payment gateway; it is never stored or logged. This keeps YOUR integration out of PCI DSS scope; Saleonix itself remains in scope (SAQ D) as the party transmitting cardholder data. |
| Authentication | Secret key (sk_) only; server-side. Keys are stored hashed |
| Tenant isolation | A key can only see/manage its own store's subscriptions and prices |
| Environment isolation | test keys create test checkouts; a checkout refuses to charge if the store's environment changed after creation |
| Rate limiting | Per-IP and per-key limits with Retry-After on 429 |
| Idempotency | Idempotency-Key on create; cancel is idempotent by design |
| Hosted link | Signed (HMAC), expiring, single-use — it burns on the first payment attempt |
| Audit | Every create/update/cancel is written to the store audit log |
Never use sk_ in a browser