Teasr Backstage

Connect your shop's systems to Teasr

Backstage is the Teasr API for shops. Get paid orders into your own app the moment they happen, and keep Teasr stock in step with your inventory system.

Overview

Three building blocks

ToolDirectionUse it to
Order webhookTeasr → your appReceive every paid order in real time (HTTPS POST, signed).
Inventory APIYour app → TeasrSet stock on your Teasr listings by SKU, variant id, or listing id.
Orders APIBothBackfill or poll orders you might have missed, and mark orders shipped with tracking.

No code? The order webhook also works with Zapier and Make, including creating the order in Shopify. The setup guide is in Seller Hub → Integrations.

Step 1

Getting started

  1. Open Seller Hub → Integrations and create a key under API keys. Copy it right away: Teasr only shows it once. Keys start with tsk_.
  2. Keep the key on your server. Never put it in a browser, mobile app, or public repo. Anyone with it can change your stock and read your orders, including buyer addresses. Revoke a key in the same place and it stops working immediately.
  3. Send it on every request as Authorization: Bearer tsk_… (or X-Teasr-Api-Key: tsk_…).

Base URL: https://backstage.teasr.live/v1. Check your key:

curl https://backstage.teasr.live/v1/me \
  -H "Authorization: Bearer tsk_your_key"
{
  "data": { "sellerId": "…", "sellerName": "Velvet Vault", "handle": "velvetvault" },
  "timestamp": "2026-10-12T17:05:00.000Z"
}

Every successful response wraps the result in data. Errors come back as { "statusCode": 400, "message": "…" }, where message is a string or a list of strings.

Step 2

Inventory and stock

How Teasr identifies an item

Each stock update names its item by exactly one of:

  • sku: your SKU. Variants carry their own SKU; a listing without variants has a listing-level SKU. Matching ignores case and surrounding spaces. If the same SKU is on more than one of your listings, all of them are updated, since they are the same item on your shelf.
  • variantId: a Teasr variant id.
  • productId: a Teasr listing id, for listings without variants only.

SKUs come from your CSV import or the listing editor. To fill in missing ones, list your catalog and set them through the API (below). Doing that once means your system can push stock by SKU from then on, without storing any Teasr ids.

List your catalog

GET/v1/products?page=1&limit=100

Your live and draft listings (archived ones are left out), oldest first, with variants, SKUs, and stock. limit is up to 250; keep requesting the next page while hasMore is true. Add sku=ABC-1 to find the listings carrying one SKU.

{
  "data": {
    "products": [
      {
        "id": "4c1f…",
        "title": "Lace Bodysuit",
        "status": "active",
        "sku": null,
        "price": "60.00",
        "quantity": 7,
        "isDigital": false,
        "stockSource": "teasr",
        "createdAt": "2026-09-01T15:20:11.000Z",
        "updatedAt": "2026-10-12T16:02:40.000Z",
        "variants": [
          { "id": "9a0e…", "title": "S", "sku": "VV-LACE-S", "price": "60.00", "quantity": 3 },
          { "id": "b71d…", "title": "M", "sku": "VV-LACE-M", "price": "60.00", "quantity": 4 }
        ]
      }
    ],
    "page": 1,
    "limit": 100,
    "hasMore": false
  }
}
  • A listing with variants has sku: null, and its quantity is the total across its variants. Teasr keeps that total up to date for you.
  • quantity: null means unlimited (digital listings).
  • stockSource says who owns the count. Only teasr listings accept stock updates through the API. shopify and spicerack listings are synced from those stores, and nalpac listings are managed by the distributor.

GET/v1/products/{productId}

One listing, same shape.

PATCH/v1/products/{productId}

Set the SKU on a listing without variants. Body: { "sku": "VV-BLF" }. Send null to clear it.

PATCH/v1/products/{productId}/variants/{variantId}

Set the SKU on one variant. Same body.

Push stock

POST/v1/inventory

Set the stock for up to 250 items per request. Quantities are absolute (how many you have), not changes, so sending the same request twice is safe.

curl -X POST https://backstage.teasr.live/v1/inventory \
  -H "Authorization: Bearer tsk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "sku": "VV-BLF", "quantity": 12 },
      { "sku": "VV-LACE-S", "quantity": 0 },
      { "variantId": "b71d…", "quantity": 4 }
    ]
  }'
{
  "data": {
    "results": [
      {
        "sku": "VV-BLF",
        "status": "updated",
        "matches": [
          { "productId": "1e2a…", "variantId": null, "previousQuantity": 5, "quantity": 12, "status": "updated" }
        ]
      },
      { "sku": "VV-LACE-S", "status": "updated", "matches": [ … ] },
      { "variantId": "b71d…", "status": "unchanged", "matches": [ … ] }
    ]
  }
}

Results come back in the same order as items. One bad item never fails the others, so read each result:

statusMeaning
updatedStock changed on at least one matching listing.
unchangedAlready at that quantity. Nothing written.
not_foundNo listing of yours has that SKU or id.
skippedFound but not writable, with a reason: the stock is synced from Shopify, Spicerack, or the distributor, the listing is digital, or you sent a productId for a listing with variants.

A quantity of 0 marks the listing sold out. It stays on Teasr, sorted after in-stock items, and becomes buyable again as soon as you push stock back.

Keeping both sides in step

A sale on Teasr lowers the Teasr stock straight away. Your system needs to hear about that sale too, or its next push will put the sold unit back. The reliable pattern:

  1. Receive each paid order through the order webhook and take the units out of your own inventory.
  2. Whenever stock changes in your system (sales anywhere, restocks, counts), push the new absolute quantity for those SKUs.
  3. Every hour or so, poll GET /v1/orders for anything the webhook missed, then push a full snapshot of your stock.

Step 3

Order webhook

Teasr sends an HTTPS POST to your URL the moment an order is paid. Set the URL in Seller Hub → Integrations → Order webhook, then use Send test order to get a sample. The URL must be https:// on a public host.

POST /your/endpoint
Content-Type: application/json
User-Agent: Teasr-Webhooks/1.0
X-Teasr-Topic: order.paid
X-Teasr-Webhook-Version: 2026-08-01
X-Teasr-Delivery-Id: 0d6c7c3e-…
X-Teasr-Hmac-Sha256: q1Zx…=

{
  "event": "order.paid",
  "webhookVersion": "2026-08-01",
  "sentAt": "2026-10-12T17:03:45.000Z",
  "order": { …the order object… }
}
  • Reply with any 2xx within 10 seconds. Do the heavy work after you reply.
  • A failed delivery is retried twice: after 5 seconds and after 30 seconds. Retries reuse the same X-Teasr-Delivery-Id. After that, pick the order up with the Orders API.
  • Each order is sent once. Still, store order.idand ignore ones you have already processed, so a retry after a slow reply can't create a duplicate.
  • Test deliveries have "test": true at the top level. Skip them in production.
  • Each order has one line item, so lineItems[0] is always the item sold.

Verify the signature

X-Teasr-Hmac-Sha256 is the base64 HMAC-SHA256 of the raw request body, keyed with the signing secret shown next to your webhook URL. Compute it over the exact bytes you received, before parsing the JSON.

// Node.js (Express)
const crypto = require("crypto");

app.post("/teasr/orders", express.raw({ type: "application/json" }), (req, res) => {
  const expected = crypto
    .createHmac("sha256", process.env.TEASR_WEBHOOK_SECRET)
    .update(req.body) // raw Buffer
    .digest("base64");
  const received = req.get("X-Teasr-Hmac-Sha256") || "";
  const ok =
    expected.length === received.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
  if (!ok) return res.status(401).end();

  const { event, test, order } = JSON.parse(req.body);
  res.status(200).end();
  if (event === "order.paid" && !test) queueOrder(order);
});
# Python (Flask)
import base64, hashlib, hmac, os
from flask import Flask, request, abort

@app.post("/teasr/orders")
def teasr_orders():
    raw = request.get_data()
    expected = base64.b64encode(
        hmac.new(os.environ["TEASR_WEBHOOK_SECRET"].encode(), raw, hashlib.sha256).digest()
    ).decode()
    if not hmac.compare_digest(expected, request.headers.get("X-Teasr-Hmac-Sha256", "")):
        abort(401)
    payload = request.get_json()
    if payload["event"] == "order.paid" and not payload.get("test"):
        queue_order(payload["order"])
    return "", 200

If the secret leaks, rotate it from the same panel and update your receiver.

Step 4

Orders API

GET/v1/orders?updatedSince=2026-10-12T00:00:00Z&limit=50

Your paid and refunded orders, oldest change first. An order shows up again whenever it changes (shipped, delivered, refunded), so one loop catches new orders and updates. limit is up to 250.

{
  "data": {
    "orders": [ { …order object… } ],
    "nextCursor": "MTc5MTgwMTI5NTg0MXxjY2Nj…",
    "hasMore": true
  }
}

Start with updatedSince (or nothing, for all orders), then pass cursor=nextCursor while hasMore is true. Save the last nextCursor; on your next poll, send it again to get only what changed since.

GET/v1/orders/{orderId}

One order, same shape as the webhook.

POST/v1/orders/{orderId}/fulfillment

Mark a paid order shipped. This does exactly what Seller Hub does: the buyer gets the shipped email with tracking, and your payout for the order is released.

curl -X POST https://backstage.teasr.live/v1/orders/b7e2c9a4-…/fulfillment \
  -H "Authorization: Bearer tsk_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "trackingNumber": "9400111899223456789012", "trackingCarrier": "USPS" }'

Both fields are optional, but send tracking whenever you have it. Orders from the same live show that ship in one box share the tracking number and are marked shipped together.

Reference

The order object

The webhook and the Orders API return the same object:

{
  "id": "b7e2c9a4-1f3d-4e8a-9c21-6f5d8a0b3e77",
  "orderNumber": "TSR-10482",
  "status": "pending",
  "paymentStatus": "paid",
  "createdAt": "2026-10-12T17:03:41.000Z",
  "updatedAt": "2026-10-12T17:03:44.000Z",
  "currency": "USD",
  "subtotalAmount": "64.00",
  "shippingAmount": "6.99",
  "taxAmount": "5.68",
  "totalAmount": "76.67",
  "source": "live_show",
  "liveShowId": "3f9a1c2e-7b6d-4a51-8e02-d94c5b7f1a30",
  "isGiveaway": false,
  "seller": { "id": "…", "sellerName": "Velvet Vault", "handle": "velvetvault" },
  "buyer": {
    "firstName": "Jamie",
    "lastName": "Rivera",
    "email": "jamie.rivera@example.com",
    "phone": "+15555550142"
  },
  "shippingAddress": {
    "name": "Jamie Rivera",
    "line1": "482 Cedarwood Ave",
    "line2": "Apt 3B",
    "city": "Austin",
    "state": "TX",
    "postalCode": "78704",
    "country": "US"
  },
  "lineItems": [
    {
      "productId": "…",
      "variantId": "…",
      "title": "Silk Blindfold Set",
      "variantTitle": "Midnight Black",
      "sku": "VV-BLF-BLK",
      "quantity": 1,
      "unitPrice": "64.00",
      "totalPrice": "64.00",
      "shopifyVariantGid": null,
      "shopifyVariantId": null
    }
  ],
  "fulfillment": {
    "trackingNumber": null,
    "trackingCarrier": null,
    "shippedAt": null,
    "deliveredAt": null
  }
}
FieldNotes
idTeasr order id. Use it to deduplicate.
orderNumberShown to the buyer, e.g. TSR-10482.
statuspending, processing, shipped, delivered, cancelled, or refunded.
paymentStatuspaid or refunded.
subtotalAmount, shippingAmount, taxAmount, totalAmountDecimal strings in USD. shippingAmount and taxAmount can be null (free or older orders).
sourcelive_show (with liveShowId) or teasr for any other checkout.
isGiveawayTrue for giveaway wins: ship it, but nothing was charged for the item.
shippingAddressWhere to ship. null for digital items.
lineItems[0].skuThe variant SKU, or the listing SKU for listings without variants.
lineItems[0].shopifyVariantIdNumeric Shopify variant id, only for listings linked to Shopify.
fulfillmentTracking and timestamps once the order ships.

New fields may be added at any time, so ignore fields you don't recognize. Breaking changes get a new webhookVersion.

Reference

Limits and errors

  • 120 requests per minute. Over that you get 429; wait a minute and retry. Batch stock updates (250 items per request) instead of sending one request per SKU.
  • Up to 10 active API keys per shop.
StatusMeaning
400The request is malformed. message says which field.
401Missing, invalid, or revoked API key.
404Not found, or it belongs to another shop.
429Too many requests.
500Something broke on our side. Retry with backoff.

Questions or stuck on something? Contact support and mention the Seller API.