Skip to content

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.

discoveryhttp
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. 1

    Create a store and a product

    A $0 product is enough to test the whole flow without paying.

  2. 2

    Read your catalogue, no key needed

    Type your store slug and press the button.

  3. 3

    Create an API key

    Dashboard, Developers, API keys. The key is shown once; keep it on your server.

  4. 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. 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. 1

    Open Developers, API keys

    In your dashboard. Any member of the store can open it.

  2. 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. 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_…"
401 unauthorized401 key_revoked401 key_expired403 ip_not_allowed403 insufficient_scope403 store_suspended
Key errors
CodeMeaning
401 unauthorizedA missing or invalid key.
401 key_revokedA revoked key.
401 key_expiredA key past its expiry.
403 ip_not_allowedA key used from an address outside its allowlist.
403 insufficient_scopeA key without the permission an endpoint needs.
403 store_suspendedA 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

All
  • Products

    Products, variations, keys and stock

    readwrite
  • Categories

    Categories and their order

    readwrite
  • Coupons

    Discount codes

    readwrite
  • Orders

    Orders, manual payments, refunds, redelivery

    readwrite
  • Subscriptions

    Recurring buyers: list, cancel, extend

    readwrite
  • Customers

    Buyers, store balance, bans

    readwrite
  • Reviews

    Replies and visibility

    readwrite
  • Tickets

    Support threads

    readwrite
  • Store

    Store settings and identity

    readwrite
  • Webhooks

    Webhook endpoints, secrets and test events

    readwrite
  • Analytics

    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:read

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.

successjson
{ "ok": true, "products": [ … ], "nextCursor": "Y2xxazF…", "hasMore": true }
failurejson
{ "ok": false, "error": { "code": "validation_error", "message": "Give the product a title", "field": "title" } }
200201400401403404409422429
Status codes
StatusCodeMeaning
200, 201Success (201 when something was created).
400Code: bad_request, validation_errorA parameter or body field is malformed; validation errors name the field.
401Code: unauthorized, key_revoked, key_expiredMissing, invalid, revoked or expired key.
403Code: insufficient_scope, ip_not_allowed, store_suspended, plan_limitThe key may not do this, or the plan does not allow more.
404Code: not_foundUnknown store, product, order, customer, ticket or webhook.
409Code: conflict, …A business rule refused the change; the code says which (invoice_open, partial_payment, pool_full, stock_from_keys…).
422Code: idempotency_conflictThe Idempotency-Key was already used with a different request.
429Code: rate_limitedToo 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.

X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-Reset429Retry-After
120 requests per minute per key. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 carries 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.

Each page returns up to limit items and a nextCursor; pass it as cursor to get the next page. A null nextCursor means the last page.
paginghttp
# 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_conflict
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
// 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_conflict

Shown 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.

Import in Postman or Insomnia: paste https://api.sellhidden.com/v1/openapi.json.

openapibash
# 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

Get 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