DEVELOPER PLATFORM

API Documentation

Everything you need to integrate with the SellStein platform

Base URLhttps://api.sellstein.com
Versionv1
12Endpoints
30/minRate limit
REST · JSONProtocol
6Scopes

Stored only in your browser — never sent anywhere.

Language
Quick reference

Getting Started

Learn how to authenticate and make your first API call.

Quickstart

1

Create Your Account

Create your account and set up your first business

2

Configure Your Store

Add products, configure payments, and customize your store

3

Launch & Sell

Launch your store and start accepting orders

Make your first request
bash
# Test your API key
curl -X GET "https://api.sellstein.com/api/v1/products?limit=25" \
  -H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"

Authentication

All API requests require a Bearer token sent via the Authorization header.

Bearer Token

Include your API key in the Authorization header of every request.

bash
Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx

Every key follows the format sk_<first 8 chars of business id>_<random>. There is a single key type — no separate live/test keys and no sandbox environment. Create and manage keys in the dashboard under Developers → API Keys.

Security Warning

Never expose your API keys in client-side code. Always make API calls from your server.

Scopes

Each API key is granted a set of scopes. A key can only call endpoints covered by its scopes.

ScopeGrants
products:readRead products, discounts.
products:writeCreate and update products.
products:deleteDelete products (distinct from products:write).
orders:readRead orders and the analytics summary.
orders:writeUpdate order, fulfillment, and payment status.
customers:readRead customers and their order history.

Products

List, retrieve, create, update, and delete products in your store.

GET/api/v1/products
products:read

List products in your store, newest first. Requires the products:read scope. Each product's images, variants, and metadata are returned as parsed JSON.

Parameters
ParameterTypeRequiredDescription
pageintegerOptionalPage number for pagination. Defaults to 1.
limitintegerOptionalResults per page (max 100). Defaults to 25.
statusstringOptionalFilter by status: active, draft, or archived.
Request
bash
curl -X GET "https://api.sellstein.com/api/v1/products?status=active&limit=25" \
  -H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"
Response
json
{
  "data": [
    {
      "id": "abc123",
      "name": "Premium License Key",
      "slug": "premium-license-key",
      "price": 2999,
      "currency": "usd",
      "status": "active",
      "product_type": "digital",
      "inventory_count": 142,
      "images": [],
      "variants": null,
      "metadata": null,
      "created_at": "2026-01-15T10:30:00.000Z",
      "updated_at": "2026-01-22T14:12:00.000Z"
    }
  ],
  "page": 1,
  "limit": 25,
  "has_more": false
}
GET/api/v1/products/:id
products:read

Retrieve a single product by its ID. Requires the products:read scope. Returns 404 not_found if the product does not exist in your store.

Request
bash
curl -X GET "https://api.sellstein.com/api/v1/products/abc123" \
  -H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"
Response
json
{
  "data": {
    "id": "abc123",
    "name": "Premium License Key",
    "slug": "premium-license-key",
    "price": 2999,
    "currency": "usd",
    "status": "active",
    "product_type": "digital",
    "inventory_count": 142,
    "images": [],
    "variants": null,
    "metadata": null,
    "created_at": "2026-01-15T10:30:00.000Z",
    "updated_at": "2026-01-22T14:12:00.000Z"
  }
}
POST/api/v1/products
products:write

Create a product. Requires the products:write scope. Prices are integers in minor units (cents): use 2999 for $29.99. Enforces your plan's product limit — returns 403 plan_limit_exceeded if you are over it.

Parameters
ParameterTypeRequiredDescription
namestringRequiredProduct name.
slugstringOptionalURL slug. Auto-generated from name if omitted.
descriptionstringOptionalProduct description.
priceintegerOptionalPrice in minor units (cents). Defaults to 0.
compare_at_priceintegerOptionalOriginal/compare-at price in minor units.
currencystringOptionalISO currency code. Defaults to "usd".
product_typestringOptionaldigital, physical, service, or subscription. Defaults to digital.
statusstringOptionaldraft, active, or archived. Defaults to draft.
imagesarrayOptionalArray of image URLs.
variantsarray | objectOptionalProduct variants.
weightintegerOptionalWeight (for physical products).
requires_shippingbooleanOptionalWhether the product needs shipping.
inventory_countintegerOptionalAvailable inventory.
inventory_trackingbooleanOptionalWhether to track inventory.
category_idstringOptionalID of the category to assign.
sort_orderintegerOptionalDisplay sort order.
metadataobjectOptionalArbitrary key/value metadata.
Request
bash
curl -X POST "https://api.sellstein.com/api/v1/products" \
  -H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pro Toolkit License",
    "price": 4999,
    "currency": "usd",
    "product_type": "digital",
    "status": "active"
  }'
Response
json
{
  "data": {
    "id": "nW3pRt8",
    "name": "Pro Toolkit License",
    "slug": "pro-toolkit-license",
    "price": 4999,
    "currency": "usd",
    "product_type": "digital",
    "status": "active",
    "created_at": "2026-03-03T10:15:00.000Z",
    "updated_at": "2026-03-03T10:15:00.000Z"
  }
}
PUT/api/v1/products/:id
products:write

Update a product. Requires the products:write scope. Partial update — only the fields you send are changed; everything else keeps its current value. Accepts the same fields as the create endpoint.

Parameters
ParameterTypeRequiredDescription
namestringOptionalUpdated product name.
priceintegerOptionalUpdated price in minor units (cents).
statusstringOptionaldraft, active, or archived.
inventory_countintegerOptionalUpdated inventory count.
descriptionstringOptionalUpdated description.
Request
bash
curl -X PUT "https://api.sellstein.com/api/v1/products/nW3pRt8" \
  -H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx" \
  -H "Content-Type: application/json" \
  -d '{ "price": 3999, "status": "active" }'
Response
json
{
  "data": {
    "id": "nW3pRt8",
    "name": "Pro Toolkit License",
    "slug": "pro-toolkit-license",
    "price": 3999,
    "currency": "usd",
    "product_type": "digital",
    "status": "active",
    "updated_at": "2026-03-03T11:45:00.000Z"
  }
}
DELETE/api/v1/products/:id
products:delete

Delete a product. Requires the dedicated products:delete scope — a key that only has products:write gets 403 forbidden. Orphaned references (discount codes, subscription plans, reviews) are cleaned up automatically.

Request
bash
curl -X DELETE "https://api.sellstein.com/api/v1/products/nW3pRt8" \
  -H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"
Response
json
{
  "success": true,
  "deleted": "nW3pRt8"
}

Orders

List and retrieve orders, and update their status.

GET/api/v1/orders
orders:read

List orders, newest first. Requires the orders:read scope. Each order's shipping_address and billing_address are returned as parsed JSON.

Parameters
ParameterTypeRequiredDescription
pageintegerOptionalPage number. Defaults to 1.
limitintegerOptionalResults per page (max 100). Defaults to 25.
statusstringOptionalFilter by order status (e.g. confirmed, shipped, refunded).
Request
bash
curl -X GET "https://api.sellstein.com/api/v1/orders?status=confirmed&limit=25" \
  -H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"
Response
json
{
  "data": [
    {
      "id": "ord_Qw7mNp3",
      "order_number": 1042,
      "status": "confirmed",
      "fulfillment_status": "unfulfilled",
      "payment_status": "paid",
      "customer_email": "jane@example.com",
      "total": 4999,
      "currency": "usd",
      "shipping_address": null,
      "billing_address": null,
      "created_at": "2026-02-28T16:30:00.000Z"
    }
  ],
  "page": 1,
  "limit": 25,
  "has_more": true
}
GET/api/v1/orders/:id
orders:read

Retrieve a single order by its ID, including its line items. Requires the orders:read scope. Returns 404 not_found if the order does not exist in your store.

Request
bash
curl -X GET "https://api.sellstein.com/api/v1/orders/ord_Qw7mNp3" \
  -H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"
Response
json
{
  "data": {
    "id": "ord_Qw7mNp3",
    "order_number": 1042,
    "status": "confirmed",
    "fulfillment_status": "unfulfilled",
    "payment_status": "paid",
    "total": 4999,
    "currency": "usd",
    "shipping_address": null,
    "billing_address": null,
    "items": [
      {
        "product_id": "abc123",
        "quantity": 1,
        "variant_info": null
      }
    ],
    "created_at": "2026-02-28T16:30:00.000Z"
  }
}
PUT/api/v1/orders/:id/status
orders:write

Update an order's status, fulfillment, payment status, or notes. Requires the orders:write scope. Send at least one field; an invalid value returns 400 validation_error. Note: payment_status cannot be set to 'paid' via the API — that transition only happens through verified payment webhooks.

Parameters
ParameterTypeRequiredDescription
statusstringOptionalpending, confirmed, processing, shipped, delivered, cancelled, or refunded.
fulfillment_statusstringOptionalunfulfilled, partial, or fulfilled.
payment_statusstringOptionalpending, refunded, or failed. (paid is not accepted.)
notesstringOptionalInternal note on the order.
Request
bash
curl -X PUT "https://api.sellstein.com/api/v1/orders/ord_Qw7mNp3/status" \
  -H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx" \
  -H "Content-Type: application/json" \
  -d '{ "status": "shipped", "fulfillment_status": "fulfilled" }'
Response
json
{
  "data": {
    "id": "ord_Qw7mNp3",
    "order_number": 1042,
    "status": "shipped",
    "fulfillment_status": "fulfilled",
    "payment_status": "paid",
    "shipping_address": null,
    "billing_address": null,
    "updated_at": "2026-03-01T09:15:00.000Z"
  }
}

Customers

List and retrieve customers and their order history (read-only).

GET/api/v1/customers
customers:read

List customers, newest first. Requires the customers:read scope. The customer endpoints are read-only — there is no public API to create, update, or delete customers.

Parameters
ParameterTypeRequiredDescription
pageintegerOptionalPage number. Defaults to 1.
limitintegerOptionalResults per page (max 100). Defaults to 25.
searchstringOptionalMatch customers by email or name.
Request
bash
curl -X GET "https://api.sellstein.com/api/v1/customers?search=jane&limit=25" \
  -H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"
Response
json
{
  "data": [
    {
      "id": "cust_Rt5mWq9",
      "email": "jane@example.com",
      "name": "Jane Doe",
      "created_at": "2025-06-12T09:55:00.000Z"
    }
  ],
  "page": 1,
  "limit": 25,
  "has_more": false
}
GET/api/v1/customers/:id
customers:read

Retrieve a single customer by ID, including up to 10 of their most recent orders. Requires the customers:read scope. Returns 404 not_found if the customer does not exist in your store.

Request
bash
curl -X GET "https://api.sellstein.com/api/v1/customers/cust_Rt5mWq9" \
  -H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"
Response
json
{
  "data": {
    "id": "cust_Rt5mWq9",
    "email": "jane@example.com",
    "name": "Jane Doe",
    "created_at": "2025-06-12T09:55:00.000Z",
    "recent_orders": [
      {
        "id": "ord_Qw7mNp3",
        "order_number": 1042,
        "status": "confirmed",
        "total": 4999,
        "currency": "usd",
        "created_at": "2026-02-28T16:30:00.000Z"
      }
    ]
  }
}

Discounts

List the discount codes configured in your store.

GET/api/v1/discounts
products:read

List your store's discount codes, newest first. Requires the products:read scope.

Parameters
ParameterTypeRequiredDescription
pageintegerOptionalPage number. Defaults to 1.
limitintegerOptionalResults per page (max 100). Defaults to 25.
Request
bash
curl -X GET "https://api.sellstein.com/api/v1/discounts?limit=25" \
  -H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"
Response
json
{
  "data": [
    {
      "id": "disc_8Hb2Lp",
      "code": "WELCOME10",
      "type": "percentage",
      "value": 10,
      "status": "active",
      "created_at": "2026-01-05T12:00:00.000Z"
    }
  ],
  "page": 1,
  "limit": 25,
  "has_more": false
}

Analytics

Get a quick summary of orders, revenue, customers, and products.

GET/api/v1/analytics/summary
orders:read

Get a quick overview of your store: order, customer, and product counts plus total revenue. Requires the orders:read scope. total_revenue is the sum of paid orders' totals, in minor units (cents).

Request
bash
curl -X GET "https://api.sellstein.com/api/v1/analytics/summary" \
  -H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"
Response
json
{
  "data": {
    "total_orders": 142,
    "paid_orders": 128,
    "total_revenue": 425000,
    "total_customers": 89,
    "total_products": 15,
    "active_products": 12
  }
}

Webhooks

Receive real-time notifications when events happen in your store.

Webhook Setup

Configure webhook endpoints in your dashboard under Developers → Webhooks.

bash
POST https://your-server.com/webhooks/sellstein
Content-Type: application/json
X-Sellstein-Signature: sha256=5d7861...

{
  "id": "evt_Mn4pQr8",
  "event": "order.created",
  "created_at": "2026-03-03T10:15:00.000Z",
  "data": { ... }
}

Each endpoint is issued a signing secret in the format whsec_.... Every delivery carries an X-Sellstein-Signature header of the form sha256=<hex>, an HMAC-SHA256 of the raw request body keyed with that secret.

Webhook Events

EventDescription
order.createdA new order was placed.
order.updatedAn order was updated.
order.fulfilledAn order was marked fulfilled.
order.refundedAn order was fully or partially refunded.
order.cancelledAn order was cancelled.
product.createdA new product was created.
product.updatedA product was updated.
product.deletedA product was deleted.
customer.createdA new customer was added.
customer.updatedA customer was updated.
checkout.completedA checkout session completed successfully.
subscription.createdA new subscription began.
subscription.cancelledA subscription was cancelled.
subscription.renewedA subscription payment succeeded.
payment.receivedA payment was received.
payment.failedA payment attempt failed.
inventory.lowA product hit its low-inventory threshold.
review.createdA customer left a review.
shipping.shippedA shipment was dispatched.
shipping.deliveredA shipment was delivered.
return.requestedA return was requested.
return.completedA return was completed.

Signature Verification

Verify webhook signatures to ensure requests are from SellStein.

javascript
import crypto from 'crypto';

function verifyWebhookSignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');

  const sig = signature.replace('sha256=', '');

  return crypto.timingSafeEqual(
    Buffer.from(sig, 'hex'),
    Buffer.from(expected, 'hex')
  );
}

// Express example
app.post('/webhooks/sellstein', (req, res) => {
  const signature = req.headers['x-sellstein-signature'];
  const isValid = verifyWebhookSignature(
    JSON.stringify(req.body),
    signature,
    process.env.WEBHOOK_SECRET
  );

  if (!isValid) return res.status(401).send('Invalid signature');

  const { event, data } = req.body;
  switch (event) {
    case 'order.created':
      handleNewOrder(data);
      break;
    case 'order.refunded':
      handleRefund(data);
      break;
  }

  res.status(200).send('OK');
});

Embeds & Buy Button

Drop a fully-hosted checkout into any website with one script tag and one custom element. The <sellstein-buy-button> web component opens a slide-in checkout drawer (or redirects to a hosted page) — no backend, no API keys, no PCI scope on your side. It works in plain HTML, React, Vue, Shopify, WordPress, Webflow, or anything that renders HTML.

Quick start

Paste these two lines anywhere in your page. Replace your-store-slug with your public store slug and prod_id_or_slug with a product ID or slug. That is the entire integration — clicking the button opens the checkout drawer.

html
<script async src="https://api.sellstein.com/embed/buy-button.js"></script>
<sellstein-buy-button store="your-store-slug" product="prod_id_or_slug"></sellstein-buy-button>

The loader is also available at the versioned alias https://api.sellstein.com/embed/v1/buy-button.js. Both load the same web component. The script is async-safe and registers the custom element as soon as it parses; you can place the element before or after the script tag.

Attributes

Configure the button entirely through HTML attributes. store and product are required; everything else is optional.

AttributeRequiredDefaultDescription
storeRequiredYour public store slug (the same slug as your storefront URL).
productRequiredProduct ID or product slug to sell.
modeOptionaldrawerdrawer opens an in-page slide-in checkout; redirect navigates to the hosted checkout page.
quantityOptional1Initial quantity, 1–999.
labelOptionalBuy nowButton text.
variantOptionalVariant id/name to preselect (for products with variants).
colorOptional#6366f1Button background colour as a #hex value.
shapeOptionalroundedButton shape: rounded, pill, or square.
success-urlOptionalURL to send the buyer to after a successful purchase. See the limitation note below.
cancel-urlOptionalURL to return the buyer to if they abandon checkout. See the limitation note below.
client-reference-idOptionalYour own reference string (≤200 chars) echoed back in the sellstein:success event.
html
<sellstein-buy-button
  store="acme-store"
  product="pro-toolkit-license"
  mode="drawer"
  quantity="1"
  label="Get the toolkit"
  color="#6366f1"
  shape="pill"
  client-reference-id="campaign-spring-2026"
></sellstein-buy-button>

Events

The element dispatches bubbling CustomEvents on itself. Listen for them to react to the buyer's journey.

EventdetailDescription
sellstein:ready{ store, product }The button mounted and resolved a valid, active checkout link.
sellstein:success{ orderId, clientReferenceId }The purchase completed. clientReferenceId echoes the attribute you set.
sellstein:close{}The buyer closed the drawer without completing checkout.
sellstein:unavailable{ store, product }No active checkout link exists for this store/product, so the button cannot open.
javascript
const btn = document.querySelector('sellstein-buy-button');

btn.addEventListener('sellstein:ready', (e) => {
  console.log('ready', e.detail.store, e.detail.product);
});

btn.addEventListener('sellstein:success', (e) => {
  console.log('order', e.detail.orderId, e.detail.clientReferenceId);
  // fire your analytics / thank-you UI here
});

btn.addEventListener('sellstein:close', () => {
  console.log('checkout closed');
});

btn.addEventListener('sellstein:unavailable', (e) => {
  console.warn('no active checkout link for', e.detail.product);
});

Imperative API

Once the script loads it exposes a global window.SellStein. Use it to open checkout from your own buttons, links, or app logic — no custom element required.

MethodReturnsDescription
SellStein.openCheckout(opts)Promise<boolean>Opens checkout for { store, product, quantity?, variant?, mode?, clientReferenceId?, successUrl?, cancelUrl? }. Resolves true once a checkout opened, false if no active link was found.
SellStein.closeCheckout()voidCloses the open drawer programmatically.
SellStein.versionstringThe loaded embed script version.
javascript
// Open checkout from any element
document.getElementById('buy').addEventListener('click', async () => {
  const opened = await window.SellStein.openCheckout({
    store: 'acme-store',
    product: 'pro-toolkit-license',
    quantity: 2,
    mode: 'drawer',
    clientReferenceId: 'campaign-spring-2026',
  });
  if (!opened) {
    // no active checkout link — fall back to your storefront
  }
});

// Close it programmatically
window.SellStein.closeCheckout();

// Read the loaded version
console.log(window.SellStein.version);

Framework guides

HTML

Use the quick-start snippet as-is. Nothing else is required.

html
<script async src="https://api.sellstein.com/embed/buy-button.js"></script>
<sellstein-buy-button store="acme-store" product="pro-toolkit-license"></sellstein-buy-button>
React / Next.js

Render the custom element directly — React passes string attributes straight through to DOM elements with hyphenated names. Load the loader once (e.g. with Next.js <Script>, or a <script> in your root layout / index.html). In TypeScript, add the element to JSX intrinsics so the compiler accepts it.

javascript
import Script from 'next/script';

// One-time: declare the element for TypeScript (e.g. in a .d.ts)
declare global {
  namespace JSX {
    interface IntrinsicElements {
      'sellstein-buy-button': React.DetailedHTMLProps<
        React.HTMLAttributes<HTMLElement>,
        HTMLElement
      > & { store: string; product: string; mode?: string; color?: string };
    }
  }
}

export default function BuyPage() {
  return (
    <>
      <Script src="https://api.sellstein.com/embed/buy-button.js" strategy="afterInteractive" />
      <sellstein-buy-button store="acme-store" product="pro-toolkit-license" />
    </>
  );
}
Vue

Tell Vue the tag is a custom element so it does not try to resolve it as a component, then use it in any template. Load the loader script in index.html or via a mounted hook.

javascript
// main.js / main.ts
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);
app.config.compilerOptions.isCustomElement = (tag) =>
  tag === 'sellstein-buy-button';
app.mount('#app');

// In any template:
// <sellstein-buy-button store="acme-store" product="pro-toolkit-license" />
Shopify theme

Paste the loader and element into any section, snippet, or a Custom Liquid / Custom HTML block in the theme editor. It runs alongside Shopify's own scripts.

html
{% comment %} sections/sellstein-buy.liquid {% endcomment %}
<script async src="https://api.sellstein.com/embed/buy-button.js"></script>
<sellstein-buy-button store="acme-store" product="pro-toolkit-license"></sellstein-buy-button>
WordPress / Webflow

Add a Custom HTML block (WordPress Gutenberg "Custom HTML" block, or a Webflow "Embed" element) and paste the same two lines. For WordPress, you can also drop the script into the theme header so the element works anywhere on the site.

html
<script async src="https://api.sellstein.com/embed/buy-button.js"></script>
<sellstein-buy-button store="acme-store" product="pro-toolkit-license"></sellstein-buy-button>

Content Security Policy

If your site sends a Content-Security-Policy header, allow these origins so the loader, the checkout iframe, and the checkout network calls are not blocked. Merge them into your existing directives.

bash
script-src  https://api.sellstein.com
frame-src   https://sellstein.com
connect-src https://api.sellstein.com

Styling

The button renders inside a Shadow DOM, so your site's CSS cannot bleed in and the button always looks correct regardless of your stylesheet. To restyle it, use the color and shape attributes rather than CSS overrides.

Current limitation: success-url / cancel-url

client-reference-id is fully supported — it is echoed back in the sellstein:success event. The success-url and cancel-url attributes are accepted today, but redirect behaviour currently follows the destination configured on the merchant's checkout-link settings, not these attributes. Server-side honoring of the per-embed URLs is on the roadmap. Until then, set your post-purchase URLs in the dashboard checkout-link settings, and use the sellstein:success event to drive any in-page redirect you need.

Troubleshooting

SymptomCauseFix
Button never rendersWrong store slug, product not active, or your CSP blocks the script.Verify the slug matches your storefront, set the product to active, and add the CSP origins above.
Nothing loads on a local fileThe page is served over file:// — web components and fetch are restricted there.Serve over http:// or https:// (e.g. a local dev server).
Drawer never opens on clickThe element has not finished resolving its checkout link.Confirm the sellstein:ready event fired before the click; check the console for sellstein:unavailable.
Product unavailableNo active checkout link exists for that store/product.Create or activate a checkout link for the product, then reload — listen for sellstein:unavailable to detect this in code.

Rate Limits

Understand API rate limiting and how to handle it.

30
30 requests per minute per key
Per key
Rate Limit Scope
60s
Rate Limit Window

Handling Rate Limits

Every endpoint under /api/v1/* allows 30 requests per minute per API key. Every response carries the headers below so you can track your budget. When you exceed it, the request is rejected with HTTP 429 (plus a Retry-After header). Back off and retry with exponential backoff.

bash
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 28
X-RateLimit-Reset: 60
json
{
  "error": "Rate limit exceeded — 30 requests per minute per API key.",
  "code": "rate_limited"
}
javascript
async function fetchWithRetry(url, options, retries = 3) {
  for (let i = 0; i < retries; i++) {
    const response = await fetch(url, options);

    if (response.status === 429) {
      const waitMs = 1000 * Math.pow(2, i); // Exponential backoff
      await new Promise((r) => setTimeout(r, waitMs));
      continue;
    }

    return response;
  }

  throw new Error('Max retries exceeded');
}

Errors

Standard error codes and response format.

Error Response Format

All errors follow a consistent JSON format.

json
{
  "error": "Insufficient permissions",
  "code": "forbidden",
  "required": "products:delete"
}

Errors are a flat object with a human-readable error message and a machine-readable lowercase code. A 403 forbidden response also includes required — the scope the key is missing.

StatusCodeDescription
401auth_requiredNo API key was provided. Set the Authorization: Bearer header.
401invalid_keyThe API key is unknown, revoked, or inactive.
401key_expiredThe API key has passed its expiry date.
403forbiddenThe key lacks the scope for this action. The required scope is in the "required" field.
403plan_limit_exceededCreating this resource would exceed your plan limit.
404not_foundThe requested resource does not exist in your store.
400validation_errorThe request body is missing a required field or contains an invalid value.
429rate_limitedToo many requests (limit: 30/min per API key). Honor the Retry-After header and back off.

Testing & Sandbox

How to test SellStein safely before you flip to real money. There is one important thing to know up front: SellStein issues a single API-key type — there are no separate live and test keys today, and no isolated test-mode environment. Sandbox testing happens at the payment-processor layer, using the connected processor's test cards.

Test vs live keys

A SellStein API key (sk_...) is a single key type — it is neither "live" nor "test", and there is no separate sandbox API. Calls made with your key act on your real store data. To experiment without affecting your production catalogue, create a separate store (or a clearly-marked draft product set) and a dedicated API key scoped to it. The embed Buy Button is even simpler: it carries no key at all — it only references a public store slug and product — so embedding it on a staging page is always safe.

Test card numbers

Card behaviour comes from your connected payment processor's sandbox, not from SellStein. Put the processor into its sandbox/test mode first, then use its standard test cards. The most widely-supported values are below; use any future expiry date, any 3-digit CVC, and any postal code.

Card numberResultDescription
4242 4242 4242 4242SuccessA standard approved Visa payment in the processor sandbox.
4000 0000 0000 0002DeclinedA generic card decline — use it to test your failure handling.

These numbers are the ubiquitous test cards recognized by most sandboxes. The exact set of accepted cards and decline codes depends on the processor connected to your store — check that processor's testing docs for its full list.

Testing a webhook

Point a webhook endpoint at a tunnel to your local machine (for example with a tool that exposes localhost over HTTPS), then trigger a real event in a test store — place a sandbox order, update a product, or change an order's status via the API. Verify every delivery's signature before trusting the body, exactly as shown in the Webhooks section. To prove your verifier rejects forgeries, send a request with a deliberately wrong X-Sellstein-Signature and confirm your handler returns 401.

bash
# 1. Expose your local server over HTTPS (any tunnel works)
#    -> https://your-tunnel.example/webhooks/sellstein

# 2. Add that URL as a webhook endpoint in the dashboard.

# 3. Trigger an event from your test store, e.g. update an order:
curl -X PUT "https://api.sellstein.com/api/v1/orders/ord_Qw7mNp3/status" \
  -H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx" \
  -H "Content-Type: application/json" \
  -d '{ "status": "shipped" }'

# 4. Confirm your endpoint received order.updated and the signature verified.

Going-live checklist

  • Switch your connected payment processor out of sandbox/test mode into live mode.
  • Create a fresh, production-scoped API key with only the scopes your integration actually uses.
  • Store keys and webhook signing secrets in environment variables — never commit them.
  • Verify every webhook signature with a constant-time comparison and return 401 on mismatch.
  • Point embeds at your live store slug and active products; confirm the sellstein:ready event fires.
  • Add the CSP origins from the Embeds section to any site that loads the Buy Button.
  • Handle 429 (rate limit) with exponential backoff, and surface 4xx errors to the buyer gracefully.
  • Run one real low-value purchase end-to-end before announcing, then refund it.

SDKs & Libraries

Official and community SDKs for popular languages.

{ }

Node.js / TypeScript

v0.1.0

The official @sellstein/node package wraps the REST API with a typed client and a webhook-signature helper. Install it from npm:

bash
npm install @sellstein/node

Quickstart

javascript
import { SellStein } from '@sellstein/node';

const client = new SellStein({ apiKey: 'sk_2f9a1c3e_8KdLmNpQrStUvWx' });

const { data: products } = await client.products.list({ status: 'active', limit: 25 });

const { data: product } = await client.products.create({
  name: 'Pro Toolkit License',
  price: 4999, // minor units — $49.99
  currency: 'usd',
  product_type: 'digital',
  status: 'active',
});

Verifying webhooks with the SDK

The package exports verifyWebhookSignature so you do not have to hand-roll the HMAC check. Pass the raw request body, the X-Sellstein-Signature header, and your endpoint's signing secret.

javascript
import { verifyWebhookSignature } from '@sellstein/node';

app.post('/webhooks/sellstein', (req, res) => {
  const ok = verifyWebhookSignature(
    req.rawBody,                          // the raw, unparsed body
    req.headers['x-sellstein-signature'],
    process.env.SELLSTEIN_WEBHOOK_SECRET,
  );
  if (!ok) return res.status(401).send('Invalid signature');

  const { event, data } = JSON.parse(req.rawBody);
  if (event === 'order.created') handleNewOrder(data);

  res.status(200).send('OK');
});

Other languages

Node.js is the first official SDK. The rest of the lineup is on the roadmap — until then, every endpoint is a plain REST call you can hit from any HTTP client (see the cURL and Python samples throughout these docs), and the OpenAPI spec below can generate a client for most languages.

>_

Python

Roadmap — Q3 2026
<?

PHP

Roadmap — Q3 2026
//

Go

Roadmap — Q4 2026
#!

Ruby

Roadmap — Q4 2026
$

cURL

No install needed
{ }

OpenAPI-generated

Generate today

OpenAPI & Postman

A machine-readable spec for the entire REST API. Use it to generate typed clients, import the API into Postman or Insomnia, or drive your own tooling.

Machine-readable spec

The OpenAPI 3 document describes every endpoint, parameter, response shape, and error code. Point any OpenAPI-aware tool at this URL:

bash
https://api.sellstein.com/api/v1/openapi.json

In Postman, choose Import → Link and paste the spec URL above to generate a ready-to-run collection. The same URL works with client generators such as openapi-generator.

Migrate from Shopify

Moving from Shopify to SellStein is mostly a mapping exercise: your Buy Buttons, your catalogue, and your storefront each have a direct equivalent. Here is the short version.

What maps to what

ShopifySellStein
Shopify Buy ButtonThe <sellstein-buy-button> web component (see the Embeds section).
Products, variants, and imagesImported into your catalogue via the dashboard migration tool.
CustomersImported via the dashboard migration tool.
Orders & historyImported via the dashboard migration tool.
Online Store / themeA SellStein storefront — rebuild the storefront, then point your domain at it.

Steps

1

Import your data

Use the dashboard migration tool to pull products, variants, images, customers, and orders from your Shopify store.

2

Replace Buy Buttons

Swap each Shopify Buy Button for a <sellstein-buy-button> element pointing at the matching product. See the Embeds & Buy Button section for the exact snippet.

3

Move your storefront

Rebuild your storefront in SellStein and repoint your custom domain. Your embeds keep working anywhere they are pasted.

Need Help?

Our developer support team is here to help you integrate.