All help

Using SupplySynk as a backend

Another app (for example a site built on bzlabs.io, an internal tool, or an AI agent) can use SupplySynk as its inventory and order backend: products, stock, orders, customers, purchasing, insights and Square sales, through the REST API or MCP. Both run the same services as the dashboard, with the same checks, audit log and webhooks, so an app can't do anything the business couldn't do itself.

Pick an interface

You are buildingUseAuth
A website that shows the shop's products and takes pickup ordersStorefront API /api/storefront/v1 (guide)Publishable key pk_live_…, safe in a web page
A server (site backend, sync job, integration)REST /api/v1API key, sent only from your server
An AI agent acting for a personMCP /api/mcp/<business-slug>OAuth sign-in (the person approves scopes and an access level)
An AI agent running unattendedMCP /api/mcpAPI key
Something that reacts to changesWebhooks (register over REST)Signing secret per endpoint

Authentication and tenancy

  • API keys are created by an owner or admin in Developers → API Keys, with only the scopes the app needs. Send Authorization: Bearer ssk_… (or HTTP Basic clientId:clientSecret). The key alone decides which business you act on; there is no organization id in requests. On a business host (acme.supplysynk.com or a custom domain) the key must belong to that business.
  • Scopes are read:* / write:* per area (products, inventory, orders, customers, suppliers, locations, categories, purchase_orders), plus read:reports, read:ai-insights, write:ai-insights, manage:webhooks, manage:integrations and read:settings / write:settings (the online store switch, GET/PUT /storefront/settings). Each endpoint and tool names its scope.
  • Never put an API key in a browser or mobile app. Keys are secret. For a web page use a publishable key with the Storefront API (below).

REST API

  • Base URL: https://<your-business>.supplysynk.com/api/v1 (or the app URL).
  • Spec: GET /api/v1/openapi.json (OpenAPI 3.1, public, generated from the same schemas the server validates with). Generate a typed client from it.
  • Pagination: ?limit= (1–100, default 25) and ?offset=; responses are { data, total, limit, offset }.
  • Idempotency: send Idempotency-Key: <uuid> on POST /orders, /purchase-orders, /transfers and /counter/sales (counter billing, guide). A retry with the same key and body returns the first result instead of creating a duplicate.
  • Errors: every error (auth, validation, not found, conflicts, 500) uses one envelope, { "error": { "code", "message", "details", "request_id" } } (details only when there is something to add), and the same id is in the X-Request-Id header. 400 invalid_request (validation, with field details) or invalid_json, 401, 403 forbidden (missing scope or role), 404 not_found (unknown id, another organization's id, or an id that isn't a UUID at all), 409 (conflict, invalid_state, insufficient_stock, insufficient_available), 429.
  • Usage: every call made with an API key (REST and MCP) is logged and shown under Developers → Usage Analytics (kept 90 days).
  • Rate limits: per key (default 1,000 requests/hour). Every response has X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset; a 429 adds Retry-After.
  • Money is in the business currency. Amounts (prices, costs, order and purchase-order totals, insights) are plain numbers in the organization's currency, USD or INR for now; nothing is converted. GET /api/v1/insights (and the get_sales_insights MCP tool) report it as period.currency, with the business's country (US or IN, D-22) as period.country. Purchase orders and new suppliers default to the business currency. For an Indian business, phone numbers are checked and stored as +91 98765 43210, and addresses are stored as { line1, line2, city, district, region, regionCode, postalCode, country } with the State/UT name in region, its GST state code in regionCode and a 6-digit PIN; an invalid one is a 400 naming the field (e.g. billingAddress.postalCode). Old address keys (street, state, zip) are still accepted and saved in the new shape.
  • Stock is never written directly. Use orders (reserve → fulfil → return), adjustments, transfers, counts and purchase-order receipts; each writes a ledger movement and fires webhooks. A product's stock history is GET /api/v1/inventory/movements?productId=… (newest first, with locationName and the adjustment reason as notes).
# Products with stock at each location
curl -s "https://acme.supplysynk.com/api/v1/products?limit=50" -H "Authorization: Bearer $SUPPLYSYNK_KEY"

# Place an order that reserves stock, safely retryable
curl -s -X POST https://acme.supplysynk.com/api/v1/orders \
  -H "Authorization: Bearer $SUPPLYSYNK_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7d3c0d2e-6a4f-4d55-9f43-2f0a0d9f1b11" \
  -d '{"channel":"web","channelOrderId":"WEB-1042","customerEmail":"sam@example.com","items":[{"productId":"<uuid>","quantity":2}]}'

Images and AI

  • Photos: upload in the dashboard, or add from a public link with the add_product_photo_from_url MCP tool. Each image in GET /products/{id} has url, mediumUrl, thumbUrl, alt, isPrimary and, for AI-made images, generated: true. Label or filter those in your shop (D-17).
  • POST /products/{id}/images/generate makes an AI image (studio, lifestyle, cleaned-up background or custom) and adds it to the product. Uses the business's monthly AI image allowance (GET /ai/image-allowance).
  • POST /products/{id}/describe and POST /products/describe-photo (multipart upload) draft a name, description, tags, category and alt text from a photo. Nothing is saved; apply what you keep with PATCH /products/{id}.

Webhooks

Owners and admins add endpoints in Developers → Webhooks, or an app adds its own over the API with a manage:webhooks key (POST /api/v1/webhooks; list event types with GET /api/v1/webhooks/events, check recent deliveries with GET /api/v1/webhooks/{id}/deliveries). Events: product.created|updated|deleted, inventory.updated|low_stock|out_of_stock, order.created|updated|fulfilled|cancelled|returned|paid|ready_for_pickup, customer.created|updated, contact.created, contact.consent_changed, supplier.created|updated, store_page.published|unpublished (an online store page changed; data has id, kind, slug, path, title) and more (the dashboard lists them all).

Each delivery is a POST with body { "id", "type", "created_at", "organization_id", "data" } and headers SupplySynk-Event-Id, SupplySynk-Event-Type and SupplySynk-Signature: t=<unix seconds>,v1=<hex> where v1 is HMAC-SHA256(secret, "<t>.<raw body>"). Verify it with a constant-time compare, reject old timestamps (e.g. > 5 minutes), and deduplicate on SupplySynk-Event-Id: delivery is at least once. Answer 2xx quickly; failures are retried with backoff for about two days, and an endpoint that keeps failing is disabled.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody: string, header: string, secret: string) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest();
  const given = Buffer.from(parts.v1 ?? "", "hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && given.length === expected.length && timingSafeEqual(given, expected);
}

MCP

POST /api/mcp/<business-slug> (or the shared POST /api/mcp) exposes the same operations as tools (products, stock, orders, purchasing, transfers, counts, insights, import, Square, master data, plus task-shaped tools). Agents sign in with OAuth or use an API key; tools/list shows only what the caller may use. OAuth connections have an access level; changes that need a person's go-ahead return { status: "pending_approval", approvalId } and wait in the approval queue (GET /api/v1/approvals; members approve in the app). GET /api/v1/ai/connection returns the business's MCP address. Full guide: MCP.md.

Square

A business that sells in person with Square connects it once in Settings → Square. From then on Square sales arrive as ordinary orders with channel: "square" (so GET /orders, order.created webhooks and Insights include them), and the sync log, catalog link, stock push and daily takings are available over REST and MCP. Guide: SQUARE.md.

Online store checkout (pickup)

The built-in store takes pickup orders once a way to pay is on (D-21; guide: online payments). Money always goes to the shop's own account; SupplySynk never holds it.

  • Public endpoints used by the store pages live under the store, not /api/v1: POST /api/store/<site>/cart (current prices), POST /api/store/<site>/checkout (place an order; send Idempotency-Key), GET /api/store/<site>/orders/<token> (the shopper's order page) and POST /api/store/<site>/orders/<token>/reference (UPI UTR). <site> is the business slug or verified custom domain; the business comes only from it, never from the body. They answer 404 for unpublished stores, accept POSTs only from the same origin, are rate limited per IP and per store, and never return costs or stock counts. Other sites get the same through the Storefront API (next section).
  • Orders from the store have channel: "online" and are confirmed (stock reserved). paymentStatus is pending, to_verify (UPI reference entered) or paid; fulfillmentStatus becomes ready_for_pickup when staff mark it. Filter with GET /orders?channel=online; details (method, UTR, pickup, phone, hold) at GET /orders/{id}/online; staff actions POST /orders/{id}/mark-paid, /ready-for-pickup, /picked-up.
  • Settings: GET / PUT /api/v1/payments/connections; POST /payments/connections/{provider}/test (a harmless call with the stored keys) and DELETE /payments/connections/{provider} (disconnect). Keys and tokens are sealed (AES-256-GCM) and never returned (storedCredentials lists names only). Each provider has a state: connected, needs_attention, not_connected or not_set_up (the server lacks its platform keys).
  • Hosted checkout (Stripe, PayPal, Razorpay, Square online checkout): the checkout answers next: { kind: "redirect", url }; the shopper pays on the provider's page and comes back to their order page (?return=1), which checks for the confirmation. Payments are created on the shop's own account: Stripe direct charges with Stripe-Account (Connect Standard, OAuth; never an application fee), the shop's own PayPal REST app and Razorpay keys, and the shop's Square connection. Adapters live in src/lib/payments/ behind PaymentProvider (types.ts); no SDKs.
  • Provider webhooks: POST /api/payments/webhooks/<provider>/<connectionId> (PayPal: verified with PayPal's verify API and the shop's webhook id; Razorpay: X-Razorpay-Signature with the shop's webhook secret), and Stripe's single Connect endpoint POST /api/payments/stripe/webhook (Stripe-Signature, 5-minute tolerance, routed by the event's account). Square online payments arrive on the Square webhook. Events are stored once (payment_events, by provider event id): a paid event confirms the order once (order.paid), a closed-unpaid page ends the hold at once, a refund is noted. PayPal is captured when the shopper returns (GET /api/payments/paypal/return, PayPal-Request-Id per checkout) or on CHECKOUT.ORDER.APPROVED, never after the hold ended. A payment that arrives after the hold ended is recorded and flagged on the order for a refund.
  • Payments that need a person: a payment for a different amount than the order is recorded as to_verify (order paymentStatus: to_verify, stock stays held) and owners/admins are emailed; staff mark it paid or refund it. A payment after the hold ended is recorded, noted and emailed too. Hosted pages that can be closed (Square payment links, Stripe sessions, Razorpay links) are closed when the hold ends. Public payment endpoints are rate limited per IP and refuse bodies over 512 KB.
  • Disconnecting with checkouts still paying answers 409 open_checkouts; ?force=true cancels those orders first. Only what verifies late webhooks is kept (the Stripe account id, the Razorpay webhook secret), so late payments are recorded and flagged rather than lost.
  • Refunds: POST /orders/{id}/refund refunds the provider payment in full on the shop's account (owners and admins signed in to SupplySynk only; not API keys or agents). Refunds made in the provider's dashboard are noted by webhook.
  • Holds end unpaid orders (cancelled, stock released) after 48 h (pay at pickup), 30 min (UPI) or 35 min (hosted checkout: the provider's page closes at 31). Expiry runs lazily and daily; for timely expiry on Vercel Hobby, call GET /api/cron/checkouts with Authorization: Bearer $CRON_SECRET every few minutes from an external scheduler.

Storefront API (publishable keys)

For a website (a bzlabs.io site, a theme, a static page) that shows the shop's products and takes pickup orders. Step-by-step with a copy-paste example: Storefront API guide. Spec: GET /api/storefront/v1/openapi.json. Decision: D-24.

  • Key: pk_live_…, created by an owner or admin in Developers → Storefront keys (or POST /api/v1/storefront/keys, write:settings; MCP create_storefront_key). Shown once, stored hashed, revocable (DELETE /api/v1/storefront/keys/{id}), with its last use recorded. Send Authorization: Bearer pk_live_…. A publishable key is refused by /api/v1 and MCP; a secret key is refused by the Storefront API.
  • Allowed websites (CORS): each key lists origins such as https://papaswillow.com (https, or http://localhost while building). A browser call from any other origin is 403 origin_not_allowed. Calls with no Origin header (your server) are allowed.
  • Endpoints: GET /store (branding, contact, hours with open now, ways to pay with the shop's note, pickup locations), GET /categories, GET /products?category=&q=&sort=&limit=&cursor=, GET /products/{slug}, POST /cart, POST /checkout (send Idempotency-Key), GET /orders/{token}.
  • Same data and rules as the built-in store: published stores only (404 otherwise), active products, availability as a label, never costs, suppliers, SKUs or stock counts; checkout prices from the database, stock held, shopper and staff emails, rate limited per shopper and per store.
  • Rate limits (own buckets): browsers 120 calls a minute per shopper IP and 3,000 per key; servers 60 a minute per key, so cache on your side (responses are Cache-Control: private, max-age=30). 429 with Retry-After.

Marketing (contacts, buy links, post drafts)

/api/v1/marketing/** (tag Marketing in the OpenAPI spec; scopes read:marketing, read:contacts (personal data; not in the read-only bundle), write:marketing, manage:marketing): contacts with per-channel consent (list, add, consent history, record consent with a source and reason, CSV import with a required consent_source column, export of opted-in contacts as CSV or vCard, erasure), store sign-ups waiting for confirmation (email double opt-in; WhatsApp confirmed by a manager), saved segments, short buy links (/go/<code> on the business address; store pages only, or an https address added by an admin) with clicks, orders and revenue, post drafts with images and AI captions, and the store's marketing settings. Only message contacts whose consent.<channel>.subscribed is true, and include their unsubscribe link (GET /marketing/contacts/{id} → unsubscribeUrls). Orders placed in the built-in store are attributed to the link or UTMs of the visit (30 days, last touch); orders through the Storefront API aren't attributed yet. Shop owners' guide: guides/marketing.md; background: product/marketing.md.

Tax (GST and US sales tax)

Guide for shop owners: tax. Decision: D-25. Engine: src/lib/tax (pure, amounts in minor units, rounding half-up per line component).

  • Settings: GET / PATCH /api/v1/tax/settings (read:settings / write:settings, owner or admin). regime: India none | composition | regular; US none | sales_tax. Also gstin (checked), legalName, tradeName, pricesIncludeTax (India only; US prices exclude tax), defaultRate (India: a GST slab; US: the sales tax rate) and rounding (none | nearest_1). configured is false until a regime is saved; until then orders keep the old arithmetic exactly (no tax, no round-off).
  • Products: taxRate (null = the business default; India: a GST slab), taxExempt, hsnCode (4/6/8 digits), mrp, unit (pcs, kg, g, L, ml, m, dozen, pair, set). India: a sellingPrice (or a variant's) above the mrp is 400 above_mrp. Clothing and footwear HSN codes take their rate from the price band (5% up to ₹2,500 a piece, 18% above; to verify), overriding taxRate.
  • Products, packaged-goods details (India Stage 4, D-29; stored for any country, shown on India stores): countryOfOrigin, manufacturerRole (manufacturer | packer | importer, needs manufacturerName), manufacturerName, manufacturerAddress, netQuantity (up to 3 decimals) with netQuantityUnit (same codes as unit; one needs the other), bestBefore, legalNote. A variant with a field null uses its family's.
  • Online store seller details (India): PUT /storefront/settings with sellerInfo: { grievanceName, grievanceDesignation, grievanceEmail, grievancePhone, returnsPolicy } (owner or admin; null clears a field). The store footer shows them with the legal name and GSTIN from the tax settings; the Storefront API returns them as seller on GET /store.
  • Locations: gstin and stateCode (India): the place of supply for sales made there. Default: the business GSTIN, the state from the address.
  • Orders: POST /orders works tax out unless a taxAmount is given on any line or the order; then the given amounts are used as before and the tax fields stay null. Optional placeOfSupply (state name or GST code; default the selling location's: IGST when it differs) and buyerGstin. Responses add taxRegime, taxInclusive, placeOfSupply, roundOff, sellerGstin, buyerGstin, taxBreakup.components[] (component, rate, taxableValue, amount) and, per item, hsn, taxRate, taxableValue and taxes[]. Components: CGST, SGST, UTGST, IGST, SALES_TAX.
    • Prices include tax (taxInclusive: true, India): totalAmount = subtotal − discountAmount + shippingAmount + roundOff (the tax is inside); otherwise + taxAmount as well.
    • An order-level discount is spread over the lines before tax; a line's totalAmount is what's paid for it after its share.
    • India: an item unitPrice above the product's MRP is 400 above_mrp.
  • Purchase orders: without a taxAmount, GST-registered Indian businesses get input tax from each product's rate on its cost (taxes[] per item; IGST from a supplier in another state, by the supplier's address; taxNote says when the supplier's state was unknown and the same state was assumed). Viewers don't see it.
  • Preview: GET /api/v1/tax/preview?items=[{"productId":"…","quantity":2}] (JSON in the query) or POST with { items, locationId?, placeOfSupply?, discountAmount?, shippingAmount? } (read:products). Lines without a product take unitPrice and taxRate. Nothing is saved.
  • Checkout: the store cart and checkout responses add tax (inclusive, label, amount, roundOff, total) and the checkout subtotal; total is what the shopper pays. Square orders keep Square's own tax.

GST documents (India)

Decision D-27; guide tax.

  • POST /orders/{id}/invoice (write:orders): issues the Tax Invoice or Bill of Supply, or returns the one issued (also automatic on full fulfilment). GET /gst/documents?from=&to=&type=&orderId= and GET /gst/documents/{id} (read:orders): number, totals, snapshot (everything printed) and publicUrl (/bill/<token>, add ?format=thermal for 80 mm).
  • POST /gst/documents/{id}/credit-note (write:orders): { items?: [{ productId, quantity }], reason }; without items, everything not yet credited. Automatic for returns and cancellations of invoiced orders.
  • POST /orders/{id}/gst-retry (write:orders): issues whatever the order is still owed (invoice, credit notes); orders show gstPending and gstPendingError while one can't be issued. Errors: buyer_details_missing, hsn_missing, number_taken, series_full (409).
  • Files (not JSON; read:reports): GET /gst/gstr1?period=2026-10|2026-Q3&format=json|csv&section=, GET /gst/registers/sales?from=&to=, GET /gst/registers/purchases?from=&to= (staff and up), GET /gst/tally?from=&to=|period=&inventory=&part=all|masters|vouchers (Tally Prime import XML; members need the manager role).
  • Tally ledger names: GET /gst/tally/settings (read:reports), PATCH (write:settings; null resets a name). See guides/tally.md.

Not yet

These are needed for some backend uses and are on the roadmap (product/roadmap.md):

MissingWorkaround todayPlanned
Connecting Square over the APIOne-time connect in the dashboard (the seller must approve on Square's page)By design
Customer sign-in for shoppersYour app's own accounts; store them as customers via /customersPhase 3