DocsAPI ReferenceProducts
Products
Manage your catalog from your own server: create products with a price in one call, list, update and delete them. Every product is returned with a price_id ready to hand to /v1/checkout/sessions, /v1/subscriptions or a payment link — no second request needed.
How it works
This is the same Product + Price catalog the dashboard's Products page manages — see Products & Prices for the underlying concepts (one-time vs. recurring, tax handling). The API adds server-to-server access with your secret key, so you can sync a catalog from your own system, an ERP, or a storefront you built yourself.
Amounts are integer minor units
amount is always an integer number of cents — 1999 means EUR 19.99, exactly like Checkout Sessions and Subscriptions. Sending a float or a numeric string is rejected — this is deliberate, so a units bug (e.g. multiplying by 100 twice) fails loudly instead of overcharging a customer by 100x.Test mode and live mode never mix
sk_test_ exists only in test mode: it is invisible to sk_live_ requests, and vice versa, even though both keys belong to the same account. There is no parameter to cross environments — use Copy to Live in the dashboard to promote a tested product.Create a product
/api/v1/productsSecret key (sk_)| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | 1–255 characters. |
| amount | integer | Yes | Integer minor units (cents). 1999 = EUR 19.99. |
| description | string | null | no | Up to 2000 characters. |
| type | "digital" | "physical" | no | Default digital. |
| sku | string | no | 3–64 chars, letters/digits/./_/-. Unique across the whole platform. Omit to have one generated. |
| show_in_store | boolean | no | Whether the product appears on your public storefront. Default true. |
| images | string[] | no | Up to 10 absolute http(s) image URLs. |
| tax_rate | integer | null | no | VAT percentage, 0–100. Omit or 0 for no tax. |
| tax_inclusive | boolean | no | Whether amount already contains the tax. Only takes effect alongside a non-zero tax_rate, and only when tax calculation is enabled on your account. |
| mode | "one_time" | "recurring" | no | Default one_time. |
| interval | "day" | "week" | "month" | "year" | With recurring | Required when mode is recurring; rejected otherwise. |
| interval_count | integer | no | Bill every N intervals, 1–365. Default 1. |
| active | boolean | no | Whether the price can be used for new sales. Default true. |
curl -X POST https://saleonix.com/api/v1/products \
-H "Authorization: Bearer sk_test_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Pro plan",
"amount": 1999,
"type": "digital",
"tax_rate": 18,
"tax_inclusive": true
}'Response — 201 Created
json
{
"id": "cprod1abc...",
"object": "product",
"livemode": false,
"name": "Pro plan",
"description": null,
"sku": "A1B2C3D4",
"type": "digital",
"show_in_store": true,
"images": [],
"amount": 1999,
"amount_decimal": "19.99",
"currency": "eur",
"tax_rate": 18,
"tax_inclusive": true,
"has_license": false,
"has_file": false,
"license_duration": null,
"price_id": "price_cprice1xyz...",
"price": {
"id": "price_cprice1xyz...",
"object": "price",
"amount": 1999,
"amount_decimal": "19.99",
"currency": "eur",
"mode": "one_time",
"interval": null,
"interval_count": null,
"active": true,
"created": 1755763200,
"updated": 1755763200
},
"created": 1755763200,
"updated": 1755763200
}Recurring prices
ts
// A recurring price. Sellable once subscriptions are enabled on your account —
// see the Subscriptions API for the current status.
await fetch("https://saleonix.com/api/v1/products", {
method: "POST",
headers: { Authorization: `Bearer ${SECRET_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
name: "Pro plan — monthly",
amount: 999,
mode: "recurring",
interval: "month",
interval_count: 1,
}),
});List products
/api/v1/productsSecret key (sk_)| Field | Type | Required | Description |
|---|---|---|---|
| limit | integer | no | 1–100. Default 10. |
| starting_after | string | no | Cursor: a product id from a previous page. |
| type | "digital" | "physical" | no | Filter by type. |
| mode | "one_time" | "recurring" | no | Filter by the product's price mode. |
| show_in_store | "true" | "false" | no | Filter by storefront visibility. |
| sku | string | no | Filter by exact SKU. |
bash
curl "https://saleonix.com/api/v1/products?limit=20&type=digital" \
-H "Authorization: Bearer sk_test_xxx"Response
json
{
"object": "list",
"url": "/v1/products",
"has_more": false,
"livemode": false,
"data": [ { "id": "cprod1abc...", "object": "product", "...": "..." } ]
}Retrieve a product
/api/v1/products/{id}Secret key (sk_)Returns 404 resource_missing for an id that does not exist, that belongs to another store, or that belongs to your own store in the other environment — the response is identical in all three cases, so the endpoint cannot be used to probe what exists elsewhere.
Update a product
/api/v1/products/{id}Secret key (sk_)Partial update — send only the fields you want to change. Accepts the same body as create, with every field optional. POST /api/v1/products/{id} is accepted as an alias, for clients that follow the POST-to-update convention.
bash
curl -X PATCH https://saleonix.com/api/v1/products/cprod1abc... \
-H "Authorization: Bearer sk_test_xxx" \
-H "Content-Type: application/json" \
-d '{ "amount": 2499, "show_in_store": false }'Changing the price plan
mode: "recurring" without an interval is rejected, even if the product was already recurring before — the resulting price is validated as a whole, not field-by-field, so you can never save a schedule with a mode but no billing period. Switching recurring → one_time clears interval and interval_count automatically.Delete a product
/api/v1/products/{id}Secret key (sk_)Deleting a product also removes its price and detaches it from past orders and payment links. Two guards stop it from silently destroying something a customer already paid for:
| Condition | Result |
|---|---|
| The product has a subscription that is pending, active, past-due or suspended | 409 product_has_active_subscriptions — always. Cancel the subscription(s) first, or set show_in_store: false to retire the product instead. |
| The product has issued license keys | 409 product_has_licenses unless the request includes ?force=true. Forcing deletes the keys — they stop validating for those customers immediately. |
bash
# Blocked: the product has active/suspended subscriptions.
curl -X DELETE https://saleonix.com/api/v1/products/cprod1abc... \
-H "Authorization: Bearer sk_live_xxx"
# Blocked: the product has issued license keys — repeat with ?force=true
# to delete anyway (those keys stop validating immediately).
curl -X DELETE "https://saleonix.com/api/v1/products/cprod1abc...?force=true" \
-H "Authorization: Bearer sk_live_xxx"Response
json
{ "id": "cprod1abc...", "object": "product", "deleted": true, "livemode": true }Not supported via the API
These stay dashboard-only and are rejected with a specific error if sent — they are not silently ignored:
| Field | Why |
|---|---|
has_license | License configuration (max users, lock policy) has no API shape yet. Configure it from Product → Licensing in the dashboard. |
file_url / has_file | Digital delivery files are uploaded from the dashboard. file_url is also never returned by GET — it is the paid deliverable's location, handed out by the post-payment delivery path only. |
price / currency | Rejected with a message pointing at amount — these are the classic units-confusion field names from other payment APIs. |
Security model
| Property | Behaviour |
|---|---|
| Authentication | Secret key only (sk_). A publishable key (pk_) is refused with 401 wrong_key_type — catalog writes must never be reachable from a browser. |
| Tenant isolation | Every query is scoped to the key's store. A product id from another merchant answers 404, identical to an id that does not exist. |
| Environment isolation | Every query is alsoscoped to the key's environment (test vs. live), enforced in the same database query as the tenant check — not assumed from the id. A sk_test_ key can never read, modify or delete a live product, even one belonging to the same account, and vice versa. |
| Account status | A suspended account's sk_live_ key is refused with 403 account_suspended — matching the dashboard, a suspended account can only operate in test mode. Its sk_test_ key keeps working. |
| Rate limiting | Per-IP and per-key limits on every request, plus a stricter per-key write limit on create/update/delete. |
| Catalog quota | Each store has a maximum number of products; creating beyond it is refused rather than degrading performance for everyone. |
| Request size | JSON bodies are capped well below the platform's general request-size limit — a product payload is a few KB. |
| SKU collisions | sku is unique platform-wide. A collision returns 409 sku_unavailable without confirming whether the SKU belongs to you or another merchant. |
