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 follownext_cursorwith?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
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /store | any | The store this key belongs to, and the key's scopes. |
| GET | /products | read_products | List products with variants. Filters: status, handle, collection_id. |
| POST | /products | write_products | Create a product. Body: product fields, variants[], collection_ids[]. |
| GET | /products/:id | read_products | One product. |
| PATCH | /products/:id | write_products | Update product fields; collection_ids replaces collections. |
| DELETE | /products/:id | write_products | Delete a product and its variants. |
| POST | /products/:id/variants | write_products | Add a variant. |
| PATCH | /variants/:id | write_products | Update a variant. inventory sets stock; inventory_adjust changes it atomically. |
| DELETE | /variants/:id | write_products | Delete a variant (not the last one). |
| GET | /collections | read_products | All collections with product counts. |
| GET | /orders | read_orders | List orders. Filters: financial_status, fulfillment_status, email, created_at_min. |
| GET | /orders/:id | read_orders | One order with line items. |
| POST | /orders/:id/fulfill | write_orders | Mark shipped. Body: tracking_number, tracking_url, notify_customer (default true). |
| GET | /customers | read_customers | List customers. Filters: email, accepts_marketing. |
| GET | /customers/:id | read_customers | One 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 itWebhooks
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));
}