API reference

A REST API for one store's catalog, orders and customers, plus signed webhooks for store events. No extra cost, no app approval.

Authentication

Create a key in Admin → Developers, choosing only the permissions it needs. Send it as a bearer token; the key decides which store you're working with.

curl https://206.168.244.254.sslip.io/api/v1/products?limit=10 \
  -H "Authorization: Bearer stly_…"

Scopes: read_products, write_products, read_orders, write_orders, read_customers. Keys are shown once; revoke and replace one if it leaks.

Conventions

  • JSON in and out. Responses wrap results in data.
  • Money is an integer in the store currency's minor unit (cents): 2200 = $22.00. Weights are grams.
  • Lists are newest first. Pass limit (1–250, default 50) and follow next_cursor with ?cursor=… until it's null.
  • Errors: { "error": { "status": 422, "message": "…" } } — 401 bad key, 403 missing scope, 404 not found (or another store's), 409 conflict, 422 invalid input, 429 rate limited.
  • Rate limit: 120 requests per minute per key.

Endpoints

Base URL https://206.168.244.254.sslip.io/api/v1

MethodPathScopeDescription
GET/storeanyThe store this key belongs to, and the key's scopes.
GET/productsread_productsList products with variants. Filters: status, handle, collection_id.
POST/productswrite_productsCreate a product. Body: product fields, variants[], collection_ids[].
GET/products/:idread_productsOne product.
PATCH/products/:idwrite_productsUpdate product fields; collection_ids replaces collections.
DELETE/products/:idwrite_productsDelete a product and its variants.
POST/products/:id/variantswrite_productsAdd a variant.
PATCH/variants/:idwrite_productsUpdate a variant. inventory sets stock; inventory_adjust changes it atomically.
DELETE/variants/:idwrite_productsDelete a variant (not the last one).
GET/collectionsread_productsAll collections with product counts.
GET/ordersread_ordersList orders. Filters: financial_status, fulfillment_status, email, created_at_min.
GET/orders/:idread_ordersOne order with line items.
POST/orders/:id/fulfillwrite_ordersMark shipped. Body: tracking_number, tracking_url, notify_customer (default true).
GET/customersread_customersList customers. Filters: email, accepts_marketing.
GET/customers/:idread_customersOne customer with orders_count and total_spent.

Create a product

curl -X POST https://206.168.244.254.sslip.io/api/v1/products \
  -H "Authorization: Bearer stly_…" -H "Content-Type: application/json" \
  -d '{
    "title": "Trail Socks", "status": "active", "tags": ["apparel"],
    "images": ["https://example.com/socks.jpg"],
    "variants": [
      { "title": "M", "sku": "SOCK-M", "price": 1800, "inventory": 40, "weight_grams": 80 },
      { "title": "L", "sku": "SOCK-L", "price": 1800, "inventory": 25, "weight_grams": 90 }
    ]
  }'

Sync stock from a warehouse

curl -X PATCH https://206.168.244.254.sslip.io/api/v1/variants/VARIANT_ID \
  -H "Authorization: Bearer stly_…" -H "Content-Type: application/json" \
  -d '{ "inventory_adjust": -3 }'   # or { "inventory": 120 } to set it

Webhooks

Add an https endpoint in Admin → Developers and pick events. We POST JSON like this:

{
  "id": "delivery id (same on retries — use it to ignore duplicates)",
  "topic": "orders/paid",
  "store_id": "…",
  "created_at": "2026-10-28T12:00:00.000Z",
  "data": { …the order, product or customer, same shape as the API… }
}

Events: orders/created, orders/paid, orders/fulfilled, orders/refunded, orders/cancelled, products/create, products/update, products/delete, customers/create.

Reply with any 2xx within 10 seconds. Otherwise we retry with backoff (2, 4, 8 … minutes, up to 6 hours apart) for up to 8 attempts; you can also retry from the admin. Headers: X-Storely-Topic, X-Storely-Delivery, X-Storely-Signature.

Verify the signature

X-Storely-Signature: t=<unix seconds>,v1=<hex> where v1 is HMAC-SHA256 of `${t}.${body}` with the endpoint's signing secret.

import crypto from "node:crypto";

// Express-style handler; use the RAW request body, not re-serialized JSON.
export function verify(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // replay guard
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return parts.v1?.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}