Docs
Owner API
Operate your store day-to-day from your own platform — orders, fulfillment, financials, payouts, and gift cards — without opening the dashboard. REST + JSON, and the same key works as an MCP connector for AI agents.
Authentication
Create a key in your store console under Settings → API keys. Keys are scoped to one store and to the permissions you pick; the secret is shown once. Send it as a Bearer token.
curl https://app.threadzshops.com/api/v1/store \ -H "Authorization: Bearer ts_pat_..."
Scopes: store:read · orders:read · financials:read · gift_cards:read · gift_cards:write · webhooks:manage. Missing scope → 403 missing_scope. Rate limits: 120 reads/min, 20 writes/min per key (429 + Retry-After). All money values are integer cents, USD.
Store
GET /api/v1/store
→ { "name": "Radicl", "slug": "radicl", "status": "active",
"markup_percent": 45, "live_url": "https://app.threadzshops.com/radicl",
"subscription": { "live": false, "comped": true }, ... }Orders + fulfillment
GET /api/v1/orders?status=paid&page=0&per_page=25
→ { "orders": [ { "order_number": "RAD-1001", "status": "paid",
"customer": { "name": "...", "email": "..." },
"totals": { "subtotal_cents": 6399, "total_cents": 6399, ... },
"units": 1, "has_tracking": false } ],
"page": 0, "per_page": 25, "has_more": false }
GET /api/v1/orders/RAD-1001
→ { ..., "items": [ { "product_title": "...", "size": "M", "qty": 1,
"unit_price_cents": 6399 } ],
"fulfillment": {
"pushed_to_warehouse_at": "...", "external_id": "...",
"tracking": { "carrier": "...", "number": "...", "url": "..." },
"timeline": { "paid_at": "...", "fulfilling_at": null,
"shipped_at": null, "delivered_at": null } },
"emails": [ { "template": "order-confirmation", "status": "sent" } ] }Financials + payouts
GET /api/v1/financials/summary?window=30
→ { "grossSalesCents": ..., "netSalesCents": ...,
"liftThreadzCogsCents": ..., "ownerGrossProfitCents": ...,
"stripeFeesCents": ..., "refundsCents": ...,
"ownerNetPayoutCents": ..., "orderCount": ..., "aovCents": ...,
"purchases": [ per-order rows ] }
GET /api/v1/payouts
→ { "connect_status": "none" | "pending" | "active",
"pending_cents": ..., "paid_to_date_cents": ...,
"statements": [ { "period_start": "...", "net_cents": ...,
"status": "pending" | "paid" } ] }Gift cards
Codes come back masked on reads; the full code is returned exactly once at creation. Send an Idempotency-Key header on creates so a retried request can never double-issue.
POST /api/v1/gift-cards
-H "Idempotency-Key: <uuid>"
{ "amount_cents": 2500, "issued_to_email": "vip@example.com" }
→ 201 { "gift_card": { "id": "...", "code": "GC-XXXX-XXXX",
"balance_cents": 2500, "status": "active" } }
GET /api/v1/gift-cards?status=active
→ { "gift_cards": [ { "code_masked": "••••••••XXXX",
"balance_cents": 2500, "status": "active",
"redemptions": [ { "amount_cents": ..., "order_number": "..." } ] } ] }
POST /api/v1/gift-cards/{id}/void → { "ok": true, "status": "voided" }Webhooks
Get pushed instead of polling. Events: order.paid, order.shipped, order.refunded, gift_card.redeemed, ping. HTTPS endpoints only, max 5 per store. Failed deliveries retry on a backoff (1m → 5m → 30m → 2h → 12h, then dead).
POST /api/v1/webhooks
{ "url": "https://your.app/threadz-hook",
"events": ["order.paid", "order.shipped"] }
→ 201 { "webhook": { "id": "..." }, "secret": "whsec_ts_..." } // shown once
POST /api/v1/webhooks/{id}/test → sends a signed "ping" now
GET /api/v1/webhooks → endpoints + recent delivery stats
DELETE /api/v1/webhooks/{id} → deactivateEvery delivery is signed. Header X-Threadz-Signature: t=<unix>,v1=<hex> where v1 = HMAC-SHA256(secret, `${t}.${rawBody}`):
import crypto from "node:crypto";
function verify(header, rawBody, secret) {
const t = header.match(/t=(\d+)/)?.[1];
const v1 = header.match(/v1=([a-f0-9]+)/)?.[1];
const expect = crypto.createHmac("sha256", secret)
.update(`${t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expect));
}MCP (AI agents)
Point any MCP-capable host (Claude, Cursor, your own agent) at https://app.threadzshops.com/api/owner/mcp with the same Bearer key. Tools mirror the REST surface: store_get, orders_list, orders_get, financials_summary, payouts_list, gift_cards_list, gift_cards_create, gift_cards_void, webhooks_list. Scopes are enforced identically — a read-only key gets read-only tools results.
Recipes
The three integrations most owners build first, end to end.
1 — A revenue tile in your dashboard
One call on page load (or on a 5-minute cache). Needs financials:read.
const r = await fetch("https://app.threadzshops.com/api/v1/financials/summary?window=30", {
headers: { Authorization: `Bearer ${process.env.THREADZ_KEY}` },
}).then((x) => x.json());
render({
revenue: usd(r.netSalesCents), // what customers paid
profit: usd(r.ownerGrossProfitCents),// your markup share
payout: usd(r.ownerNetPayoutCents), // what lands in your bank
orders: r.orderCount,
aov: usd(r.aovCents),
});
// r.estimated === true until real Stripe fees settle — label it "est."2 — Live order status in your app (no polling)
Register a webhook once, then update rows as events arrive. Needs webhooks:manage to set up and orders:read for the detail fetch.
// one-time setup
await fetch("https://app.threadzshops.com/api/v1/webhooks", {
method: "POST",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
url: "https://your.app/api/threadz-hook",
events: ["order.paid", "order.shipped", "order.refunded"],
}),
}); // ← save the returned secret
// your receiver
export async function POST(req) {
const raw = await req.text();
if (!verify(req.headers.get("x-threadz-signature"), raw, SECRET)) return new Response(null, { status: 401 });
const { event, data } = JSON.parse(raw);
if (event === "order.paid") await db.orders.upsert({ n: data.order_number, status: "paid" });
if (event === "order.shipped") await db.orders.upsert({ n: data.order_number, status: "shipped", tracking: data.tracking });
// fetch full detail when you need line items:
// GET https://app.threadzshops.com/api/v1/orders/${data.order_number}
return new Response("ok");
}3 — Issue store credit from your own tools
A support agent clicks "give $25 credit" in YOUR admin — your backend issues a real, redeemable gift card. Needs gift_cards:write. Always send an Idempotency-Key.
const { gift_card } = await fetch("https://app.threadzshops.com/api/v1/gift-cards", {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(), // retry-safe: same key = same card
},
body: JSON.stringify({ amount_cents: 2500, issued_to_email: customer.email }),
}).then((x) => x.json());
emailCustomer(customer, `Here's $25 on us: ${gift_card.code}`);
// leaked or sent by mistake? POST /api/v1/gift-cards/${gift_card.id}/void
// you'll also get a gift_card.redeemed webhook when they use itErrors
401 unauthorized / invalid_key — missing, unknown, revoked, or expired key
403 not_a_store_key — account key used on the Owner API
403 missing_scope — key lacks the route's scope
429 rate_limited — see Retry-After
4xx { "error": "...", "detail": "..." } — validation failures name the fieldQuestions or a missing endpoint you need? Tell us — this surface grows with its first integrators.