Everything you need to integrate with the SellStein platform
https://api.sellstein.comv1Stored only in your browser — never sent anywhere.
Learn how to authenticate and make your first API call.
Create Your Account
Create your account and set up your first business
Configure Your Store
Add products, configure payments, and customize your store
Launch & Sell
Launch your store and start accepting orders
# Test your API key
curl -X GET "https://api.sellstein.com/api/v1/products?limit=25" \
-H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"All API requests require a Bearer token sent via the Authorization header.
Include your API key in the Authorization header of every request.
Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWxEvery 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.
Each API key is granted a set of scopes. A key can only call endpoints covered by its scopes.
| Scope | Grants |
|---|---|
| products:read | Read products, discounts. |
| products:write | Create and update products. |
| products:delete | Delete products (distinct from products:write). |
| orders:read | Read orders and the analytics summary. |
| orders:write | Update order, fulfillment, and payment status. |
| customers:read | Read customers and their order history. |
List, retrieve, create, update, and delete products in your store.
/api/v1/productsList products in your store, newest first. Requires the products:read scope. Each product's images, variants, and metadata are returned as parsed JSON.
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | integer | Optional | Page number for pagination. Defaults to 1. |
| limit | integer | Optional | Results per page (max 100). Defaults to 25. |
| status | string | Optional | Filter by status: active, draft, or archived. |
curl -X GET "https://api.sellstein.com/api/v1/products?status=active&limit=25" \
-H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"{
"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
}/api/v1/products/:idRetrieve a single product by its ID. Requires the products:read scope. Returns 404 not_found if the product does not exist in your store.
curl -X GET "https://api.sellstein.com/api/v1/products/abc123" \
-H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"{
"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"
}
}/api/v1/productsCreate 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Required | Product name. |
| slug | string | Optional | URL slug. Auto-generated from name if omitted. |
| description | string | Optional | Product description. |
| price | integer | Optional | Price in minor units (cents). Defaults to 0. |
| compare_at_price | integer | Optional | Original/compare-at price in minor units. |
| currency | string | Optional | ISO currency code. Defaults to "usd". |
| product_type | string | Optional | digital, physical, service, or subscription. Defaults to digital. |
| status | string | Optional | draft, active, or archived. Defaults to draft. |
| images | array | Optional | Array of image URLs. |
| variants | array | object | Optional | Product variants. |
| weight | integer | Optional | Weight (for physical products). |
| requires_shipping | boolean | Optional | Whether the product needs shipping. |
| inventory_count | integer | Optional | Available inventory. |
| inventory_tracking | boolean | Optional | Whether to track inventory. |
| category_id | string | Optional | ID of the category to assign. |
| sort_order | integer | Optional | Display sort order. |
| metadata | object | Optional | Arbitrary key/value metadata. |
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"
}'{
"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"
}
}/api/v1/products/:idUpdate 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Optional | Updated product name. |
| price | integer | Optional | Updated price in minor units (cents). |
| status | string | Optional | draft, active, or archived. |
| inventory_count | integer | Optional | Updated inventory count. |
| description | string | Optional | Updated description. |
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" }'{
"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"
}
}/api/v1/products/:idDelete 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.
curl -X DELETE "https://api.sellstein.com/api/v1/products/nW3pRt8" \
-H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"{
"success": true,
"deleted": "nW3pRt8"
}List and retrieve orders, and update their status.
/api/v1/ordersList orders, newest first. Requires the orders:read scope. Each order's shipping_address and billing_address are returned as parsed JSON.
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | integer | Optional | Page number. Defaults to 1. |
| limit | integer | Optional | Results per page (max 100). Defaults to 25. |
| status | string | Optional | Filter by order status (e.g. confirmed, shipped, refunded). |
curl -X GET "https://api.sellstein.com/api/v1/orders?status=confirmed&limit=25" \
-H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"{
"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
}/api/v1/orders/:idRetrieve 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.
curl -X GET "https://api.sellstein.com/api/v1/orders/ord_Qw7mNp3" \
-H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"{
"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"
}
}/api/v1/orders/:id/statusUpdate 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
| status | string | Optional | pending, confirmed, processing, shipped, delivered, cancelled, or refunded. |
| fulfillment_status | string | Optional | unfulfilled, partial, or fulfilled. |
| payment_status | string | Optional | pending, refunded, or failed. (paid is not accepted.) |
| notes | string | Optional | Internal note on the 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", "fulfillment_status": "fulfilled" }'{
"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"
}
}List and retrieve customers and their order history (read-only).
/api/v1/customersList 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | integer | Optional | Page number. Defaults to 1. |
| limit | integer | Optional | Results per page (max 100). Defaults to 25. |
| search | string | Optional | Match customers by email or name. |
curl -X GET "https://api.sellstein.com/api/v1/customers?search=jane&limit=25" \
-H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"{
"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
}/api/v1/customers/:idRetrieve 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.
curl -X GET "https://api.sellstein.com/api/v1/customers/cust_Rt5mWq9" \
-H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"{
"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"
}
]
}
}List the discount codes configured in your store.
/api/v1/discountsList your store's discount codes, newest first. Requires the products:read scope.
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | integer | Optional | Page number. Defaults to 1. |
| limit | integer | Optional | Results per page (max 100). Defaults to 25. |
curl -X GET "https://api.sellstein.com/api/v1/discounts?limit=25" \
-H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"{
"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
}Get a quick summary of orders, revenue, customers, and products.
/api/v1/analytics/summaryGet 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).
curl -X GET "https://api.sellstein.com/api/v1/analytics/summary" \
-H "Authorization: Bearer sk_2f9a1c3e_8KdLmNpQrStUvWx"{
"data": {
"total_orders": 142,
"paid_orders": 128,
"total_revenue": 425000,
"total_customers": 89,
"total_products": 15,
"active_products": 12
}
}Receive real-time notifications when events happen in your store.
Configure webhook endpoints in your dashboard under Developers → Webhooks.
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.
| Event | Description |
|---|---|
| order.created | A new order was placed. |
| order.updated | An order was updated. |
| order.fulfilled | An order was marked fulfilled. |
| order.refunded | An order was fully or partially refunded. |
| order.cancelled | An order was cancelled. |
| product.created | A new product was created. |
| product.updated | A product was updated. |
| product.deleted | A product was deleted. |
| customer.created | A new customer was added. |
| customer.updated | A customer was updated. |
| checkout.completed | A checkout session completed successfully. |
| subscription.created | A new subscription began. |
| subscription.cancelled | A subscription was cancelled. |
| subscription.renewed | A subscription payment succeeded. |
| payment.received | A payment was received. |
| payment.failed | A payment attempt failed. |
| inventory.low | A product hit its low-inventory threshold. |
| review.created | A customer left a review. |
| shipping.shipped | A shipment was dispatched. |
| shipping.delivered | A shipment was delivered. |
| return.requested | A return was requested. |
| return.completed | A return was completed. |
Verify webhook signatures to ensure requests are from SellStein.
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');
});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.
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.
<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.
Configure the button entirely through HTML attributes. store and product are required; everything else is optional.
| Attribute | Required | Default | Description |
|---|---|---|---|
| store | Required | — | Your public store slug (the same slug as your storefront URL). |
| product | Required | — | Product ID or product slug to sell. |
| mode | Optional | drawer | drawer opens an in-page slide-in checkout; redirect navigates to the hosted checkout page. |
| quantity | Optional | 1 | Initial quantity, 1–999. |
| label | Optional | Buy now | Button text. |
| variant | Optional | — | Variant id/name to preselect (for products with variants). |
| color | Optional | #6366f1 | Button background colour as a #hex value. |
| shape | Optional | rounded | Button shape: rounded, pill, or square. |
| success-url | Optional | — | URL to send the buyer to after a successful purchase. See the limitation note below. |
| cancel-url | Optional | — | URL to return the buyer to if they abandon checkout. See the limitation note below. |
| client-reference-id | Optional | — | Your own reference string (≤200 chars) echoed back in the sellstein:success event. |
<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>The element dispatches bubbling CustomEvents on itself. Listen for them to react to the buyer's journey.
| Event | detail | Description |
|---|---|---|
| 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. |
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);
});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.
| Method | Returns | Description |
|---|---|---|
| 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() | void | Closes the open drawer programmatically. |
| SellStein.version | string | The loaded embed script version. |
// 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);Use the quick-start snippet as-is. Nothing else is required.
<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>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.
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" />
</>
);
}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.
// 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" />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.
{% 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>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.
<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>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.
script-src https://api.sellstein.com
frame-src https://sellstein.com
connect-src https://api.sellstein.comThe 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.
| Symptom | Cause | Fix |
|---|---|---|
| Button never renders | Wrong 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 file | The 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 click | The element has not finished resolving its checkout link. | Confirm the sellstein:ready event fired before the click; check the console for sellstein:unavailable. |
| Product unavailable | No 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. |
Understand API rate limiting and how to handle it.
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.
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 28
X-RateLimit-Reset: 60{
"error": "Rate limit exceeded — 30 requests per minute per API key.",
"code": "rate_limited"
}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');
}Standard error codes and response format.
All errors follow a consistent JSON format.
{
"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.
| Status | Code | Description |
|---|---|---|
| 401 | auth_required | No API key was provided. Set the Authorization: Bearer header. |
| 401 | invalid_key | The API key is unknown, revoked, or inactive. |
| 401 | key_expired | The API key has passed its expiry date. |
| 403 | forbidden | The key lacks the scope for this action. The required scope is in the "required" field. |
| 403 | plan_limit_exceeded | Creating this resource would exceed your plan limit. |
| 404 | not_found | The requested resource does not exist in your store. |
| 400 | validation_error | The request body is missing a required field or contains an invalid value. |
| 429 | rate_limited | Too many requests (limit: 30/min per API key). Honor the Retry-After header and back off. |
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.
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.
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 number | Result | Description |
|---|---|---|
| 4242 4242 4242 4242 | Success | A standard approved Visa payment in the processor sandbox. |
| 4000 0000 0000 0002 | Declined | A 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.
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.
# 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.Official and community SDKs for popular languages.
The official @sellstein/node package wraps the REST API with a typed client and a webhook-signature helper. Install it from npm:
npm install @sellstein/nodeimport { 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',
});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.
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');
});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.
Roadmap — Q3 2026Roadmap — Q3 2026Roadmap — Q4 2026Roadmap — Q4 2026No install neededGenerate todayA 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.
The OpenAPI 3 document describes every endpoint, parameter, response shape, and error code. Point any OpenAPI-aware tool at this URL:
https://api.sellstein.com/api/v1/openapi.jsonIn 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.
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.
| Shopify | SellStein |
|---|---|
| Shopify Buy Button | The <sellstein-buy-button> web component (see the Embeds section). |
| Products, variants, and images | Imported into your catalogue via the dashboard migration tool. |
| Customers | Imported via the dashboard migration tool. |
| Orders & history | Imported via the dashboard migration tool. |
| Online Store / theme | A SellStein storefront — rebuild the storefront, then point your domain at it. |
Import your data
Use the dashboard migration tool to pull products, variants, images, customers, and orders from your Shopify store.
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.
Move your storefront
Rebuild your storefront in SellStein and repoint your custom domain. Your embeds keep working anywhere they are pasted.