Developer docs
Start here
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.
GET https://api.sellhidden.com/v1
# lists every endpoint with its full URLEvery 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. - 5
Sell from anywhere
Add the Buy button to your site or link straight to checkout.
# 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)// 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)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
// 1. Your catalogue, no key needed
$products = json_decode(file_get_contents('https://api.sellhidden.com/v1/public/products/my-store'), true);
// 2. Your orders, with the key from Developers, API keys
$key = getenv('HIDDEN_API_KEY');
$ch = curl_init('https://api.sellhidden.com/v1/orders?status=paid&limit=5');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => 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)Shown in curl
Authentication
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.
Send the key as a Bearer token from your server:
curl \
https://api.sellhidden.com/v1/orders?status=paid&limit=25 \
-H "Authorization: Bearer hsk_live_…"| 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.
Scopes
AllProducts
Products, variations, keys and stock
readwriteCategories
Categories and their order
readwriteCoupons
Discount codes
readwriteOrders
Orders, manual payments, refunds, redelivery
readwriteSubscriptions
Recurring buyers: list, cancel, extend
readwriteCustomers
Buyers, store balance, bans
readwriteReviews
Replies and visibility
readwriteTickets
Support threads
readwriteStore
Store settings and identity
readwriteWebhooks
Webhook endpoints, secrets and test events
readwriteAnalytics
Revenue and order statistics
read
This key can call 24 of 65 endpoints
products:read,categories:read,coupons:read,orders:read,subscriptions:read,customers:read,reviews:read,tickets:read,store:read,webhooks:read,analytics:readAll 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.
{ "ok": true, "products": [ … ], "nextCursor": "Y2xxazF…", "hasMore": true }{ "ok": false, "error": { "code": "validation_error", "message": "Give the product a title", "field": "title" } }| Status | Code | Meaning |
|---|---|---|
| 200, 201 | Success (201 when something was created). | |
| 400 | Code: bad_request, validation_error | A parameter or body field is malformed; validation errors name the field. |
| 401 | Code: unauthorized, key_revoked, key_expired | Missing, invalid, revoked or expired key. |
| 403 | Code: insufficient_scope, ip_not_allowed, store_suspended, plan_limit | The key may not do this, or the plan does not allow more. |
| 404 | Code: not_found | Unknown store, product, order, customer, ticket or webhook. |
| 409 | Code: conflict, … | A business rule refused the change; the code says which (invoice_open, partial_payment, pool_full, stock_from_keys…). |
| 422 | Code: idempotency_conflict | The Idempotency-Key was already used with a different request. |
| 429 | Code: 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.
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.
# 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.
# 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// 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_conflictimport 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
// First call creates the category
$key = getenv('HIDDEN_API_KEY');
$idempotencyKey = bin2hex(random_bytes(16));
$ch = curl_init('https://api.sellhidden.com/v1/categories');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => 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_conflictShown in curl
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
hidden-openapi.json · every path, field and scope
Postman collection
hidden-postman.json · one folder per group
Import in Postman or Insomnia: paste https://api.sellhidden.com/v1/openapi.json.
# 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.tsGet help
Send us the endpoint, the request or delivery id and the response you got. Security findings go through responsible disclosure. Live platform status is on the support page.
See also