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}      → deactivate

Every 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 it

Errors

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 field

Questions or a missing endpoint you need? Tell us — this surface grows with its first integrators.

ThreadzShops

Done-for-you apparel brands for creators, teams, and businesses. Share your vision — we build the store and ship every order.

© 2026 ThreadzShops. All rights reserved.

Built in Mesa, AZ · printed in Utah