SprytStack

/ Developers

SprytStack API

Connect your own tools to your business: requests, orders, menu and analytics over a simple JSON API. Try every endpoint right here with your API key.

Getting started

Every request needs an API key for one business. Owners create keys in their dashboard under API & AI connections (Pro plan), choose read only or read & write, and can revoke them at any time. The same keys work for the MCP endpoint used by AI assistants.

Base URL
https://sprytstack.com/api/v1
Authentication
Authorization: Bearer spk_… or X-API-Key: spk_…
Format
JSON in and out. Amounts are integer cents plus a display string. Lists come as { "data": [...] }.
Rate limit
120 requests per minute per key.
Spec
openapi.json (OpenAPI 3.1 — import into Postman, Insomnia or a code generator)

Keep keys on your server. Anyone with a key can read that business's customer details until it's revoked. Cancellations and refunds aren't available through the API.

Errors

Errors use HTTP status codes and a JSON body: { "error": { "code", "message", "details"? } }.

StatuscodeWhen
400invalid_requestA parameter or the JSON body is wrong; `details` lists every problem.
401unauthorizedNo key, or the key is unknown, revoked or expired.
403forbidden · insufficient_scope · feature_not_enabledPlan without API access, a read-only key on a write endpoint, or a module that's off.
404not_foundNo such endpoint, or no such item for this business.
409conflictThe change isn't possible right now (e.g. the order is already completed).
429rate_limitedOver 120 requests a minute for this key; wait for Retry-After.

Webhooks

Instead of polling, let SprytStack call you. Owners add a Webhooks, Zapier, Make or n8n connector in their dashboard → Connectors, pick events, and every event is POSTed as JSON to their URL. Request and order objects have the same shape as in the REST API.

EventWhen
request.createdA customer booked or asked for a quote on the website (after any deposit is paid).
request.status_changedA request moved to another step of the workflow. `previous_status` says where it was.
request.cancelledThe business or the customer cancelled a request.
order.createdA new online order (card orders once they're paid). (ordering module)
order.status_changedAn order was accepted, prepared, made ready, sent out, completed or cancelled. (ordering module)
order.refundedA card payment was refunded in full (when the order was cancelled, or from Stripe). (ordering module)
connector.testSent when the owner clicks “Send test event”.
Example payload
{
  "id": "evt_2kQ9mX4pT7aB",
  "type": "request.status_changed",
  "created_at": "2026-10-04T15:20:40.000Z",
  "business": {
    "id": "8f0c2d1e-…",
    "name": "FlowFix Plumbing"
  },
  "data": {
    "previous_status": "Request Received",
    "request": {
      "id": "K7Q2MX",
      "service": "Water heater repair",
      "when": "Tue, Oct 6 · Morning (8am – 12pm)",
      "status": "Scheduled",
      "step": 2,
      "cancelled": false,
      "customer": {
        "name": "Sam Rivera",
        "phone": "+1 206 555 0147",
        "email": "sam@example.com",
        "marketing_opt_in": false
      },
      "notes": "",
      "created_at": "2026-10-04T15:02:11.000Z",
      "updated_at": "2026-10-04T15:20:40.000Z"
    },
    "dashboard_url": "https://sprytstack.com/owner/8f0c2d1e-…"
  }
}

Signatures

Deliveries follow the Standard Webhooks spec: headers webhook-id, webhook-timestamp and webhook-signature (v1, + base64 HMAC-SHA256 of {id}.{timestamp}.{body}, keyed with your whsec_… signing secret). Check it on the raw body and reject timestamps older than five minutes:

import { Webhook } from "standardwebhooks"; // npm i standardwebhooks

const wh = new Webhook(process.env.SPRYTSTACK_WEBHOOK_SECRET.replace(/^whsec_/, ""));

app.post("/hooks/sprytstack", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    event = wh.verify(req.body.toString(), req.headers); // throws if forged or older than 5 min
  } catch {
    return res.sendStatus(400);
  }
  if (alreadyHandled(event.id)) return res.sendStatus(200); // retries reuse the same id
  handle(event);
  res.sendStatus(200);
});

Delivery and retries

  • Answer with any 2xx within 10 seconds; do slow work after responding.
  • Other answers and timeouts are retried after 1 min, 5 min, 30 min, 2 h, 6 h and 12 h (about a day), then marked failed. Owners can redeliver from the log.
  • 410 Gone pauses the connector straight away; ten events in a row that fail every retry pause it too.
  • The event id is the same on every retry — use it to ignore duplicates. Events can arrive out of order; use updated_at.
  • HTTPS only, standard port, public addresses only; redirects aren't followed.

Business

Business overview

GET/api/v1/business

The business this API key belongs to: open and new requests, today's orders and revenue, which modules are on and whether online ordering is open.

Any key

Example response · 200
{
  "id": "8f0c…",
  "name": "FlowFix Plumbing",
  "site": "https://sprytstack.com/app/flowfix-plumbing",
  "status": "live",
  "request_noun": "service request",
  "workflow": [
    "Request Received",
    "Technician Assigned",
    "Scheduled",
    "Technician On Site",
    "Work Completed",
    "Paid"
  ],
  "open_requests": 4,
  "new_requests": 1,
  "orders_last_24h": null,
  "taking_online_orders": false,
  "modules": [
    "booking",
    "analytics",
    "mcp"
  ]
}

Requests

List requests

GET/api/v1/requests

Bookings, quotes or appointments from the website (the industry decides the word), newest first. Requests still waiting for a deposit payment are left out.

Any key

Parameters

  • statusquery · stringnew = just received · open = not finished · done = last step reached. One of new, open, done, cancelled, all. Default open.
  • limitquery · integerHow many to return. 1–100. Default 20.
Example response · 200
{
  "data": [
    {
      "id": "K7Q2MX",
      "service": "Water heater repair",
      "when": "Tue, Oct 6 · Morning (8am – 12pm)",
      "scheduled_date": "2026-10-06",
      "scheduled_time": "08:00",
      "status": "Scheduled",
      "step": 2,
      "cancelled": false,
      "customer": {
        "name": "Sam Rivera",
        "phone": "+1 206 555 0147",
        "email": "sam@example.com",
        "marketing_opt_in": false
      },
      "notes": "Side gate code 4411",
      "deposit": null,
      "created_at": "2026-10-04T15:02:11.000Z",
      "updated_at": "2026-10-04T15:20:40.000Z"
    }
  ]
}

Get a request

GET/api/v1/requests/{id}

One request with the steps still ahead of it and its full history.

Any key

Parameters

  • id *path · stringRequest reference (case-insensitive).
Example response · 200
{
  "id": "K7Q2MX",
  "service": "Water heater repair",
  "when": "Tue, Oct 6 · Morning (8am – 12pm)",
  "scheduled_date": "2026-10-06",
  "scheduled_time": "08:00",
  "status": "Scheduled",
  "step": 2,
  "cancelled": false,
  "customer": {
    "name": "Sam Rivera",
    "phone": "+1 206 555 0147",
    "email": "sam@example.com",
    "marketing_opt_in": false
  },
  "notes": "Side gate code 4411",
  "deposit": null,
  "created_at": "2026-10-04T15:02:11.000Z",
  "updated_at": "2026-10-04T15:20:40.000Z",
  "next_steps": [
    "Technician On Site",
    "Work Completed",
    "Paid"
  ],
  "history": [
    {
      "at": "2026-10-04T15:02:11.000Z",
      "by": "customer",
      "text": "Requested on the website"
    }
  ]
}

Move a request to a step

PATCH/api/v1/requests/{id}

Sets the request's workflow step (see `workflow` in the business overview). The customer gets the usual automatic updates. Cancelling isn't available through the API.

Read & write key

Parameters

  • id *path · stringRequest reference (case-insensitive).

JSON body

  • status *body · stringOne of the business's workflow steps, e.g. "Scheduled".
Example response · 200
{
  "id": "K7Q2MX",
  "service": "Water heater repair",
  "when": "Tue, Oct 6 · Morning (8am – 12pm)",
  "scheduled_date": "2026-10-06",
  "scheduled_time": "08:00",
  "status": "Scheduled",
  "step": 2,
  "cancelled": false,
  "customer": {
    "name": "Sam Rivera",
    "phone": "+1 206 555 0147",
    "email": "sam@example.com",
    "marketing_opt_in": false
  },
  "notes": "Side gate code 4411",
  "deposit": null,
  "created_at": "2026-10-04T15:02:11.000Z",
  "updated_at": "2026-10-04T15:20:40.000Z"
}

Orders

List orders

GET/api/v1/orders

Online orders, newest first. Card checkouts that were never paid are left out.

Any keyNeeds the ordering module

Parameters

  • daysquery · integerHow many days back. 1–30. Default 1.
  • statusquery · stringactive = not completed or cancelled. One of active, received, accepted, preparing, ready, out_for_delivery, completed, cancelled, all. Default active.
Example response · 200
{
  "data": [
    {
      "id": "Q7K2PX",
      "type": "pickup",
      "status": "preparing",
      "status_label": "Preparing",
      "total_cents": 2450,
      "currency": "USD",
      "total": "$24.50",
      "payment_status": "paid",
      "payment_method": "online",
      "customer": {
        "name": "Alex Kim",
        "phone": "+1 206 555 0199",
        "email": "alex@example.com",
        "marketing_opt_in": true
      },
      "item_count": 3,
      "placed_at": "2026-10-04T17:45:03.000Z",
      "updated_at": "2026-10-04T17:52:19.000Z"
    }
  ]
}

Get an order

GET/api/v1/orders/{id}

One order with its items, totals, payment, next steps and history.

Any keyNeeds the ordering module

Parameters

  • id *path · stringOrder reference (case-insensitive).
Example response · 200
{
  "id": "Q7K2PX",
  "type": "pickup",
  "status": "preparing",
  "status_label": "Preparing",
  "total_cents": 2450,
  "currency": "USD",
  "total": "$24.50",
  "payment_status": "paid",
  "payment_method": "online",
  "customer": {
    "name": "Alex Kim",
    "phone": "+1 206 555 0199",
    "email": "alex@example.com",
    "marketing_opt_in": true
  },
  "item_count": 3,
  "placed_at": "2026-10-04T17:45:03.000Z",
  "updated_at": "2026-10-04T17:52:19.000Z",
  "lines": [
    {
      "item_id": "latte",
      "name": "Oat latte",
      "qty": 2,
      "unit_cents": 550,
      "total_cents": 1100,
      "note": ""
    }
  ],
  "subtotal_cents": 2250,
  "delivery_fee_cents": 0,
  "tax_cents": 200,
  "address": "",
  "notes": "",
  "next_steps": [
    "ready",
    "completed"
  ],
  "history": []
}

Move an order forward

PATCH/api/v1/orders/{id}

Accept, start preparing, mark ready / out for delivery, or complete an order. The customer gets the usual updates. Cancelling and refunds aren't available through the API.

Read & write keyNeeds the ordering module

Parameters

  • id *path · stringOrder reference (case-insensitive).

JSON body

  • status *body · stringThe step to move to. One of accepted, preparing, ready, out_for_delivery, completed.
Example response · 200
{
  "id": "Q7K2PX",
  "type": "pickup",
  "status": "ready",
  "status_label": "Ready for pickup",
  "total_cents": 2450,
  "currency": "USD",
  "total": "$24.50",
  "payment_status": "paid",
  "payment_method": "online",
  "customer": {
    "name": "Alex Kim",
    "phone": "+1 206 555 0199",
    "email": "alex@example.com",
    "marketing_opt_in": true
  },
  "item_count": 3,
  "placed_at": "2026-10-04T17:45:03.000Z",
  "updated_at": "2026-10-04T17:52:19.000Z"
}

Menu

Get the menu

GET/api/v1/menu

Menu items with prices and whether each is available or sold out, and whether online ordering is open.

Any keyNeeds the ordering module

Example response · 200
{
  "currency": "USD",
  "accepting_orders": true,
  "items": [
    {
      "id": "latte",
      "name": "Oat latte",
      "category": "Coffee",
      "description": "",
      "price_cents": 550,
      "price": "$5.50",
      "available": true
    }
  ]
}

Mark an item sold out or available

PATCH/api/v1/menu/items/{itemId}

Use Get the menu to find item ids.

Read & write keyNeeds the ordering module

Parameters

  • itemId *path · stringMenu item id.

JSON body

  • available *body · booleanfalse = sold out.
Example response · 200
{
  "id": "latte",
  "name": "Oat latte",
  "category": "Coffee",
  "description": "",
  "price_cents": 550,
  "price": "$5.50",
  "available": false
}

Pause or resume online orders

PATCH/api/v1/ordering

Stop taking online orders (e.g. the kitchen is overloaded) or start again. Customers can still browse the menu while paused.

Read & write keyNeeds the ordering module

JSON body

  • accepting *body · booleantrue = take online orders.
Example response · 200
{
  "accepting_orders": false
}

Analytics

Website analytics

GET/api/v1/analytics

Cookie-free visitor counts, page views, sources, devices and booking/ordering actions. Aggregates only — no personal data.

Any keyNeeds the analytics module

Parameters

  • daysquery · integerPeriod ending today. One of 7, 30. Default 7.
Example response · 200
{
  "days": 7,
  "from": "2026-09-28",
  "to": "2026-10-04",
  "visitors": 412,
  "page_views": 1290,
  "daily": [
    {
      "day": "2026-10-04",
      "visitors": 61,
      "pageviews": 180
    }
  ],
  "top_sources": [
    {
      "source": "google",
      "visitors": 190
    }
  ],
  "devices": [
    {
      "device": "mobile",
      "visitors": 288
    }
  ],
  "actions": {
    "booking_submitted": 9
  }
}