Skip to contentSellstamp

API reference

API reference

The same API the dashboard uses, with no private back doors. Versioned REST under /api/v1, authenticated with a scoped key you can restrict to one store or one product, rotate without an outage, and revoke in a click. 50 public endpoints today — each one named below, with the scope it wants, and all 50 described by an OpenAPI document you can import straight into Postman.

Request

curl https://<api-host>/api/v1/public/orders \
  -H "Authorization: Bearer sellstamp_live_…" \
  -G -d limit=50

RateLimit-Limit: 120
RateLimit-Remaining: 119
RateLimit-Reset: 47

Every response says where you stand in the budget.

Authentication

A bearer key, resolved by its hash on a unique index exactly as a session cookie is. The workspace comes from the key's own row, which is why a caller can never name a workspace and why isolation here is structural rather than a check somebody has to remember.

The key
sellstamp_live_… or sellstamp_test_…, 32 random bytes from the same generator behind session cookies. Stored as a SHA-256 hash — the raw value exists only in the response that created it, is in no log, and support cannot recover it. Lost one? Revoke it and make another.
A test key is not a sandbox
The two modes behave identically: same tables, same rows, same real side effects. Nothing in the product branches on the mode. It exists so a key in the wrong environment’s log is recognisable at a glance, and so you can revoke a test integration without touching a production one.
The two refusals
No key, a malformed header, an unknown key and a revoked key all return one byte-identical 401 — that is the only comparison an attacker holding no valid credential can make. A key that authenticates but lacks the scope gets a 403 naming the scope the route requires, never what your key holds.
Rotation, not a second secret
Rotating issues a new key with the same authority and puts the old one on a deadline — 24 hours by default, zero for an immediate cut-over after a leak, up to a week. Both rows show in the dashboard. Rotation is allowed even at your plan’s key limit: replacing a leaked credential must never be the thing a plan blocks.
Expiry
Create a key with an end date and past it, it returns the same generic 401 as a revoked one — and stops occupying a plan slot, because it authenticates nothing.

Scopes, and where they reach

A scope says what a key may do. Its store list says where. Both are chosen when the key is made; the scopes can be edited afterwards without changing the secret, so widening or narrowing an integration costs no redeploy.

orders.read
Read orders and their line items (GET /public/orders, GET /public/orders/:orderId)
orders.refund
Refund an order in full (POST /public/orders/:orderId/refund)
products.read
Read products and their variants (GET /public/products, GET /public/reviews, GET /public/shop, GET /public/changes, GET /public/products/:productId)
products.write
Create products (POST /public/products)
products.update
Edit existing products (PATCH /public/products/:productId)
products.delete
Archive products, taking them off sale (DELETE /public/products/:productId)
customers.read
Read the customer list and what each has bought (GET /public/customers, GET /public/customers/:email)
coupons.write
Create discount coupons (POST /public/coupons)
licenses.read
Read issued licenses (GET /public/licenses, POST /public/licenses/lookup)
licenses.manage
Suspend and resume licenses, reset their activations and release one device (GET /public/licenses/:licenseId/activations, POST /public/licenses/:licenseId/suspend, POST /public/licenses/:licenseId/resume, POST /public/licenses/:licenseId/reset-activations, POST /public/licenses/:licenseId/activations/:activationId/release)
courses.read
Read courses, their students and each student's progress (GET /public/courses, GET /public/courses/:courseId/students, GET /public/courses/:courseId/students/:studentId)
courses.write
Enrol students in a course and revoke their access (POST /public/courses/:courseId/students, POST /public/courses/:courseId/students/:studentId/revoke)
community.read
Read paid community access: who is in, who is waiting to connect, and why access ended (GET /public/community/members, GET /public/community/members/:accessId)
loyalty.read
Read loyalty members: points balances, tiers and each member's history (GET /public/loyalty/members, GET /public/loyalty/members/:accountId)
loyalty.write
Add or take away a customer's loyalty points, with a reason (large adjustments still need a person) (POST /public/loyalty/adjust)
wishlists.read
Read which products customers wishlist most, and how many are waiting for an alert (GET /public/wishlists/insight)
apps.develop
Manage your own marketplace apps: submit versions, read their logs, run conformance checks, create a test store (GET /public/developer/apps, PUT /public/developer/apps/:listingId/manifest, POST /public/developer/apps/:listingId/submit, GET /public/developer/apps/:listingId/logs, POST /public/developer/apps/:listingId/conformance, POST /public/developer/test-store)
  • Restrict it to a store

    A key with no store list reaches every store in the workspace, including ones you add later. Name one and it reaches only that one. Name two and every list has to say which — a silently merged view of some of a workspace's stores is a number you would read as the whole workspace.

  • Or to a product

    Narrower than a store and applied on top of it: that product, the orders containing it, and its licences. Such a key cannot create products — whatever it made would not be one of its own, so it could not read back the thing it just made.

  • A withheld store is a 404

    Naming a store the key was not granted answers exactly as naming one that does not exist, which is what stops a restricted key from using the store field to enumerate the rest of the workspace.

The endpoints

50 of them, read from the OpenAPI document this API serves rather than typed here — which is the only reason this number is right. The scope list above is derived from the same place: a scope with no endpoint behind it would be a promise the API cannot keep.

GET /public/orders
Needs orders.read. Your orders, newest first. Lists sales, so a test purchase you made to yourself is not in it — an integration syncing this into an accounting ledger must not import an order you made to yourself.
GET /public/orders/:orderId
Needs orders.read. One order, whole, line items included. No shop is named — the key’s own restrictions are the entire filter, so an order outside them is a 404. A test purchase fetched directly does come back, marked isTest: the row is not a secret, it is just not a sale.
POST /public/orders/:orderId/refund
Needs orders.refund. Refund an order in full. The same function behind the refund button, with its row lock and its ceilings — and narrower on purpose: full only, never as store credit, and it never withdraws a customer’s downloads or licences. That last one is a staff decision with its own permission, and no scope grants it.
GET /public/products
Needs products.read. Your catalogue with its variants.
POST /public/products
Needs products.write. Create a product. It runs the same function the dashboard and onboarding call, so your plan limit, the licensing checks and the slug rules apply here by construction rather than by this route remembering them.
GET /public/reviews
Needs products.read.
GET /public/shop
Needs products.read.
GET /public/changes
Needs products.read.
GET /public/products/:productId
Needs products.read.
PATCH /public/products/:productId
Needs products.update. Edit a product. Its own scope, not products.write, so a key that was granted “create” did not quietly become one that can reprice your catalogue. It runs the same function the edit form calls, so the slug history, the recorded list price, the forward-only licence rules and the plan checks all still apply.
DELETE /public/products/:productId
Needs products.delete. Take a listing off sale. It archives: there is no hard delete, because an order has to keep pointing at a real product row. The listing leaves the storefront and checkout and frees its plan slot; every existing order, download and licence is untouched, and you can restore it from the dashboard.
GET /public/customers
Needs customers.read. The customers of one store, and what each has spent net of refunds. Needs a shopId unless your key is restricted to exactly one store. Deliberately narrower than your own customer page: no notes, no tags, no risk flags, no store credit, no payment providers, and no marketing-consent state.
GET /public/customers/:email
Needs customers.read. One customer and the orders they placed at that store. Same redactions as the list. The address is a path segment, so percent-encode it.
POST /public/coupons
Needs coupons.write. Create a discount code. Same normalisation and same per-store uniqueness as the dashboard’s own form. Editing and archiving a coupon are still dashboard-only.
GET /public/app
No scope — an installed app’s own token, nothing else. What an installed app’s own install looks like from the app’s side, including its webhook signing secret. App tokens only — an API key has no install and gets a 401 here.
GET /public/app/settings
No scope — an installed app’s own token, nothing else.
GET /public/app-data
No scope — an installed app’s own token, nothing else.
GET /public/app-data/:resourceType/:resourceId/:key
No scope — an installed app’s own token, nothing else.
PUT /public/app-data/:resourceType/:resourceId/:key
No scope — an installed app’s own token, nothing else.
DELETE /public/app-data/:resourceType/:resourceId/:key
No scope — an installed app’s own token, nothing else.
GET /public/developer/apps
Needs apps.develop.
PUT /public/developer/apps/:listingId/manifest
Needs apps.develop.
POST /public/developer/apps/:listingId/submit
Needs apps.develop.
GET /public/developer/apps/:listingId/logs
Needs apps.develop.
POST /public/developer/apps/:listingId/conformance
Needs apps.develop.
POST /public/developer/test-store
Needs apps.develop.
POST /public/app-data/bulk
No scope — an installed app’s own token, nothing else.
POST /public/app/triggers
No scope — an installed app’s own token, nothing else.
POST /public/app/rotate-secret
No scope — an installed app’s own token, nothing else.
POST /public/hooks
No scope — an installed app’s own token, nothing else.
GET /public/hooks
No scope — an installed app’s own token, nothing else.
DELETE /public/hooks/:id
No scope — an installed app’s own token, nothing else.
GET /public/community/members
Needs community.read.
GET /public/community/members/:accessId
Needs community.read.
GET /public/loyalty/members
Needs loyalty.read.
GET /public/loyalty/members/:accountId
Needs loyalty.read.
POST /public/loyalty/adjust
Needs loyalty.write.
GET /public/wishlists/insight
Needs wishlists.read.
GET /public/courses
Needs courses.read.
GET /public/courses/:courseId/students
Needs courses.read.
POST /public/courses/:courseId/students
Needs courses.write.
GET /public/courses/:courseId/students/:studentId
Needs courses.read.
POST /public/courses/:courseId/students/:studentId/revoke
Needs courses.write.
GET /public/licenses
Needs licenses.read. Issued licences, with the keys masked — revealing one is its own act behind its own step-up grant, and an API key does not carry that authority.
POST /public/licenses/lookup
Needs licenses.read.
GET /public/licenses/:licenseId/activations
Needs licenses.manage.
POST /public/licenses/:licenseId/suspend
Needs licenses.manage.
POST /public/licenses/:licenseId/resume
Needs licenses.manage.
POST /public/licenses/:licenseId/reset-activations
Needs licenses.manage.
POST /public/licenses/:licenseId/activations/:activationId/release
Needs licenses.manage.

The OpenAPI document

Everything above, machine-readable. Generated from the same Zod schemas the routes validate requests against, served unauthenticated, and checked against the live routes by a test that fails in both directions — a route with no entry, and an entry with no route.

Import it

curl https://<api-host>/api/v1/openapi.json -o sellstamp.json

# Postman  → Import → sellstamp.json
# Insomnia → Import → sellstamp.json
# n8n / Zapier / Make → custom connector from an OpenAPI file
  • Why 3.0 and not 3.1

    3.1 is the better specification and 3.0 is the one every importer reads without complaint. This document exists to be imported, so it is the one that gets imported.

  • No separate Postman collection

    A collection file is a second description of the same 50 endpoints, maintained by hand, which is precisely the thing this replaced. Postman imports OpenAPI directly.

  • Not a credential

    The spec names no merchant and no row, so it needs no key — every tool that makes it worth generating fetches it before it has anywhere to put one.

The contract every endpoint keeps

Errors
RFC 7807 application/problem+json, every time: 401 for auth, 403 for scope, 404 for not-found and for anything belonging to another workspace, 409 for conflicts, 410 for expired grants, 422 for validation, 429 for rate limits. A cross-workspace 404 is deliberate — 403 would confirm the thing exists.
Pagination
Keyset, never offset. ?limit= (100 max) and ?cursor=, and the response carries nextCursor and hasMore. Rows land in these tables while you are walking them, and an offset would silently skip one and repeat another; comparing against the last row’s sort key cannot.
Idempotency
Idempotency-Key on the write, 1–255 characters. The key is claimed only once the request is well formed and aimed at a store yours can reach, so a typo never burns it.
Money
Integer minor units and an ISO currency code, always both fields, never a decimal. Totals are never summed across currencies.
Timestamps
ISO-8601, UTC.

Rate limits, and knowing where you stand

Per key, not per address, and separate from the per-IP limits the rest of the API applies. Your plan sets the number: 120 requests a minute on Growth, 600 on Scale, 6,000 on Enterprise — and a key may carry a lower one of its own if you want to throttle a script you do not fully trust. It can never be set above the plan.

  • The headers, in one spelling

    RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset on every response, using the IETF draft names. A 429 carries all three plus Retry-After, in seconds — back off on that number rather than guessing. The older X-RateLimit-* spelling is not emitted.

  • The first request is limited at 120

    Whatever the key's tier. The limiter has to choose a ceiling before any lookup could say whose key this is, which is the right order — spraying invalid keys must be throttled before it costs a query. The real number is learned during authentication and used from the next request on.

  • And a usage meter

    The dashboard charts your workspace's requests per day over thirty days, per key, with the templated routes each one actually calls — which identifies a script faster than its name does. A 403 counts: “which key keeps hitting a scope wall” is one of the questions the page exists to answer. No URL, query string, body or address is stored.

Getting a key

Settings → API keys, in the dashboard. Creating, editing and rotating each need a fresh proof of identity on top of your role — minting a machine credential is not something a step-up for a different purpose should silently authorise. Revoking is deliberately not gated: turning a credential off is the safe direction.

What this does not do

  • No API keys on Trial or Starter. Growth allows five, Scale twenty-five, Enterprise as many as you need — and only live keys occupy a slot.
  • No generated client in another language. The OpenAPI document above makes a Python or Go SDK a real option; codegen quality varies wildly by target, so which ones are worth publishing is its own decision rather than a thing to do because it became possible.
  • No sandbox, no fixture data and no fake payment provider behind the mode flag. Building one is a parallel dataset and a fake provider, not a flag.
  • No GraphQL, no bulk export endpoint, and no way to read a licence key — the reveal is a human act behind a step-up grant.
  • No public endpoint for subscriptions or fulfilment. Those are dashboard routes today; webhooks are how an integration learns they happened.