# Hidden API documentation (complete) > Index: https://docs.sellhidden.com/llms.txt. OpenAPI: https://docs.sellhidden.com/openapi.json ## Instructions for agents - Base URL https://api.sellhidden.com/v1. Public endpoints need no key. Authenticated endpoints take `Authorization: Bearer `. - Ask the user for their API key and store slug. Never print, log or commit the key; keep it in an environment variable. - Send `Idempotency-Key` on every POST. - Verify every webhook: HMAC-SHA256 of `${X-Hidden-Timestamp}.${rawBody}` with the endpoint secret, compared in constant time; reject timestamps older than 5 minutes; dedupe by X-Hidden-Delivery; answer 2xx within 15 seconds. - There is no test mode: every call is live. Use a $0 product or a temporary key from the Try it panel to test. - Orders are created by buyers at checkout; the API manages them afterwards. --- # Hidden API documentation Source: https://docs.sellhidden.com Show your products on your own website, get a signed message the moment someone pays, and deliver from your own server. Start with [the quick start](https://docs.sellhidden.com/start) or [the API reference](https://docs.sellhidden.com/reference). ## What you can do - **[Show your products anywhere](https://docs.sellhidden.com/public)**: Your website reads products, prices and stock from Hidden. No key needed. [Public API](https://docs.sellhidden.com/public) · [Buy button](https://docs.sellhidden.com/buy-button) - **[Know the moment someone pays](https://docs.sellhidden.com/webhooks)**: Hidden sends your server a signed message for every order and every change. [Webhooks](https://docs.sellhidden.com/webhooks) - **[Deliver from your own server](https://docs.sellhidden.com/dynamic-delivery)**: On a paid order, Hidden asks your server what the buyer should receive, and delivers your answer. [Dynamic delivery](https://docs.sellhidden.com/dynamic-delivery) - **[Run your store from code](https://docs.sellhidden.com/reference)**: Create products, add keys, refund orders, answer tickets. Everything the dashboard does, from your own code. [API reference](https://docs.sellhidden.com/reference) ## Choose how to start - **[Write it yourself](https://docs.sellhidden.com/start)**: Pick your language: curl, Node, Python or PHP. Your first call takes five minutes. [Start here](https://docs.sellhidden.com/start) - **[No code](https://docs.sellhidden.com/buy-button)**: Paste one snippet on your site and sell from there. [Buy button](https://docs.sellhidden.com/buy-button) - **[Let an AI do it](https://docs.sellhidden.com/ai)**: Give these docs to your AI assistant and answer its questions. [Build with AI](https://docs.sellhidden.com/ai) ## Everything in the API Every group is generated from the same list that runs the API, so it is always current. [Public API · 5 endpoints · no key](https://docs.sellhidden.com/public) · [Webhook events · 29](https://docs.sellhidden.com/webhooks) · [Products · 14 endpoints](https://docs.sellhidden.com/reference/products) · [Categories · 6 endpoints](https://docs.sellhidden.com/reference/categories) · [Coupons · 5 endpoints](https://docs.sellhidden.com/reference/coupons) · [Orders · 10 endpoints](https://docs.sellhidden.com/reference/orders) · [Subscriptions · 4 endpoints](https://docs.sellhidden.com/reference/subscriptions) · [Customers · 5 endpoints](https://docs.sellhidden.com/reference/customers) · [Reviews · 5 endpoints](https://docs.sellhidden.com/reference/reviews) · [Tickets · 5 endpoints](https://docs.sellhidden.com/reference/tickets) · [Store · 2 endpoints](https://docs.sellhidden.com/reference/store) · [Webhooks · 8 endpoints](https://docs.sellhidden.com/reference/webhooks) · [Analytics · 1 endpoints](https://docs.sellhidden.com/reference/analytics) · [Your key · 1 endpoints](https://docs.sellhidden.com/reference/keys) ## Build with AI Give these docs to your assistant: paste the prompt from [Build with AI](https://docs.sellhidden.com/ai), or hand it every page as one file. ## Get help Send us the endpoint, the request or delivery id and the response you got: [Developer support](https://sellhidden.com/support?as=developer). Security findings go through [responsible disclosure](https://sellhidden.com/security#disclosure). Live platform status is on the [support page](https://sellhidden.com/support#status). [Embed configurator](https://sellhidden.com/embed) · [Plan limits](https://sellhidden.com/pricing#compare) · [Webhook terms](https://sellhidden.com/terms#notifications-and-webhooks) · [Security: keys and signatures](https://sellhidden.com/security#integrations) --- # Start here Source: https://docs.sellhidden.com/start Five minutes from an empty store to a signed webhook on your server. Everything is on the Free plan. ## Your first call in five minutes The API lives under `https://api.sellhidden.com/v1`. Public endpoints need no key and are safe to call from a browser: they serve products, categories, visible reviews and recent sales for one store. Authenticated endpoints need a personal access key and manage your products, keys, orders, customers, coupons, reviews, tickets, store settings and webhooks; call them from your server only. ```http GET https://api.sellhidden.com/v1 # lists every endpoint with its full URL ``` Every response is JSON. Times are ISO 8601 in UTC. Money is USD with two decimals unless a field says otherwise. Ids are opaque strings; do not parse them. 1. **Create a store and a product** A $0 product is enough to test the whole flow without paying. 2. **Read your catalogue, no key needed** Type your store slug and press the button. 3. **Create an API key** Dashboard, Developers, API keys. The key is shown once; keep it on your server. 4. **Add a webhook endpoint** Developers, Webhooks. Subscribe to `order.paid`, copy the signing secret, press Send test event and check the signature with the snippet on the [Webhooks page](https://docs.sellhidden.com/webhooks#verify-signature). 5. **Sell from anywhere** Add the [Buy button](https://docs.sellhidden.com/buy-button) to your site or link straight to checkout. Call GET https://api.sellhidden.com/v1/public/products/ in a browser to see your products. **curl** ```bash # 1. Your catalogue, no key needed curl https://api.sellhidden.com/v1/public/products/my-store # 2. Your orders, with the key from Developers, API keys curl https://api.sellhidden.com/v1/orders?status=paid&limit=5 \ -H "Authorization: Bearer hsk_live_…" # 3. Receive a signed test event on your endpoint # (press "Send test event" in Developers, Webhooks, then verify X-Hidden-Signature) ``` **Node** ```javascript // 1. Your catalogue, no key needed const products = await fetch("https://api.sellhidden.com/v1/public/products/my-store").then((r) => r.json()); // 2. Your orders, with the key from Developers, API keys const key = process.env.HIDDEN_API_KEY; const orders = await fetch("https://api.sellhidden.com/v1/orders?status=paid&limit=5", { headers: { Authorization: `Bearer ${key}` }, }).then((r) => r.json()); // 3. Receive a signed test event on your endpoint // (press "Send test event" in Developers, Webhooks, then verify X-Hidden-Signature) ``` **Python** ```python import os, requests # 1. Your catalogue, no key needed products = requests.get("https://api.sellhidden.com/v1/public/products/my-store").json() # 2. Your orders, with the key from Developers, API keys key = os.environ["HIDDEN_API_KEY"] orders = requests.get( "https://api.sellhidden.com/v1/orders", params={"status": "paid", "limit": 5}, headers={"Authorization": f"Bearer {key}"}, ).json() # 3. Receive a signed test event on your endpoint # (press "Send test event" in Developers, Webhooks, then verify X-Hidden-Signature) ``` **PHP** ```php true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key], ]); $orders = json_decode(curl_exec($ch), true); // 3. Receive a signed test event on your endpoint // (press "Send test event" in Developers, Webhooks, then verify X-Hidden-Signature) ``` ## Authentication Your server sends Authorization: Bearer hsk_live_… to Hidden and gets a 200 back. You create your own API keys and choose what each one may do: read or write, per resource. Nobody else can make a key for your store. Keys start with `hsk_live_` and are shown once: we keep only a SHA-256 hash and the first characters for display. Up to 10 keys can be active per store; revoking one keeps it listed for the audit trail. 1. **Open Developers, API keys** In your dashboard. Any member of the store can open it. 2. **Create a key and pick its scopes** Name it after where it will run, tick read or write per resource, and set an expiry or an IP allowlist if you want one. 3. **Copy it once** The full key is shown one time. Keep it on your server; if it ever leaks, revoke it and make a new one. [Open API keys](https://dashboard.sellhidden.com/developers/api-keys) Send the key as a Bearer token from your server: **curl** ```bash curl \ https://api.sellhidden.com/v1/orders?status=paid&limit=25 \ -H "Authorization: Bearer hsk_live_…" ``` **Node** ```javascript const key = process.env.HIDDEN_API_KEY; const res = await fetch("https://api.sellhidden.com/v1/orders?status=paid&limit=25", { headers: { Authorization: `Bearer ${key}`, }, }); const data = await res.json(); ``` **Python** ```python import os import requests key = os.environ["HIDDEN_API_KEY"] res = requests.get( "https://api.sellhidden.com/v1/orders?status=paid&limit=25", headers={ "Authorization": f"Bearer {key}", }, ) data = res.json() ``` **PHP** ```php true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $key, ], ]); $data = json_decode(curl_exec($ch), true); ``` 401 unauthorized · 401 key_revoked · 401 key_expired · 403 ip_not_allowed · 403 insufficient_scope · 403 store_suspended | Code | Meaning | | --- | --- | | `401 unauthorized` | A missing or invalid key. | | `401 key_revoked` | A revoked key. | | `401 key_expired` | A key past its expiry. | | `403 ip_not_allowed` | A key used from an address outside its allowlist. | | `403 insufficient_scope` | A key without the permission an endpoint needs. | | `403 store_suspended` | A suspended store. | `GET /me` shows what a key can do. ## Keys and scopes Every key carries a list of scopes, one `read` and one `write` per resource; a write scope includes its read scope. Pick them when you create the key, together with an optional **IP allowlist** (addresses or CIDR ranges your servers call from) and an **expiry**. Each endpoint names the scope it needs. Writes made through the API appear in your store's audit log with the key that made them. | Resource | Scopes | Covers | | --- | --- | --- | | Products | `products:read`, `products:write` | Products, variations, keys and stock | | Categories | `categories:read`, `categories:write` | Categories and their order | | Coupons | `coupons:read`, `coupons:write` | Discount codes | | Orders | `orders:read`, `orders:write` | Orders, manual payments, refunds, redelivery | | Subscriptions | `subscriptions:read`, `subscriptions:write` | Recurring buyers: list, cancel, extend | | Customers | `customers:read`, `customers:write` | Buyers, store balance, bans | | Reviews | `reviews:read`, `reviews:write` | Replies and visibility | | Tickets | `tickets:read`, `tickets:write` | Support threads | | Store | `store:read`, `store:write` | Store settings and identity | | Webhooks | `webhooks:read`, `webhooks:write` | Webhook endpoints, secrets and test events | | Analytics | `analytics:read` | Revenue and order statistics | All 21 scopes together are shown as `*` (full access). ## Responses and errors Every body carries an `ok` boolean. Successful responses put their data next to it; failures carry one `error` object with a stable `code` you can branch on, a human-readable `message` and, for validation errors, the `field` at fault. ```json { "ok": true, "products": [ … ], "nextCursor": "Y2xxazF…", "hasMore": true } ``` ```json { "ok": false, "error": { "code": "validation_error", "message": "Give the product a title", "field": "title" } } ``` 200 · 201 · 400 · 401 · 403 · 404 · 409 · 422 · 429 | Status | Code | Meaning | | --- | --- | --- | | `200, 201` | — | Success (201 when something was created). | | `400` | bad_request, validation_error | A parameter or body field is malformed; validation errors name the field. | | `401` | unauthorized, key_revoked, key_expired | Missing, invalid, revoked or expired key. | | `403` | insufficient_scope, ip_not_allowed, store_suspended, plan_limit | The key may not do this, or the plan does not allow more. | | `404` | not_found | Unknown store, product, order, customer, ticket or webhook. | | `409` | conflict, … | A business rule refused the change; the code says which (invoice_open, partial_payment, pool_full, stock_from_keys…). | | `422` | idempotency_conflict | The Idempotency-Key was already used with a different request. | | `429` | rate_limited | Too many requests; honour Retry-After. | ## Rate limits Public endpoints allow 120 requests per minute per IP address and are cached for 60 seconds (`cache-control: public, s-maxage=60`), so a busy page rarely hits the origin. Authenticated endpoints allow 120 requests per minute per key. Every response includes `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`; a `429` also includes `Retry-After` in seconds. 120 requests per minute. X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset come with every response; a 429 adds Retry-After. ## Pagination List endpoints are cursor-based and return newest first. Pass `limit` (default and maximum vary per endpoint) and, for the next page, the `nextCursor` from the previous response. Cursors are opaque; do not build them yourself. Page one, then page two with nextCursor from the previous response, until hasMore is false. ```http # First page GET https://api.sellhidden.com/v1/orders?limit=50 # Next page: pass the cursor from the previous response GET https://api.sellhidden.com/v1/orders?limit=50&cursor=Y2xxazF… # Stop when "hasMore" is false (nextCursor is then null) ``` ## Idempotency A request can be lost after the server acted on it. Send an `Idempotency-Key` header (any unique string up to 255 characters, a UUID is fine) with a `POST` and you can safely retry it: for 24 hours the same key with the same body replays the first response, marked with `Idempotent-Replayed: true`, and nothing happens twice. The same key with a different body is refused with `422 idempotency_conflict`. Keys are per API key. Responses of 5xx are not stored, so a failed request can be retried with the same key. Two POSTs with the same Idempotency-Key create one category; the second answer carries Idempotent-Replayed: true. **curl** ```bash # First call creates the category curl -X POST https://api.sellhidden.com/v1/categories -H "Authorization: Bearer hsk_live_…" -H "Content-Type: application/json" -H "Idempotency-Key: 6f1c2a4e-…" -d '{ "name": "Software" }' # A retry with the same key and body replays the stored 201 # → header "Idempotent-Replayed: true", nothing is created twice # The same key with a different body → 422 idempotency_conflict ``` **Node** ```javascript // First call creates the category const key = process.env.HIDDEN_API_KEY; const idempotencyKey = crypto.randomUUID(); const res = await fetch("https://api.sellhidden.com/v1/categories", { method: "POST", headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", "Idempotency-Key": idempotencyKey, }, body: JSON.stringify({ name: "Software" }), }); // A retry with the same key and body replays the stored 201 // → header "Idempotent-Replayed: true", nothing is created twice // The same key with a different body → 422 idempotency_conflict ``` **Python** ```python import os, uuid, requests # First call creates the category key = os.environ["HIDDEN_API_KEY"] idempotency_key = str(uuid.uuid4()) res = requests.post( "https://api.sellhidden.com/v1/categories", headers={"Authorization": f"Bearer {key}", "Idempotency-Key": idempotency_key}, json={"name": "Software"}, ) # A retry with the same key and body replays the stored 201 # → header "Idempotent-Replayed: true", nothing is created twice # The same key with a different body → 422 idempotency_conflict ``` **PHP** ```php true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $key, 'Content-Type: application/json', 'Idempotency-Key: ' . $idempotencyKey, ], CURLOPT_POSTFIELDS => json_encode(['name' => 'Software']), ]); $res = curl_exec($ch); // A retry with the same key and body replays the stored 201 // → header "Idempotent-Replayed: true", nothing is created twice // The same key with a different body → 422 idempotency_conflict ``` ## OpenAPI and SDKs The whole API is described in an OpenAPI 3.1 document at `https://api.sellhidden.com/v1/openapi.json`: every path, parameter, body field, the scope each operation needs (`x-scope`) and which ones take an `Idempotency-Key`. Feed it to any generator to get a typed client in your language, or import it into your API tool. `GET https://api.sellhidden.com/v1` returns the same list with full URLs. - **[OpenAPI 3.1](/openapi.json)**: hidden-openapi.json · every path, field and scope - **[Postman collection](/postman.json)**: hidden-postman.json · one folder per group Import in Postman or Insomnia: paste `https://api.sellhidden.com/v1/openapi.json`. ```bash # The OpenAPI 3.1 document (public, cached a minute) curl https://api.sellhidden.com/v1/openapi.json # Generate a typed client, e.g. TypeScript npx openapi-typescript https://api.sellhidden.com/v1/openapi.json -o hidden-api.d.ts ``` --- # Build with AI Source: https://docs.sellhidden.com/ai Tell the assistant what you want. It reads these docs and writes the code with you. ## Prompt builder Read https://docs.sellhidden.com/llms-full.txt. It is the complete documentation of the Hidden API in one file. Help me connect my Hidden store (sellhidden.com) to my own code. Ask me first what I want to build: show my products on my website, get a message when an order is paid, deliver from my own server, or manage my store from code. Ask which language I use. Then write the integration step by step. Base URL https://api.sellhidden.com/v1. Ask me for my API key before any call, keep it in an environment variable, never log it and never put it in a URL or a chat message. Send Idempotency-Key on every POST. Every call is live: there is no test mode. ## Files for AI tools Every page is also Markdown: add `.md` to its address. - **[hidden-api-docs.md](/llms-full.txt?download=1)** `https://docs.sellhidden.com/llms-full.txt`: Every page in one file. Give it to your AI assistant. - **[llms.txt](/llms.txt)** `https://docs.sellhidden.com/llms.txt`: The index: one line per page with its Markdown address. - **[hidden-openapi.json](/openapi.json)** `https://docs.sellhidden.com/openapi.json`: OpenAPI 3.1: every path, field and scope. - **[hidden-postman.json](/postman.json)** `https://docs.sellhidden.com/postman.json`: Postman collection, one folder per group. - hidden-api-docs.md: https://docs.sellhidden.com/llms-full.txt - llms.txt: https://docs.sellhidden.com/llms.txt - hidden-openapi.json: https://docs.sellhidden.com/openapi.json - hidden-postman.json: https://docs.sellhidden.com/postman.json ## Rules for your coding tool Save this file as `AGENTS.md` in your project, or wherever your tool reads its rules. Your tool then knows the Hidden API on every later task. ```md # Hidden API: rules for coding tools - Base URL https://api.sellhidden.com/v1. Public endpoints need no key. Authenticated endpoints take `Authorization: Bearer `. - Ask the user for their API key and store slug. Never print, log or commit the key; keep it in an environment variable. - Send `Idempotency-Key` on every POST. - Verify every webhook: HMAC-SHA256 of `${X-Hidden-Timestamp}.${rawBody}` with the endpoint secret, compared in constant time; reject timestamps older than 5 minutes; dedupe by X-Hidden-Delivery; answer 2xx within 15 seconds. - There is no test mode: every call is live. Use a $0 product or a temporary key from the Try it panel to test. - Orders are created by buyers at checkout; the API manages them afterwards. ## Endpoints Base URL https://api.sellhidden.com/v1 Authorization: Bearer - Products: GET /products, POST /products, GET /products/{id}, PATCH /products/{id}, DELETE /products/{id}, GET /products/{id}/variations, POST /products/{id}/variations, GET /products/{id}/variations/{variationId}, PATCH /products/{id}/variations/{variationId}, DELETE /products/{id}/variations/{variationId}, GET /products/{id}/keys, POST /products/{id}/keys, DELETE /products/{id}/keys, POST /products/{id}/stock — scope products:read / products:write - Categories: GET /categories, POST /categories, GET /categories/{id}, PATCH /categories/{id}, DELETE /categories/{id}, POST /categories/reorder — scope categories:read / categories:write - Coupons: GET /coupons, POST /coupons, GET /coupons/{id}, PATCH /coupons/{id}, DELETE /coupons/{id} — scope coupons:read / coupons:write - Orders: GET /orders, GET /orders/{id}, POST /orders/{id}/approve, POST /orders/{id}/reject, POST /orders/{id}/cancel, POST /orders/{id}/redeliver, POST /orders/{id}/resend-receipt, POST /orders/{id}/refund, PUT /orders/{id}/note, DELETE /orders/{id}/note — scope orders:read / orders:write - Subscriptions: GET /subscriptions, GET /subscriptions/{id}, POST /subscriptions/{id}/cancel, POST /subscriptions/{id}/extend — scope subscriptions:read / subscriptions:write - Customers: GET /customers, GET /customers/{id}, POST /customers/{id}/balance, POST /customers/{id}/ban, DELETE /customers/{id}/ban — scope customers:read / customers:write - Reviews: GET /reviews, GET /reviews/{id}, PATCH /reviews/{id}, PUT /reviews/{id}/reply, DELETE /reviews/{id}/reply — scope reviews:read / reviews:write - Tickets: GET /tickets, GET /tickets/{id}, POST /tickets/{id}/messages, PATCH /tickets/{id}, POST /tickets/{id}/close — scope tickets:read / tickets:write - Store: GET /store, PATCH /store — scope store:read / store:write - Webhooks: GET /webhooks, POST /webhooks, GET /webhooks/{id}, PATCH /webhooks/{id}, DELETE /webhooks/{id}, POST /webhooks/{id}/rotate-secret, POST /webhooks/{id}/test, GET /webhooks/{id}/deliveries — scope webhooks:read / webhooks:write - Analytics: GET /analytics — scope analytics:read - Your key: GET /me — any key ## Webhooks - Every delivery is a signed POST: X-Hidden-Event, X-Hidden-Delivery, X-Hidden-Timestamp, X-Hidden-Signature (sha256=HMAC-SHA256 of `${timestamp}.${rawBody}` with the endpoint secret). Verify before you trust the payload. Docs index: https://docs.sellhidden.com/llms.txt ``` --- # Public API Source: https://docs.sellhidden.com/public Read products, categories, visible reviews and recent sales of any live store. No key, safe to call from a browser. ## Endpoints No key · CORS open · Cached 60 s · 120/min per IP A website reads products, prices and stock from Hidden with no key. Only live stores and active products are served; a suspended store is a `404`. Buyer emails in reviews are masked and order ids are never exposed. ### GET /public/products/{storeSlug} — Products of a store Active products in storefront order. `displayPriceUsd` is the lowest variation price (or the base price when there are no variations). For serial-key products `stock` is the number of unused keys. When the seller hides sold-out products on the storefront, they are omitted here too. No key needed. **Query parameters** | Query parameters | Type | Description | | --- | --- | --- | | `categorySlug` | string | Only products in this category. | | `limit` | 1..100 | Page size. Default 50. | | `cursor` | string | nextCursor from the previous page. | ```bash curl https://api.sellhidden.com/v1/public/products/my-store ``` Response (200): ```json { "ok": true, "store": { "slug": "my-store", "name": "My Store" }, "products": [ { "slug": "lifetime-license", "name": "Lifetime license", "displayPriceUsd": 29, "inStock": true, … } ], "nextCursor": null, "hasMore": false } ``` ### GET /public/products/{storeSlug}/{slug} — One product One product with its variations, category, stock and rating. This is the call the live-price embed makes. No key needed. ```bash curl \ https://api.sellhidden.com/v1/public/products/my-store/lifetime-license ``` Response (200): ```json { "ok": true, "store": { "slug": "my-store", "name": "My Store" }, "product": { "id": "clx…", "slug": "lifetime-license", "name": "Lifetime license", "description": "…", "priceUsd": 49, "displayPriceUsd": 29, "compareAtUsd": null, "currency": "USD", "coverImage": "https://…/cover.png", "deliveryType": "keys", "inStock": true, "stock": 120, "variations": [ { "id": "clx…", "label": "1 device", "priceUsd": 29, "compareAtUsd": null, "inStock": true, "stock": 80, "description": null }, { "id": "clx…", "label": "5 devices", "priceUsd": 49, "compareAtUsd": 69, "inStock": true, "stock": 40, "description": "Team pack" } ], "category": { "id": "clx…", "name": "Software", "slug": "software" }, "salesCount": 312, "reviewCount": 41, "averageRating": 4.8, "url": "https://my-store.sellhidden.com/p/lifetime-license", "createdAt": "2026-06-01T10:00:00.000Z", "updatedAt": "2026-09-20T08:12:44.000Z" } } ``` ### GET /public/categories/{storeSlug} — Categories of a store Categories in display order with the number of active products in each. Use `slug` as `categorySlug` on the products endpoint. No key needed. ```bash curl \ https://api.sellhidden.com/v1/public/categories/my-store ``` Response (200): ```json { "ok": true, "store": { "slug": "my-store", "name": "My Store" }, "categories": [ { "id": "clx…", "name": "Software", "slug": "software", "position": 0, "productCount": 12, "createdAt": "2026-05-02T09:00:00.000Z" } ] } ``` ### GET /public/reviews/{storeSlug} — Visible reviews Visible reviews with a store-wide summary. `distribution` counts one to five stars in order. Every review is tied to a paid order; sellers can hide reviews but never edit or delete them. `source` is `buyer`, or `auto` for an automatic rating recorded after seven silent days (never `verified`, and replaced as soon as the buyer writes their own). `editedAt` is set when the buyer changed their review. `sellerReply` is the store's public reply (null if none), with `sellerRepliedAt`. No key needed. **Query parameters** | Query parameters | Type | Description | | --- | --- | --- | | `productSlug` | string | Only reviews of this product. | | `rating` | 1..5 | Only this star rating. | | `limit` | 1..100 | Page size. Default 20. | | `cursor` | string | nextCursor from the previous page. | ```bash curl https://api.sellhidden.com/v1/public/reviews/my-store ``` Response (200): ```json { "ok": true, "store": { "slug": "my-store", "name": "My Store" }, "summary": { "count": 41, "average": 4.8, "distribution": [0, 1, 1, 5, 34] }, "reviews": [ { "id": "clx…", "productId": "clx…", "productSlug": "lifetime-license", "productName": "Lifetime license", "orderId": null, "buyerEmail": "j***e@e***.com", "rating": 5, "comment": "Delivered in seconds.", "verified": true, "visible": true, "source": "buyer", "editedAt": null, "sellerReply": "Thanks for the kind words!", "sellerRepliedAt": "2026-09-19T16:20:00.000Z", "createdAt": "2026-09-19T15:04:00.000Z", "updatedAt": "2026-09-19T16:20:00.000Z" } ], "nextCursor": null, "hasMore": false } ``` ### GET /public/recent-sales/{storeSlug} — Recent sales Social-proof feed of the latest paid items. Times are rounded to the hour and no buyer data is included. Returns an empty list when the seller turned the recent-sales widget off. No key needed. **Query parameters** | Query parameters | Type | Description | | --- | --- | --- | | `limit` | 1..25 | Page size. Default 10. | ```bash curl \ https://api.sellhidden.com/v1/public/recent-sales/my-store ``` Response (200): ```json { "ok": true, "store": { "slug": "my-store", "name": "My Store" }, "sales": [ { "product": { "slug": "lifetime-license", "name": "Lifetime license", "coverImage": null }, "variationLabel": "1 device", "quantity": 1, "country": null, "at": "2026-09-26T10:00:00.000Z" } ] } ``` --- # API reference Source: https://docs.sellhidden.com/reference Everything the dashboard does, from your own code. Send your key as a Bearer token from your server. ## Groups Base URL https://api.sellhidden.com/v1 · Authorization: Bearer · 120/min per key · cursor pagination · Idempotency-Key on POST · No test mode Send `Authorization: Bearer hsk_live_…` with a key you created under [Developers, API keys](https://docs.sellhidden.com/start#authentication). Everything is scoped to the key's store and to the key's scopes. Responses are never cached (`cache-control: no-store`). Writes go through the same rules as the dashboard: plan limits, delivery checks, payment locks and the audit log all apply. Orders are created by buyers at checkout; the API manages them afterwards. Every call is live. To look around safely, make a temporary read-only key in [Try it](https://docs.sellhidden.com/reference#try-it); it expires in 60 minutes. - Products: 14 endpoints - Categories: 6 endpoints - Coupons: 5 endpoints - Orders: 10 endpoints - Subscriptions: 4 endpoints - Customers: 5 endpoints - Reviews: 5 endpoints - Tickets: 5 endpoints - Store: 2 endpoints - Webhooks: 8 endpoints - Analytics: 1 endpoints - Your key: 1 endpoints ## Every endpoint All 71 endpoints on one list, public ones first. Filter by path, name or scope; every row links to its reference. | Method | Path | Summary | Scope | | --- | --- | --- | --- | | GET | `/public/products/{storeSlug}` | Products of a store | no key | | GET | `/public/products/{storeSlug}/{slug}` | One product | no key | | GET | `/public/categories/{storeSlug}` | Categories of a store | no key | | GET | `/public/reviews/{storeSlug}` | Visible reviews | no key | | GET | `/public/recent-sales/{storeSlug}` | Recent sales | no key | | GET | `/products` | List products | `products:read` | | POST | `/products` | Create a product | `products:write` | | GET | `/products/{id}` | Get a product | `products:read` | | PATCH | `/products/{id}` | Update a product | `products:write` | | DELETE | `/products/{id}` | Delete a product | `products:write` | | GET | `/products/{id}/variations` | List variations | `products:read` | | POST | `/products/{id}/variations` | Add a variation | `products:write` | | GET | `/products/{id}/variations/{variationId}` | Get a variation | `products:read` | | PATCH | `/products/{id}/variations/{variationId}` | Update a variation | `products:write` | | DELETE | `/products/{id}/variations/{variationId}` | Remove a variation | `products:write` | | GET | `/products/{id}/keys` | Pool counts and unused keys | `products:read` | | POST | `/products/{id}/keys` | Add keys in bulk | `products:write` | | DELETE | `/products/{id}/keys` | Remove unused keys | `products:write` | | POST | `/products/{id}/stock` | Set or adjust stock | `products:write` | | GET | `/categories` | List categories | `categories:read` | | POST | `/categories` | Create a category | `categories:write` | | GET | `/categories/{id}` | Get a category | `categories:read` | | PATCH | `/categories/{id}` | Update a category | `categories:write` | | DELETE | `/categories/{id}` | Delete a category | `categories:write` | | POST | `/categories/reorder` | Reorder categories | `categories:write` | | GET | `/coupons` | List coupons | `coupons:read` | | POST | `/coupons` | Create a coupon | `coupons:write` | | GET | `/coupons/{id}` | Get a coupon | `coupons:read` | | PATCH | `/coupons/{id}` | Update a coupon | `coupons:write` | | DELETE | `/coupons/{id}` | Delete a coupon | `coupons:write` | | GET | `/orders` | List orders | `orders:read` | | GET | `/orders/{id}` | Get an order | `orders:read` | | POST | `/orders/{id}/approve` | Approve a manual payment | `orders:write` | | POST | `/orders/{id}/reject` | Reject a manual payment | `orders:write` | | POST | `/orders/{id}/cancel` | Cancel an order | `orders:write` | | POST | `/orders/{id}/redeliver` | Run delivery again | `orders:write` | | POST | `/orders/{id}/resend-receipt` | Resend the receipt | `orders:write` | | POST | `/orders/{id}/refund` | Refund to store balance | `orders:write` | | PUT | `/orders/{id}/note` | Write the private note | `orders:write` | | DELETE | `/orders/{id}/note` | Remove the private note | `orders:write` | | GET | `/subscriptions` | List subscriptions | `subscriptions:read` | | GET | `/subscriptions/{id}` | Get a subscription | `subscriptions:read` | | POST | `/subscriptions/{id}/cancel` | Cancel a subscription | `subscriptions:write` | | POST | `/subscriptions/{id}/extend` | Extend by one period | `subscriptions:write` | | GET | `/customers` | List customers | `customers:read` | | GET | `/customers/{id}` | Get a customer | `customers:read` | | POST | `/customers/{id}/balance` | Adjust store balance | `customers:write` | | POST | `/customers/{id}/ban` | Ban a customer | `customers:write` | | DELETE | `/customers/{id}/ban` | Lift a ban | `customers:write` | | GET | `/reviews` | List reviews | `reviews:read` | | GET | `/reviews/{id}` | Get a review | `reviews:read` | | PATCH | `/reviews/{id}` | Hide or show a review | `reviews:write` | | PUT | `/reviews/{id}/reply` | Reply in public | `reviews:write` | | DELETE | `/reviews/{id}/reply` | Remove the reply | `reviews:write` | | GET | `/tickets` | List tickets | `tickets:read` | | GET | `/tickets/{id}` | Get a ticket | `tickets:read` | | POST | `/tickets/{id}/messages` | Reply to a ticket | `tickets:write` | | PATCH | `/tickets/{id}` | Change the status | `tickets:write` | | POST | `/tickets/{id}/close` | Close a ticket | `tickets:write` | | GET | `/store` | Get the store | `store:read` | | PATCH | `/store` | Update basic settings | `store:write` | | GET | `/webhooks` | List endpoints | `webhooks:read` | | POST | `/webhooks` | Create an endpoint | `webhooks:write` | | GET | `/webhooks/{id}` | Get an endpoint | `webhooks:read` | | PATCH | `/webhooks/{id}` | Update an endpoint | `webhooks:write` | | DELETE | `/webhooks/{id}` | Delete an endpoint | `webhooks:write` | | POST | `/webhooks/{id}/rotate-secret` | Rotate the secret | `webhooks:write` | | POST | `/webhooks/{id}/test` | Send a test event | `webhooks:write` | | GET | `/webhooks/{id}/deliveries` | Delivery log | `webhooks:read` | | GET | `/analytics` | Revenue and orders | `analytics:read` | | GET | `/me` | Inspect the key you are using | no key | ## Try it Send a request from this page. Signed in? Press **Use a temporary test key** and we make one for the store you pick: read-only unless you allow writes, gone after 60 minutes, and listed under your API keys so you can revoke it early. Or paste a key of your own. Either way the key stays in this tab and goes to the API only. Writes are real changes to your store. Use the Try it panel at https://docs.sellhidden.com/reference#try-it. --- # Products Source: https://docs.sellhidden.com/reference/products Your catalogue as the seller sees it: hidden products included, every variation with its deliverables, key pools and order counts. Writes follow the dashboard rules: plan limits, unique slugs, deliverable checks, the 2,000,000-key cap per variation and an audit log entry. ## Endpoints 14 endpoints · [products:read](https://docs.sellhidden.com/start?scope=products%3Aread#scopes) · [products:write](https://docs.sellhidden.com/start?scope=products%3Awrite#scopes) Base URL https://api.sellhidden.com/v1 · Authorization: Bearer · 120/min per key · cursor pagination · Idempotency-Key on POST · No test mode ### GET /products — List products Scope `products:read`. **Query parameters** | Query parameters | Type | Description | | --- | --- | --- | | `active` | true \| false | Only live or only hidden products. | | `categoryId` | string | Only this category. | | `q` | string | Title or slug contains this text. | | `deliveryType` | string | Primary delivery type (file, text, keys, link, discord, dynamic). | | `limit` | 1..100 | Page size. Default 50. | | `cursor` | string | nextCursor from the previous page. | ```bash curl https://api.sellhidden.com/v1/products \ -H "Authorization: Bearer hsk_live_…" ``` Response (200): ```json { "ok": true, "products": [ { "id": "clx…", "slug": "lifetime-license", "name": "Lifetime license", "active": true, "displayPriceUsd": 29, "stock": 120, "variations": [ … ], … } ], "nextCursor": null, "hasMore": false } ``` ### POST /products — Create a product A product has at least one variation. Send `variants` (recommended), or the short form `priceUsd` + `deliveryType` + `deliveryData` (+ `keys` for a key product) for a single-price product. Scope `products:write`. Accepts `Idempotency-Key`. Returns 201. **Body** | Body | Type | Description | | --- | --- | --- | | `title` (required) | string | Up to 120 characters. | | `slug` | string | URL slug; derived from the title when omitted and made unique. | | `description` | string | Markdown or plain text, up to 20,000 characters. | | `imageUrl` | string \| null | https URL or an uploaded /media URL. | | `categoryId` | string \| null | One of your categories. | | `variants` | array | Variations (see Variations below). At least one. | | `active` | boolean | Live on the storefront. Default true. Counts against the plan's product limit. | | `sortOrder` | integer | Lower first. Left out: placed after your other products. | | `seoTitle` | string \| null | Up to 70 characters. | | `seoDescription` | string \| null | Up to 200 characters. | ```bash curl -X POST https://api.sellhidden.com/v1/products \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "title": "Lifetime license", "description": "One key, forever.", "variants": [ { "label": "1 device", "priceUsd": 29, "deliverables": [ { "type": "keys", "keys": [ "AAAA-1111", "BBBB-2222" ] } ] } ] }' ``` Response (201): ```json { "ok": true, "product": { "id": "clx…", "slug": "lifetime-license", "active": true, "variations": [ { "id": "clx…", "label": "1 device", "priceUsd": 29, "stock": 2, "unusedKeys": 2, "deliverables": [ { "type": "keys", "data": "", "keyOrder": "fifo" } ] } ], … } } ``` ### GET /products/{id} — Get a product Scope `products:read`. ```bash curl https://api.sellhidden.com/v1/products/ID \ -H "Authorization: Bearer hsk_live_…" ``` ### PATCH /products/{id} — Update a product Only the fields you send change. `variants` replaces the whole variation list (variations left out are removed); to change one variation use the Variations endpoints instead. Scope `products:write`. **Body** | Body | Type | Description | | --- | --- | --- | | `title` | string | | | `slug` | string | | | `description` | string | | | `imageUrl` | string \| null | | | `categoryId` | string \| null | | | `active` | boolean | Activating respects the plan limit; a product removed by the platform stays off. | | `sortOrder` | integer | | | `seoTitle` | string \| null | | | `seoDescription` | string \| null | | | `variants` | array | Full list of variations (with `id` for existing ones). | ```bash curl -X PATCH https://api.sellhidden.com/v1/products/ID \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -d '{"active":false}' ``` ### DELETE /products/{id} — Delete a product Deleted for good when it was never ordered; archived (hidden, kept for order history) otherwise. `result` says which. Scope `products:write`. ```bash curl -X DELETE https://api.sellhidden.com/v1/products/ID \ -H "Authorization: Bearer hsk_live_…" ``` Response (200): ```json { "ok": true, "id": "clx…", "result": "deleted" } ``` ### GET /products/{id}/variations — List variations Scope `products:read`. ```bash curl https://api.sellhidden.com/v1/products/ID/variations \ -H "Authorization: Bearer hsk_live_…" ``` ### POST /products/{id}/variations — Add a variation Scope `products:write`. Accepts `Idempotency-Key`. Returns 201. **Body** | Body | Type | Description | | --- | --- | --- | | `label` (required) | string | Shown to buyers. Required when the product has more than one variation. | | `priceUsd` (required) | number | Price in USD. | | `compareAtUsd` | number \| null | Old price shown struck through. | | `stock` | integer \| null | Units left; null = unlimited. Ignored for variations that deliver keys (the pool is the stock). | | `description` | string | Short line under the label. | | `active` | boolean | Hidden variations cannot be bought. Default true. | | `minPerOrder` | 1..100 | Fewest units per order. Default 1. | | `maxPerOrder` | 1..100 \| null | Most units per order; null = no limit. | | `billingInterval` | week \| month \| quarter \| year \| null | A subscription billed per period; null (default) = one-time purchase. | | `trialDays` | 0..365 | Subscription: free trial days before the first payment. Default 0. | | `renewalDelivers` | boolean | Subscription: deliver again on every renewal (default true); false only extends access. | | `deliverables` | array | What the buyer receives: [{ type: file \| text \| keys \| link \| discord \| dynamic, data, keyOrder?: fifo \| lifo \| random, keys?: string[] }]. Each type at most once. Omitted on an existing variation = unchanged. | ```bash curl -X POST \ https://api.sellhidden.com/v1/products/ID/variations \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "label": "5 devices", "priceUsd": 49, "compareAtUsd": 69, "maxPerOrder": 3, "deliverables": [ { "type": "keys" } ] }' ``` ### GET /products/{id}/variations/{variationId} — Get a variation Scope `products:read`. ```bash curl \ https://api.sellhidden.com/v1/products/ID/variations/VARIATION_ID \ -H "Authorization: Bearer hsk_live_…" ``` ### PATCH /products/{id}/variations/{variationId} — Update a variation Only the fields you send change. Sending `deliverables` replaces that variation's deliverables. Scope `products:write`. **Body** | Body | Type | Description | | --- | --- | --- | | `label` | string | Shown to buyers. Required when the product has more than one variation. | | `priceUsd` | number | Price in USD. | | `compareAtUsd` | number \| null | Old price shown struck through. | | `stock` | integer \| null | Units left; null = unlimited. Ignored for variations that deliver keys (the pool is the stock). | | `description` | string | Short line under the label. | | `active` | boolean | Hidden variations cannot be bought. Default true. | | `minPerOrder` | 1..100 | Fewest units per order. Default 1. | | `maxPerOrder` | 1..100 \| null | Most units per order; null = no limit. | | `billingInterval` | week \| month \| quarter \| year \| null | A subscription billed per period; null (default) = one-time purchase. | | `trialDays` | 0..365 | Subscription: free trial days before the first payment. Default 0. | | `renewalDelivers` | boolean | Subscription: deliver again on every renewal (default true); false only extends access. | | `deliverables` | array | What the buyer receives: [{ type: file \| text \| keys \| link \| discord \| dynamic, data, keyOrder?: fifo \| lifo \| random, keys?: string[] }]. Each type at most once. Omitted on an existing variation = unchanged. | ```bash curl -X PATCH \ https://api.sellhidden.com/v1/products/ID/variations/VARIATION_ID \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -d '{"priceUsd":39,"minPerOrder":1,"maxPerOrder":5}' ``` ### DELETE /products/{id}/variations/{variationId} — Remove a variation A product keeps at least one variation (409 otherwise). A variation that was ordered is archived so its orders still deliver. Scope `products:write`. ```bash curl -X DELETE \ https://api.sellhidden.com/v1/products/ID/variations/VARIATION_ID \ -H "Authorization: Bearer hsk_live_…" ``` ### GET /products/{id}/keys — Pool counts and unused keys `pool` holds the counts (unused, used, shared pool, per variation); `keys` is one page of unused keys of one pool, oldest first. Scope `products:read`. **Query parameters** | Query parameters | Type | Description | | --- | --- | --- | | `variationId` | string | Which pool. Default: the product's only variation, else the product-wide shared pool. | | `q` | string | Key value contains this text. | | `limit` | 1..1000 | Page size. Default 100. | | `cursor` | string | nextCursor from the previous page. | ```bash curl https://api.sellhidden.com/v1/products/ID/keys \ -H "Authorization: Bearer hsk_live_…" ``` Response (200): ```json { "ok": true, "pool": { "unused": 1200, "used": 310, "shared": 0, "variations": { "clx…": 1200 } }, "variationId": "clx…", "keys": [ { "id": "k…", "value": "AAAA-1111", "createdAt": "…" } ], "nextCursor": "…", "hasMore": true } ``` ### POST /products/{id}/keys — Add keys in bulk Up to 50,000 keys per request (send big lists in batches); a pool holds up to 2,000,000 unused keys (400 pool_full past that). Keys already in the product's pools and repeats in the list are skipped. Orders that were waiting for keys are delivered, oldest first. Scope `products:write`. Accepts `Idempotency-Key`. Returns 201. **Body** | Body | Type | Description | | --- | --- | --- | | `keys` (required) | string[] | 1 to 50,000 keys, up to 50,000 characters each. | | `variationId` | string | Which pool. Default: the product's only variation, else the shared pool. | ```bash curl -X POST \ https://api.sellhidden.com/v1/products/ID/keys \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"keys":["AAAA-1111","BBBB-2222"]}' ``` Response (201): ```json { "ok": true, "added": 2, "skipped": 0, "skippedExisting": 0, "skippedRepeated": 0, "inOtherProducts": 0, "redelivered": 0, "stillWaiting": 0, "message": null, "pool": { "unused": 1202, "used": 310, "shared": 0, "variations": { "clx…": 1202 } } } ``` ### DELETE /products/{id}/keys — Remove unused keys Removes up to 500 unused keys by id or by value. Keys already handed to a buyer are never removed. Scope `products:write`. **Body** | Body | Type | Description | | --- | --- | --- | | `ids` | string[] | Key ids from the list endpoint. | | `values` | string[] | Key values. | ```bash curl -X DELETE \ https://api.sellhidden.com/v1/products/ID/keys \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -d '{"values":["AAAA-1111"]}' ``` Response (200): ```json { "ok": true, "removed": 1, "pool": { … } } ``` ### POST /products/{id}/stock — Set or adjust stock Sets (`stock`) or shifts (`adjust`, can be negative) the stock of one variation. Variations that deliver keys answer 409 stock_from_keys: add or remove keys instead. Scope `products:write`. Accepts `Idempotency-Key`. **Body** | Body | Type | Description | | --- | --- | --- | | `variationId` | string | Default: the product's only variation. | | `stock` | integer \| null | Absolute units; null = unlimited. | | `adjust` | integer | Relative change, e.g. -3. | ```bash curl -X POST \ https://api.sellhidden.com/v1/products/ID/stock \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"adjust":10}' ``` --- # Categories Source: https://docs.sellhidden.com/reference/categories Up to 100 categories per store. Deleting one leaves its products uncategorised. ## Endpoints 6 endpoints · [categories:read](https://docs.sellhidden.com/start?scope=categories%3Aread#scopes) · [categories:write](https://docs.sellhidden.com/start?scope=categories%3Awrite#scopes) Base URL https://api.sellhidden.com/v1 · Authorization: Bearer · 120/min per key · cursor pagination · Idempotency-Key on POST · No test mode ### GET /categories — List categories Scope `categories:read`. ```bash curl https://api.sellhidden.com/v1/categories \ -H "Authorization: Bearer hsk_live_…" ``` Response (200): ```json { "ok": true, "categories": [ { "id": "clx…", "name": "Software", "slug": "software", "position": 0, "productCount": 12, "activeProductCount": 11, "createdAt": "…" } ] } ``` ### POST /categories — Create a category Scope `categories:write`. Accepts `Idempotency-Key`. Returns 201. **Body** | Body | Type | Description | | --- | --- | --- | | `name` (required) | string | Unique in the store, up to 60 characters. | | `slug` | string | Derived from the name when omitted. | ```bash curl -X POST https://api.sellhidden.com/v1/categories \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"name":"Software"}' ``` ### GET /categories/{id} — Get a category Scope `categories:read`. ```bash curl https://api.sellhidden.com/v1/categories/ID \ -H "Authorization: Bearer hsk_live_…" ``` ### PATCH /categories/{id} — Update a category Scope `categories:write`. **Body** | Body | Type | Description | | --- | --- | --- | | `name` | string | | | `slug` | string | | | `position` | integer | Display position, 0 first. | ```bash curl -X PATCH https://api.sellhidden.com/v1/categories/ID \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -d '{"name":"Licenses"}' ``` ### DELETE /categories/{id} — Delete a category Scope `categories:write`. ```bash curl -X DELETE https://api.sellhidden.com/v1/categories/ID \ -H "Authorization: Bearer hsk_live_…" ``` Response (200): ```json { "ok": true, "id": "clx…", "uncategorisedProducts": 3 } ``` ### POST /categories/reorder — Reorder categories Scope `categories:write`. Accepts `Idempotency-Key`. **Body** | Body | Type | Description | | --- | --- | --- | | `order` (required) | string[] | Category ids in display order. | ```bash curl -X POST \ https://api.sellhidden.com/v1/categories/reorder \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"order":["clx1","clx2"]}' ``` --- # Coupons Source: https://docs.sellhidden.com/reference/coupons Discount codes. The code and the discount are fixed once created (orders keep what they used); every other rule can change. ## Endpoints 5 endpoints · [coupons:read](https://docs.sellhidden.com/start?scope=coupons%3Aread#scopes) · [coupons:write](https://docs.sellhidden.com/start?scope=coupons%3Awrite#scopes) Base URL https://api.sellhidden.com/v1 · Authorization: Bearer · 120/min per key · cursor pagination · Idempotency-Key on POST · No test mode ### GET /coupons — List coupons Scope `coupons:read`. **Query parameters** | Query parameters | Type | Description | | --- | --- | --- | | `active` | true \| false | Only usable codes (active and not expired), or only unusable ones. | | `code` | string | Code contains this text. | | `limit` | 1..200 | Page size. Default 50. | | `cursor` | string | nextCursor from the previous page. | ```bash curl https://api.sellhidden.com/v1/coupons \ -H "Authorization: Bearer hsk_live_…" ``` Response (200): ```json { "ok": true, "coupons": [ { "id": "clx…", "code": "LAUNCH20", "type": "percent", "value": 20, "minOrderUsd": null, "maxUses": 100, "usedCount": 12, "active": true, "expiresAt": null, "createdAt": "…" } ], "nextCursor": null, "hasMore": false } ``` ### POST /coupons — Create a coupon Scope `coupons:write`. Accepts `Idempotency-Key`. Returns 201. **Body** | Body | Type | Description | | --- | --- | --- | | `code` (required) | string | 2-40 characters: letters, digits, - and _. Stored upper-case. | | `percentOff` | 1..100 | Either this or amountOffUsd. | | `amountOffUsd` | number | Fixed discount in USD. | | `maxUses` | integer \| null | Total uses; null = unlimited. | | `perCustomerLimit` | 1..1000 \| null | Uses per buyer email. | | `firstOrderOnly` | boolean | Only buyers who never paid here. | | `minOrderUsd` | number \| null | Smallest cart subtotal (before the discount). | | `maxDiscountUsd` | number \| null | Cap on the discount. | | `startsAt` | ISO date \| null | Not valid before. | | `expiresAt` | ISO date \| null | Not valid after. | | `productIds` | string[] | Only these products (empty = all). | | `categoryIds` | string[] | Only these categories. | | `variantIds` | string[] | Only these variations. | | `paymentMethods` | string[] | "crypto", "balance" or a manual payment method id. | | `active` | boolean | Default true. | ```bash curl -X POST https://api.sellhidden.com/v1/coupons \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "code": "LAUNCH20", "percentOff": 20, "maxUses": 100, "expiresAt": "2026-12-31T23:59:59Z" }' ``` ### GET /coupons/{id} — Get a coupon Scope `coupons:read`. ```bash curl https://api.sellhidden.com/v1/coupons/ID \ -H "Authorization: Bearer hsk_live_…" ``` ### PATCH /coupons/{id} — Update a coupon Any rule except code, percentOff and amountOffUsd. null clears a limit or a date; null or [] clears a list. Scope `coupons:write`. **Body** | Body | Type | Description | | --- | --- | --- | | `active` | boolean | | | `maxUses` | integer \| null | Not below the uses so far. | | `perCustomerLimit` | integer \| null | | | `firstOrderOnly` | boolean | | | `minOrderUsd` | number \| null | | | `maxDiscountUsd` | number \| null | | | `startsAt` | ISO date \| null | | | `expiresAt` | ISO date \| null | | | `productIds` | string[] \| null | | | `categoryIds` | string[] \| null | | | `variantIds` | string[] \| null | | | `paymentMethods` | string[] \| null | | ```bash curl -X PATCH https://api.sellhidden.com/v1/coupons/ID \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -d '{"active":false}' ``` ### DELETE /coupons/{id} — Delete a coupon Scope `coupons:write`. ```bash curl -X DELETE https://api.sellhidden.com/v1/coupons/ID \ -H "Authorization: Bearer hsk_live_…" ``` --- # Orders Source: https://docs.sellhidden.com/reference/orders Orders are created by buyers at checkout. The API reads them and runs the same actions as the dashboard: approve or reject a manual payment, cancel, redeliver, resend the receipt, refund to store balance, keep a private note. Every order has a short reference in `publicRef` (for example `7K2Q-9XHD`, or `NOVA-7K2Q-9XHD` when the store has an order ID prefix): it is what buyers see on their order page and receipts, and it works wherever `{id}` appears in an order path, next to the long `id`. ## Endpoints 10 endpoints · [orders:read](https://docs.sellhidden.com/start?scope=orders%3Aread#scopes) · [orders:write](https://docs.sellhidden.com/start?scope=orders%3Awrite#scopes) Base URL https://api.sellhidden.com/v1 · Authorization: Bearer · 120/min per key · cursor pagination · Idempotency-Key on POST · No test mode ### GET /orders — List orders Scope `orders:read`. **Query parameters** | Query parameters | Type | Description | | --- | --- | --- | | `status` | string | pending, awaiting_verification, paid, delivered, cancelled, underpaid, oversold, expired, failed, refunded. | | `kind` | product \| topup | Product orders or buyer balance top-ups. | | `email` | string | Buyer email contains this text. | | `since` | ISO date | Created at or after. | | `until` | ISO date | Created before. | | `limit` | 1..100 | Page size. Default 25. | | `cursor` | string | nextCursor from the previous page. | ```bash curl https://api.sellhidden.com/v1/orders \ -H "Authorization: Bearer hsk_live_…" ``` ### GET /orders/{id} — Get an order {id} is the order's `publicRef` (any case, hyphen optional) or its long id. Scope `orders:read`. ```bash curl https://api.sellhidden.com/v1/orders/ID \ -H "Authorization: Bearer hsk_live_…" ``` Response (200): ```json { "order": { "id": "clx…", "publicRef": "7K2Q-9XHD", "status": "delivered", … } } ``` ### POST /orders/{id}/approve — Approve a manual payment Marks the order as paid and delivers it. A manual-method claim is verified (the platform fee is debited from your balance); a crypto order that received part of its total is settled on what arrived; any other payable order is settled as paid outside the platform, which needs `confirmOutsidePlatform: true` while its invoice is still open (409 invoice_open otherwise). Scope `orders:write`. Accepts `Idempotency-Key`. **Body** | Body | Type | Description | | --- | --- | --- | | `confirmOutsidePlatform` | boolean | The buyer paid you directly, outside the platform. | ```bash curl -X POST \ https://api.sellhidden.com/v1/orders/ID/approve \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{}' ``` Response (200): ```json { "ok": true, "credited": 0, "feeDebited": 0.95, "warning": null, "order": { "id": "clx…", "status": "delivered", … } } ``` ### POST /orders/{id}/reject — Reject a manual payment Cancels a manual-method order with the reason; the buyer is emailed. Scope `orders:write`. Accepts `Idempotency-Key`. **Body** | Body | Type | Description | | --- | --- | --- | | `reason` | string | Up to 500 characters, shown to the buyer. | ```bash curl -X POST \ https://api.sellhidden.com/v1/orders/ID/reject \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"reason":"No payment received."}' ``` ### POST /orders/{id}/cancel — Cancel an order Pending, awaiting-verification, underpaid or expired orders. An order holding a partial crypto payment answers 409 partial_payment: approve or refund it instead. Scope `orders:write`. Accepts `Idempotency-Key`. **Body** | Body | Type | Description | | --- | --- | --- | | `reason` | string | Private, up to 500 characters. | ```bash curl -X POST \ https://api.sellhidden.com/v1/orders/ID/cancel \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{}' ``` ### POST /orders/{id}/redeliver — Run delivery again For a paid order with items still not delivered (oversold, failed). 409 already_delivered when everything was delivered: resend the receipt instead. Scope `orders:write`. Accepts `Idempotency-Key`. ```bash curl -X POST \ https://api.sellhidden.com/v1/orders/ID/redeliver \ -H "Authorization: Bearer hsk_live_…" \ -H "Idempotency-Key: $(uuidgen)" ``` Response (200): ```json { "ok": true, "delivered": 2, "failed": 0, "order": { … } } ``` ### POST /orders/{id}/resend-receipt — Resend the receipt Emails the buyer their receipt and items again. Paid orders only; 3 per order and 60 per store per hour. Scope `orders:write`. Accepts `Idempotency-Key`. ```bash curl -X POST \ https://api.sellhidden.com/v1/orders/ID/resend-receipt \ -H "Authorization: Bearer hsk_live_…" \ -H "Idempotency-Key: $(uuidgen)" ``` ### POST /orders/{id}/refund — Refund to store balance The buyer receives store credit. A paid order is refunded in full or for `amountUsd`; an underpaid or expired order with a partial crypto payment hands over everything that arrived. Scope `orders:write`. Accepts `Idempotency-Key`. **Body** | Body | Type | Description | | --- | --- | --- | | `amountUsd` | number | Partial refund of a paid order; default: the full amount. | | `reason` | string | Up to 500 characters. | ```bash curl -X POST \ https://api.sellhidden.com/v1/orders/ID/refund \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"reason":"Duplicate purchase."}' ``` Response (200): ```json { "ok": true, "creditedUsd": 29, "order": { "id": "clx…", "status": "refunded", … } } ``` ### PUT /orders/{id}/note — Write the private note A note only you see (API and dashboard, as `order.note`). POST works the same. Scope `orders:write`. **Body** | Body | Type | Description | | --- | --- | --- | | `note` (required) | string | Up to 2,000 characters. | ```bash curl -X PUT https://api.sellhidden.com/v1/orders/ID/note \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -d '{"note":"Verified by hand on 2026-10-01."}' ``` ### DELETE /orders/{id}/note — Remove the private note Scope `orders:write`. ```bash curl -X DELETE \ https://api.sellhidden.com/v1/orders/ID/note \ -H "Authorization: Bearer hsk_live_…" ``` --- # Subscriptions Source: https://docs.sellhidden.com/reference/subscriptions Buyers of a variation billed per period (weekly to yearly). Renewals are fresh orders the buyer pays from the renewal email or the order page, or from their store balance; after the grace period access ends by itself. The subscription.* webhook events carry the same shape. ## Endpoints 4 endpoints · [subscriptions:read](https://docs.sellhidden.com/start?scope=subscriptions%3Aread#scopes) · [subscriptions:write](https://docs.sellhidden.com/start?scope=subscriptions%3Awrite#scopes) Base URL https://api.sellhidden.com/v1 · Authorization: Bearer · 120/min per key · cursor pagination · Idempotency-Key on POST · No test mode ### GET /subscriptions — List subscriptions Scope `subscriptions:read`. **Query parameters** | Query parameters | Type | Description | | --- | --- | --- | | `status` | trialing \| active \| past_due \| cancelled \| expired | Only this status. | | `email` | string | Buyer email contains this text. | | `product` | string | Product id. | | `limit` | 1..100 | Page size. Default 25. | | `cursor` | string | nextCursor from the previous page. | ```bash curl https://api.sellhidden.com/v1/subscriptions \ -H "Authorization: Bearer hsk_live_…" ``` Response (200): ```json { "ok": true, "subscriptions": [ { "subscriptionId": "clx…", "status": "active", "buyerEmail": "jo@example.com", "productId": "clx…", "productTitle": "Pro plan", "variantId": "clx…", "quantity": 1, "priceUsd": 9, "interval": "month", "currentPeriodStart": "…", "currentPeriodEnd": "…", "graceEndsAt": "…", "cancelAtPeriodEnd": false, "autoRenewFromBalance": false, "initialOrderId": "clx…", "renewalOrderId": null, "renewals": 2, "endedAt": null, "createdAt": "…" } ], "nextCursor": null, "hasMore": false } ``` ### GET /subscriptions/{id} — Get a subscription Scope `subscriptions:read`. ```bash curl https://api.sellhidden.com/v1/subscriptions/ID \ -H "Authorization: Bearer hsk_live_…" ``` ### POST /subscriptions/{id}/cancel — Cancel a subscription By default the buyer keeps access until the period end and nothing more is charged (cancelAtPeriodEnd). With immediate: true access ends now: Discord roles are removed, the pending renewal order is cancelled and subscription.expired is sent with the keys it delivered. Scope `subscriptions:write`. Accepts `Idempotency-Key`. **Body** | Body | Type | Description | | --- | --- | --- | | `immediate` | boolean | End now instead of at the period end. Default false. | ```bash curl -X POST \ https://api.sellhidden.com/v1/subscriptions/ID/cancel \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{}' ``` ### POST /subscriptions/{id}/extend — Extend by one period Adds one billing period for free from the current end (or from now when it already ended) and brings an expired or cancelled subscription back, Discord roles included. Scope `subscriptions:write`. Accepts `Idempotency-Key`. ```bash curl -X POST \ https://api.sellhidden.com/v1/subscriptions/ID/extend \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{}' ``` --- # Customers Source: https://docs.sellhidden.com/reference/customers Everyone who checked out or signed in on your storefront, with lifetime aggregates. ## Endpoints 5 endpoints · [customers:read](https://docs.sellhidden.com/start?scope=customers%3Aread#scopes) · [customers:write](https://docs.sellhidden.com/start?scope=customers%3Awrite#scopes) Base URL https://api.sellhidden.com/v1 · Authorization: Bearer · 120/min per key · cursor pagination · Idempotency-Key on POST · No test mode ### GET /customers — List customers Scope `customers:read`. **Query parameters** | Query parameters | Type | Description | | --- | --- | --- | | `email` | string | Email contains this text. | | `hasAccount` | true \| false | Only buyers who signed in (or only guests). | | `limit` | 1..100 | Page size. Default 50. | | `cursor` | string | nextCursor from the previous page. | ```bash curl https://api.sellhidden.com/v1/customers \ -H "Authorization: Bearer hsk_live_…" ``` ### GET /customers/{id} — Get a customer With aggregates and the ten most recent orders. Scope `customers:read`. ```bash curl https://api.sellhidden.com/v1/customers/ID \ -H "Authorization: Bearer hsk_live_…" ``` Response (200): ```json { "ok": true, "customer": { "id": "clx…", "email": "buyer@example.com", "name": null, "hasAccount": true, "balanceUsd": 12.5, "lastLoginAt": "2026-09-25T18:20:00.000Z", "createdAt": "2026-07-01T12:00:00.000Z", "orderCount": 4, "topUpCount": 1, "totalSpentUsd": 116, "openTicketCount": 0, "recentOrders": [ { "id": "clx…", "status": "delivered", … } ] } } ``` ### POST /customers/{id}/balance — Adjust store balance Adds (positive) or removes (negative) store credit. The balance never goes below zero (409 balance_too_low). The reason is written to the audit log. Scope `customers:write`. Accepts `Idempotency-Key`. **Body** | Body | Type | Description | | --- | --- | --- | | `amountUsd` (required) | number | Positive to credit, negative to debit. | | `reason` (required) | string | Up to 300 characters. | ```bash curl -X POST \ https://api.sellhidden.com/v1/customers/ID/balance \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "amountUsd": 10, "reason": "Goodwill credit for the delayed delivery." }' ``` Response (200): ```json { "ok": true, "customer": { "id": "clx…", "email": "buyer@example.com", "balanceUsd": 10, … }, "change": { "amountUsd": 10, "reason": "…" } } ``` ### POST /customers/{id}/ban — Ban a customer Blocks the buyer's email at checkout (a blacklist rule you also see under Blacklist in the dashboard). Scope `customers:write`. Accepts `Idempotency-Key`. Returns 201. **Body** | Body | Type | Description | | --- | --- | --- | | `reason` | string | Private note, up to 300 characters. | ```bash curl -X POST \ https://api.sellhidden.com/v1/customers/ID/ban \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"reason":"Chargeback."}' ``` Response (201): ```json { "ok": true, "customer": { "id": "clx…", "email": "buyer@example.com", "banned": true }, "ban": { "ruleId": "clx…", "reason": "Chargeback.", "createdAt": "…" }, "alreadyBanned": false } ``` ### DELETE /customers/{id}/ban — Lift a ban Scope `customers:write`. ```bash curl -X DELETE \ https://api.sellhidden.com/v1/customers/ID/ban \ -H "Authorization: Bearer hsk_live_…" ``` --- # Reviews Source: https://docs.sellhidden.com/reference/reviews Buyer reviews with unmasked emails. Reviews are never deleted: hide spam instead, or answer in public. ## Endpoints 5 endpoints · [reviews:read](https://docs.sellhidden.com/start?scope=reviews%3Aread#scopes) · [reviews:write](https://docs.sellhidden.com/start?scope=reviews%3Awrite#scopes) Base URL https://api.sellhidden.com/v1 · Authorization: Bearer · 120/min per key · cursor pagination · Idempotency-Key on POST · No test mode ### GET /reviews — List reviews Scope `reviews:read`. **Query parameters** | Query parameters | Type | Description | | --- | --- | --- | | `productId` | string | Only this product. | | `rating` | 1..5 | Only this rating. | | `visible` | true \| false | Only shown or only hidden reviews. | | `limit` | 1..1000 | Page size. Default 50. | | `cursor` | string | nextCursor from the previous page. | ```bash curl https://api.sellhidden.com/v1/reviews \ -H "Authorization: Bearer hsk_live_…" ``` ### GET /reviews/{id} — Get a review Scope `reviews:read`. ```bash curl https://api.sellhidden.com/v1/reviews/ID \ -H "Authorization: Bearer hsk_live_…" ``` ### PATCH /reviews/{id} — Hide or show a review Scope `reviews:write`. **Body** | Body | Type | Description | | --- | --- | --- | | `visible` (required) | boolean | false hides it from the storefront. | ```bash curl -X PATCH https://api.sellhidden.com/v1/reviews/ID \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -d '{"visible":false}' ``` ### PUT /reviews/{id}/reply — Reply in public Writes or edits the store's reply under the review. The buyer is emailed once per review, on the first reply. Automatic ratings cannot be answered (409 review_automatic). POST works the same. Scope `reviews:write`. Accepts `Idempotency-Key`. **Body** | Body | Type | Description | | --- | --- | --- | | `reply` (required) | string | Up to 1,000 characters. | ```bash curl -X PUT https://api.sellhidden.com/v1/reviews/ID/reply \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"reply":"Thanks for the kind words!"}' ``` ### DELETE /reviews/{id}/reply — Remove the reply Scope `reviews:write`. ```bash curl -X DELETE \ https://api.sellhidden.com/v1/reviews/ID/reply \ -H "Authorization: Bearer hsk_live_…" ``` --- # Tickets Source: https://docs.sellhidden.com/reference/tickets Support threads buyers open from their order or account. Replies email the buyer. ## Endpoints 5 endpoints · [tickets:read](https://docs.sellhidden.com/start?scope=tickets%3Aread#scopes) · [tickets:write](https://docs.sellhidden.com/start?scope=tickets%3Awrite#scopes) Base URL https://api.sellhidden.com/v1 · Authorization: Bearer · 120/min per key · cursor pagination · Idempotency-Key on POST · No test mode ### GET /tickets — List tickets Scope `tickets:read`. **Query parameters** | Query parameters | Type | Description | | --- | --- | --- | | `status` | open \| answered \| closed | | | `email` | string | Buyer email contains this text. | | `limit` | 1..100 | Page size. Default 25. | | `cursor` | string | nextCursor from the previous page. | ```bash curl https://api.sellhidden.com/v1/tickets \ -H "Authorization: Bearer hsk_live_…" ``` ### GET /tickets/{id} — Get a ticket With its full thread; `fromBuyer` tells you who wrote each message. Scope `tickets:read`. ```bash curl https://api.sellhidden.com/v1/tickets/ID \ -H "Authorization: Bearer hsk_live_…" ``` Response (200): ```json { "ok": true, "ticket": { "id": "clx…", "orderId": "clx…", "buyerEmail": "buyer@example.com", "subject": "Key not activating", "status": "answered", "messageCount": 2, "lastMessageAt": "2026-09-26T11:00:00.000Z", "createdAt": "2026-09-26T10:30:00.000Z", "messages": [ { "id": "clx…", "fromBuyer": true, "body": "The key says invalid.", "createdAt": "2026-09-26T10:30:00.000Z" }, { "id": "clx…", "fromBuyer": false, "body": "Try again without the dashes.", "createdAt": "2026-09-26T11:00:00.000Z" } ] } } ``` ### POST /tickets/{id}/messages — Reply to a ticket The ticket becomes answered and the buyer is emailed. A closed ticket must be reopened first. Scope `tickets:write`. Accepts `Idempotency-Key`. Returns 201. **Body** | Body | Type | Description | | --- | --- | --- | | `body` (required) | string | Up to 10,000 characters. | ```bash curl -X POST \ https://api.sellhidden.com/v1/tickets/ID/messages \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"body":"Sorry about that. A new key is on its way."}' ``` ### PATCH /tickets/{id} — Change the status Scope `tickets:write`. **Body** | Body | Type | Description | | --- | --- | --- | | `status` (required) | open \| answered \| closed | | ```bash curl -X PATCH https://api.sellhidden.com/v1/tickets/ID \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -d '{"status":"open"}' ``` ### POST /tickets/{id}/close — Close a ticket Scope `tickets:write`. Accepts `Idempotency-Key`. ```bash curl -X POST \ https://api.sellhidden.com/v1/tickets/ID/close \ -H "Authorization: Bearer hsk_live_…" \ -H "Idempotency-Key: $(uuidgen)" ``` --- # Store Source: https://docs.sellhidden.com/reference/store Your store as its seller sees it, and the basic settings. Themes, payment methods, payouts and domains stay in the dashboard. ## Endpoints 2 endpoints · [store:read](https://docs.sellhidden.com/start?scope=store%3Aread#scopes) · [store:write](https://docs.sellhidden.com/start?scope=store%3Awrite#scopes) Base URL https://api.sellhidden.com/v1 · Authorization: Bearer · 120/min per key · cursor pagination · Idempotency-Key on POST · No test mode ### GET /store — Get the store Scope `store:read`. ```bash curl https://api.sellhidden.com/v1/store \ -H "Authorization: Bearer hsk_live_…" ``` Response (200): ```json { "ok": true, "store": { "id": "clx…", "slug": "my-store", "name": "My Store", "url": "https://my-store.sellhidden.com", "plan": "pro", "balanceUsd": 1240.5, "feeBornBy": "seller", "support": { "email": "help@example.com", "discord": null, "telegram": null }, "settings": { "maintenanceMode": false, "hideOutOfStock": false, … }, … } } ``` ### PATCH /store — Update basic settings Only the fields you send change. Scope `store:write`. **Body** | Body | Type | Description | | --- | --- | --- | | `name` | string | 2-80 characters. | | `description` | string | Up to 500 characters. | | `logoUrl` | string \| null | | | `bannerUrl` | string \| null | | | `supportEmail` | string \| null | | | `supportDiscord` | string \| null | Invite URL. | | `supportTelegram` | string \| null | | | `maintenanceMode` | boolean | Closes the storefront to buyers. | | `hideOutOfStock` | boolean | | | `showStockCount` | boolean | | | `showStats` | boolean | | | `recentSalesWidget` | boolean | Live sale notifications toggle. | | `liveSalesCorner` | string | bottom-left \| bottom-right \| top-left \| top-right (default bottom-left). | | `autoReviews` | boolean | | | `autoCurrency` | boolean | Deprecated; use currencyMode ("auto"). | | `currencyMode` | usd \| fixed \| auto | Default display currency: USD, a fixed one you pick, or the currency of the buyer's location. | | `defaultCurrency` | string | ISO 4217 code used when currencyMode is "fixed". | | `showCurrencyPicker` | boolean | False hides the currency picker everywhere and the shop stays in USD. | | `feeBornBy` | seller \| customer | Who would pay a platform fee. Every plan has a 0% platform fee, so this has no effect today. | | `productsPerPage` | 4..48 | | | `subscriptionReminderDays` | 1..14 | Days before a renewal the reminder email goes out. | | `subscriptionGraceDays` | 0..3 | Days a buyer keeps access after a missed renewal. | ```bash curl -X PATCH https://api.sellhidden.com/v1/store \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -d '{"maintenanceMode":true}' ``` --- # Webhooks Source: https://docs.sellhidden.com/reference/webhooks Manage the endpoints that receive signed events (the same ones as Developers, Webhooks). The signing secret is returned once, on create and on rotate. ## Endpoints 8 endpoints · [webhooks:read](https://docs.sellhidden.com/start?scope=webhooks%3Aread#scopes) · [webhooks:write](https://docs.sellhidden.com/start?scope=webhooks%3Awrite#scopes) Base URL https://api.sellhidden.com/v1 · Authorization: Bearer · 120/min per key · cursor pagination · Idempotency-Key on POST · No test mode ### GET /webhooks — List endpoints Scope `webhooks:read`. ```bash curl https://api.sellhidden.com/v1/webhooks \ -H "Authorization: Bearer hsk_live_…" ``` Response (200): ```json { "ok": true, "webhooks": [ { "id": "clx…", "url": "https://example.com/hooks", "label": "Fulfilment", "events": [ "order.paid" ], "active": true, "consecutiveFailures": 0, "secretHint": "whsec_1a2b…", "lastDelivery": { … }, "deliveryCount": 42, "createdAt": "…" } ], "limit": 5 } ``` ### POST /webhooks — Create an endpoint Public https URLs only. Counts against the plan's endpoint limit (403 plan_limit). Scope `webhooks:write`. Accepts `Idempotency-Key`. Returns 201. **Body** | Body | Type | Description | | --- | --- | --- | | `url` (required) | string | https URL. | | `events` (required) | string[] | Event names (see Webhooks, Events), or wildcards like order.* or *. | | `label` | string | Up to 60 characters. | ```bash curl -X POST https://api.sellhidden.com/v1/webhooks \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "url": "https://example.com/hooks/hidden", "events": [ "order.paid", "order.delivered" ], "label": "Fulfilment" }' ``` Response (201): ```json { "ok": true, "webhook": { "id": "clx…", "url": "…", "events": [ "order.paid", "order.delivered" ], "active": true, "secret": "whsec_…", … } } ``` ### GET /webhooks/{id} — Get an endpoint Scope `webhooks:read`. ```bash curl https://api.sellhidden.com/v1/webhooks/ID \ -H "Authorization: Bearer hsk_live_…" ``` ### PATCH /webhooks/{id} — Update an endpoint Scope `webhooks:write`. **Body** | Body | Type | Description | | --- | --- | --- | | `url` | string | | | `label` | string | | | `events` | string[] | | | `active` | boolean | Switch it on again after it was disabled for failures. | | `resetFailures` | boolean | Reset the consecutive failure counter. | ```bash curl -X PATCH https://api.sellhidden.com/v1/webhooks/ID \ -H "Authorization: Bearer hsk_live_…" \ -H "Content-Type: application/json" \ -d '{"active":true,"resetFailures":true}' ``` ### DELETE /webhooks/{id} — Delete an endpoint Scope `webhooks:write`. ```bash curl -X DELETE https://api.sellhidden.com/v1/webhooks/ID \ -H "Authorization: Bearer hsk_live_…" ``` ### POST /webhooks/{id}/rotate-secret — Rotate the secret Returns the new secret once. Deliveries are signed with it from now on. Scope `webhooks:write`. Accepts `Idempotency-Key`. ```bash curl -X POST \ https://api.sellhidden.com/v1/webhooks/ID/rotate-secret \ -H "Authorization: Bearer hsk_live_…" \ -H "Idempotency-Key: $(uuidgen)" ``` ### POST /webhooks/{id}/test — Send a test event A signed webhook.test delivery (10 per endpoint per minute). The response carries the delivery row with the status code your endpoint returned. Scope `webhooks:write`. ```bash curl -X POST \ https://api.sellhidden.com/v1/webhooks/ID/test \ -H "Authorization: Bearer hsk_live_…" ``` ### GET /webhooks/{id}/deliveries — Delivery log Scope `webhooks:read`. **Query parameters** | Query parameters | Type | Description | | --- | --- | --- | | `status` | ok \| failed | | | `limit` | 1..100 | Page size. Default 25. | | `cursor` | string | nextCursor from the previous page. | ```bash curl https://api.sellhidden.com/v1/webhooks/ID/deliveries \ -H "Authorization: Bearer hsk_live_…" ``` --- # Analytics Source: https://docs.sellhidden.com/reference/analytics Revenue and order statistics. ## Endpoints 1 endpoint · [analytics:read](https://docs.sellhidden.com/start?scope=analytics%3Aread#scopes) Base URL https://api.sellhidden.com/v1 · Authorization: Bearer · 120/min per key · cursor pagination · Idempotency-Key on POST · No test mode ### GET /analytics — Revenue and orders Scope `analytics:read`. **Query parameters** | Query parameters | Type | Description | | --- | --- | --- | | `period` | 24h \| 7d \| 30d \| 90d \| 365d | Window ending now. Default 30d. | ```bash curl https://api.sellhidden.com/v1/analytics \ -H "Authorization: Bearer hsk_live_…" ``` Response (200): ```json { "ok": true, "period": "7d", "revenue": { "totalUsd": 1240.5, "daily": [ { "t": "2026-09-20T00:00:00.000Z", "value": 180 }, … ] }, "orders": { "total": 43, "daily": [ { "t": "2026-09-20T00:00:00.000Z", "value": 6 }, … ] }, "customerCount": 39, "topProducts": [ { "productId": "clx…", "slug": "lifetime-license", "title": "Lifetime license", "imageUrl": null, "units": 21, "revenueUsd": 609, "orders": 21 } ] } ``` --- # Your key Source: https://docs.sellhidden.com/reference/keys Any valid key can inspect itself: its store, scopes, expiry and rate-limit window. ## Endpoints 1 endpoint Base URL https://api.sellhidden.com/v1 · Authorization: Bearer · 120/min per key · cursor pagination · Idempotency-Key on POST · No test mode ### GET /me — Inspect the key you are using Any valid key: its store, scopes, expiry and the rate-limit window. Handy to check a key without touching data. No key needed. ```bash curl https://api.sellhidden.com/v1/me ``` --- # Webhooks Source: https://docs.sellhidden.com/webhooks Hidden sends your server a signed POST for every event you subscribe to, retries when you fail, and shows every attempt in the dashboard. ## What an order goes through Add endpoints under **Developers, Webhooks**. Each endpoint has a label, a list of subscribed events, its own signing secret (shown once; rotate it any time) and a delivery log you can filter and resend from. Plans allow 1 endpoint on Free, 5 on Starter and unlimited on Pro and Enterprise; logs are kept 7, 14, 30 and 90 days respectively. Endpoints must be public https URLs; private and local addresses are rejected. Order events: order.created → order.paid → order.delivered; branches: underpaid, manual_claimed, expired, cancelled, delivery_failed, oversold, refunded. Every box is a webhook event. Most orders take the top row: created, paid, delivered. ## Events | Event | Sent when | | --- | --- | | `order.created` | A checkout was submitted and an invoice issued. Nothing is paid yet. | | `order.paid` | Payment confirmed, or a manual payment verified. Delivery starts right after. | | `order.delivered` | Every item in the order was delivered. | | `order.delivery_failed` | One or more items could not be delivered. Adds reason (oversold or delivery_failed) and failures, one entry per item with its error. This event used to be sent as order.failed: endpoints set up before the rename were moved to that older name, so they keep receiving it as order.failed until you switch them to order.delivery_failed (listed under Older names). Wildcard subscriptions (order.* or *) receive it as order.delivery_failed, plus order.oversold for oversold orders. | | `order.oversold` | Stock ran out for a paid order, so part of it waits for a restock. Sent alongside order.delivery_failed with the same payload. | | `order.underpaid` | The buyer sent less than the amount due. Adds receivedUsd, remainingUsd and payableUntil: the buyer can pay the rest until then, or you accept or refund what arrived. | | `order.manual_claimed` | A buyer submitted proof for a manual payment method. Adds proof, proofImage (true when an image was attached) and manualMethod. Verify it in the dashboard. | | `order.expired` | The invoice expired without a payment. | | `order.cancelled` | Cancelled by the seller, an admin or a refunded payment. Adds reason. | | `order.refunded` | Refunded to the buyer's store balance. Adds refundedUsd, reason and partialPayment (true when only part of the total had arrived). | | `subscription.created` | A buyer paid the first period of a subscription variation, or started its free trial. The payload carries the subscription (id, status, interval, quantity, currentPeriodStart, currentPeriodEnd, graceEndsAt, trial), the buyer and the product, plus orderId and the keys delivered so far. | | `subscription.renewed` | The buyer paid a renewal (from the email link, the order page or automatically from store balance). Adds the renewal orderId and the new period dates. | | `subscription.expiring` | The period ends within the store's reminder window and the buyer was emailed a one-click renewal checkout. Adds renewalOrderId and renewalUrl. | | `subscription.cancelled` | The buyer or the seller cancelled the subscription. It stays active until currentPeriodEnd (cancelAtPeriodEnd) unless the seller ended it at once. Adds cancelledBy and endsAt. | | `subscription.expired` | The period and its grace days passed without a renewal, or a cancelled subscription reached its period end. Discord roles granted by it are removed; the payload lists every license key it delivered (keys) so you can disable them on your side. | | `topup.paid` | A buyer added funds to their store balance. Same payload as order.paid, with orderKind topup. | | `product.low_stock` | A product or variation has 3 units or fewer left after a sale. Adds productId, productSlug, title, variantId, variantLabel, deliveryType, remaining and threshold. | | `product.out_of_stock` | A product or variation sold its last unit. Same payload as product.low_stock, with remaining 0. | | `ticket.opened` | A buyer opened a support ticket. Adds ticketId, orderId, buyerEmail, subject and message. | | `ticket.reply` | A buyer replied on one of their tickets. Adds ticketId, messageId, orderId, buyerEmail, subject, message and from (buyer). | | `review.created` | A buyer left a review. replacedAutomatic is true when it replaces an automatic rating. | | `review.edited` | A buyer edited their review while it could still be changed. | | `withdrawal.requested` | A withdrawal was requested from the store balance, by you or by the automatic threshold. | | `withdrawal.processed` | A withdrawal was sent (with the transaction hash) or returned to the balance. | | `affiliate.commission.created` | A paid order was attributed to an affiliate, or you added a manual adjustment. Adds commissionId, affiliateId, affiliateCode, affiliateEmail, orderId, amountUsd, baseUsd, pct, status (pending, or flagged when a fraud guard tripped), flagReason and releaseAt. | | `affiliate.commission.released` | The hold passed (or you released it) and the amount was added to the affiliate's store balance. Same payload, with releasedAt. | | `affiliate.commission.reversed` | The order was refunded or you reversed the commission. Adds reason and clawedBackUsd (what came back out of an already credited balance). | | `store.debt_limit` | Your balance went past the debt limit and manual payment methods were paused, or it recovered and they resumed. Adds paused, balanceUsd and debtLimitUsd. | | `plan.changed` | A paid plan was activated or renewed, or the store went back to Free when it ended. Adds plan, period and planExpiresAt. | | `webhook.test` | You pressed Send test event in the dashboard. Never sent automatically. | ## Headers and payload X-Hidden-Event · X-Hidden-Delivery · X-Hidden-Timestamp · X-Hidden-Signature Deliveries are JSON POSTs. `X-Hidden-Event` names the event, `X-Hidden-Delivery` is a unique id per delivery attempt, `X-Hidden-Timestamp` is unix seconds and `X-Hidden-Signature` carries the HMAC. Order events share one `data` shape: ```http POST https://example.com/hooks/hidden Content-Type: application/json User-Agent: Hidden-Webhooks/1.0 (+https://sellhidden.com) X-Hidden-Event: order.paid X-Hidden-Timestamp: 1790000000 X-Hidden-Signature: sha256=3f8a…c21e X-Hidden-Delivery: whd_01j9… { "id": "evt_9f2c…", "event": "order.paid", "createdAt": "2026-09-26T10:01:12.000Z", "data": { "orderId": "clx…", "orderKind": "product", "storeId": "clx…", "status": "paid", "buyerEmail": "buyer@example.com", "buyerName": null, "discordUserId": null, "grossUsd": 29, "discountUsd": 0, "buyerTotalUsd": 29, "sellerNetUsd": 29, "platformFeeUsd": 0, "platformFeePaidByBuyerUsd": 0, "feeBornBy": "seller", "networkFeeUsd": 0.42, "couponCode": null, "paymentMethod": "crypto", "paidWithBalance": false, "payCurrency": "USDT", "network": "TRON", "txHash": "7c1e…a90b", "paidAmountUsd": 29, "paidCurrency": "USDT", "paidAt": "2026-09-26T10:01:12.000Z", "deliveredAt": null, "refundedAt": null, "createdAt": "2026-09-26T09:58:40.000Z", "customFields": {}, "items": [ { "itemId": "clx…", "productId": "clx…", "productSlug": "lifetime-license", "title": "Lifetime license", "variantId": "clx…", "variantLabel": "Lifetime", "quantity": 1, "unitPriceUsd": 29, "deliveryStatus": "pending" } ] } } ``` Respond with any 2xx status within 15 seconds. Anything else counts as a failure. The body you return is ignored. Orders a seller creates with **Test order** in the dashboard send the same events with `"test": true` in `data` (and in the dynamic delivery request), and have `test: true` in the API. Nothing was paid for them: never fulfil, book or count a test order. ## Verifying signatures Every delivery is signed with **HMAC-SHA256**. The signed string is the timestamp header, a dot, and the raw request body. Compute the digest with the endpoint secret, compare it in constant time with the header value (after the `sha256=` prefix), and reject anything older than a few minutes. Three steps: join the timestamp header, a dot and the raw body; compute HMAC-SHA256 with the endpoint secret; compare with X-Hidden-Signature in constant time. **Node** ```javascript import { createHmac, timingSafeEqual } from "node:crypto"; // Use the raw request body exactly as received (do not JSON.parse first). export function verifyHiddenSignature(rawBody: string, headers: Headers, secret: string) { const header = headers.get("x-hidden-signature") ?? ""; // "sha256=" const timestamp = headers.get("x-hidden-timestamp") ?? ""; // unix seconds // Reject stale deliveries (replay protection), 5 minute tolerance. if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const expected = createHmac("sha256", secret) .update(`${timestamp}.${rawBody}`) .digest("hex"); const given = header.replace(/^sha256=/, ""); if (given.length !== expected.length) return false; return timingSafeEqual(Buffer.from(given, "hex"), Buffer.from(expected, "hex")); } // Next.js route handler export async function POST(req: Request) { const raw = await req.text(); if (!verifyHiddenSignature(raw, req.headers, process.env.HIDDEN_WEBHOOK_SECRET!)) { return new Response("invalid signature", { status: 401 }); } const event = JSON.parse(raw); // De-duplicate on the X-Hidden-Delivery header before acting. // ...handle event.data return new Response("ok"); } ``` **Python** ```python import hmac, hashlib, time def verify_hidden_signature(raw_body: bytes, headers: dict, secret: str) -> bool: given = headers.get("x-hidden-signature", "").removeprefix("sha256=") ts = headers.get("x-hidden-timestamp", "0") if abs(time.time() - int(ts)) > 300: return False expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(given, expected) ``` **PHP** ```php 300) { http_response_code(401); exit; } // stale: replay protection $expected = hash_hmac("sha256", $timestamp . "." . $raw, getenv("HIDDEN_SECRET")); if (!hash_equals($expected, $given)) { http_response_code(401); exit; } $order = json_decode($raw, true); if (!empty($order["test"])) { /* a test from the product editor: nothing was paid */ } header("Content-Type: application/json"); echo json_encode(["deliveredData" => "Your licence: HDN-4K2P-88QL-9XQ1"]); ``` ## Retries and duplicates A delivery that times out or gets a 5xx, 408 or 429 response is retried up to six times: about 30 minutes, then 1, 2, 6, 12 and 24 hours after each failed attempt. Retries are sent in batches every 30 minutes, so one can arrive up to 30 minutes after it is due. Other 4xx responses are treated as permanent and not retried. Every retry carries the same `X-Hidden-Delivery` id with a fresh timestamp and signature, so store that id and ignore duplicates. After 6 consecutive failures the endpoint is switched off; fix your receiver and switch it back on in the dashboard. The dashboard log shows every attempt with its status code and when the next retry is due. Use **Send test event** to receive a signed `webhook.test` delivery, and **Resend** on any failed row to replay its exact payload. Retry schedule after a failed attempt: 30 min, 1 h, 2 h, 6 h, 12 h, 24 h; the endpoint is switched off after 6 failures in a row. [Open webhooks in the dashboard](https://dashboard.sellhidden.com/developers/webhooks) --- # Dynamic delivery Source: https://docs.sellhidden.com/dynamic-delivery Set a product's delivery type to dynamic and paste your URL. On every paid order Hidden POSTs the signed order and delivers the deliveredData string you answer with. Use it to create accounts, generate keys or call your own licence server. ## How it works A paid order reaches Hidden, Hidden asks your server, your server answers with a key, and the key is delivered to the buyer. The request has the same format as a webhook. Your JSON answer's `deliveredData` is what the buyer receives. ```http // Your endpoint receives the same signed payload as a webhook. // Respond within 10 seconds with what the buyer should receive: HTTP/1.1 200 OK Content-Type: application/json { "deliveredData": "Your licence: HDN-4K2P-88QL-9XQ1\nActivate at https://example.com/activate", "note": "Shown to the buyer under the item (optional)" } // Anything other than 2xx (or a timeout) marks the item "failed" with your response // body as the error. You can redeliver from the order page in the dashboard. // Your own supplier account cannot pay for this order? Answer 200 with // { "status": "failed", "code": "insufficient_balance", "error": "Balance too low" } // The item then waits for you instead of failing: the buyer is told it is on its way, // you get an alert, and Redeliver sends it once you have topped up. ``` **Try it before you sell.** In the product editor, **Send test request** POSTs a signed sample order to your address (`"test": true` in the body and an `X-Hidden-Test: 1` header). No order is created and nobody is charged. The editor then shows what a real order would do with your answer. "test": true · X-Hidden-Test: 1 ## Checking the signature Every request carries `X-Hidden-Timestamp` (unix seconds) and `X-Hidden-Signature` (`sha256=` + HMAC-SHA256 of `${timestamp}.${rawBody}` with the product's secret from the editor). Compute the same HMAC over the raw body and compare in constant time, rejecting timestamps older than a few minutes. Three steps: join the timestamp header, a dot and the raw body; compute HMAC-SHA256 with the product's secret; compare with X-Hidden-Signature in constant time. **Node** ```javascript import { createHmac, timingSafeEqual } from "node:crypto"; // Use the raw request body exactly as received (do not JSON.parse first). export function verifyHiddenSignature(rawBody: string, headers: Headers, secret: string) { const header = headers.get("x-hidden-signature") ?? ""; // "sha256=" const timestamp = headers.get("x-hidden-timestamp") ?? ""; // unix seconds // Reject stale deliveries (replay protection), 5 minute tolerance. if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const expected = createHmac("sha256", secret) .update(`${timestamp}.${rawBody}`) .digest("hex"); const given = header.replace(/^sha256=/, ""); if (given.length !== expected.length) return false; return timingSafeEqual(Buffer.from(given, "hex"), Buffer.from(expected, "hex")); } // Next.js route handler export async function POST(req: Request) { const raw = await req.text(); if (!verifyHiddenSignature(raw, req.headers, process.env.HIDDEN_WEBHOOK_SECRET!)) { return new Response("invalid signature", { status: 401 }); } const event = JSON.parse(raw); // De-duplicate on the X-Hidden-Delivery header before acting. // ...handle event.data return new Response("ok"); } ``` **Python** ```python import hmac, hashlib, time def verify_hidden_signature(raw_body: bytes, headers: dict, secret: str) -> bool: given = headers.get("x-hidden-signature", "").removeprefix("sha256=") ts = headers.get("x-hidden-timestamp", "0") if abs(time.time() - int(ts)) > 300: return False expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(given, expected) ``` **PHP** ```php 300) { http_response_code(401); exit; } // stale: replay protection $expected = hash_hmac("sha256", $timestamp . "." . $raw, getenv("HIDDEN_SECRET")); if (!hash_equals($expected, $given)) { http_response_code(401); exit; } $order = json_decode($raw, true); if (!empty($order["test"])) { /* a test from the product editor: nothing was paid */ } header("Content-Type: application/json"); echo json_encode(["deliveredData" => "Your licence: HDN-4K2P-88QL-9XQ1"]); ``` > **Always verify the signature.** Anyone who learns your endpoint URL could POST to it. Verify the signature before provisioning anything. --- # Buy button and live price Source: https://docs.sellhidden.com/buy-button Include one script and give any element your store and product: clicking it opens your checkout on your own page. A price span keeps itself current, and your page gets an event for every order. ## Buy button A live Buy button demo is on the HTML page. Give any element `data-hidden-store` and `data-hidden-product`, include `embed.js` once, and clicking opens a checkout modal with your live product page. The buyer never leaves the host page; the modal closes with its X, Escape, a click outside, or from checkout once the order is placed. ```html ``` The script sets no cookies, reads nothing on the host page and takes the checkout host from its own `src`, so a copied snippet cannot be redirected. Messages from the modal are accepted only from that origin. The v1 snippet (` ``` > **The order id needs a site key.** An order id opens the order page and its delivered items, so the modal only hands it to a site you named: create a site key for your website address under **Developers, Embeds** and add it to the script tag as `data-hidden-key`. The modal learns your page's origin from the browser, checks the key against it and then posts to that exact origin only. On any other page `orderId` is `null`. After removing a team member or giving up a domain, the store owner can revoke every site key at once in the same place; your sites keep selling and get the order id again once they have a new key. Treat the event as a signal for your interface; fulfil from the signed `order.paid` webhook (match it with `customFields.ref`). ## Your own iframe Framing the checkout yourself instead of using embed.js? Answer the modal's `hidden:ready` message with `hidden:hello` and you receive the same messages: ```js // Your own