Home›Restock›API
Restock · API

Restock API, as shipped in the download

Restock REST API, Webhooks & MCP

Restock ships a JSON REST API and signed webhooks so your reorder plan can be driven by whatever already holds your sales data — a shop platform, an ERP export, a nightly cron, Power Automate, Zapier, n8n, or a twenty-line script.

The important part: the numbers this API returns are the numbers on your screen. The reorder point, days of cover and suggested order quantity all come from the same deterministic forecasting engine the report page renders. There is no separate API maths, and no model in the loop — the same inputs always give the same answer.

Authentication

Create a key in API & Webhooks. A key belongs to a user and can do what a signed-in user can do. Send it on every call:

Authorization: Bearer apk_...

The key is shown once at creation and stored as a SHA-256 hash. Revoke any time; revocation takes effect on the next request.

Endpoints

Method & pathWhat it does
GET /healthzLiveness check — no auth. {"ok":true,"app":"restock","version":"3.2.0"}
GET /api/pingVerify a key
GET /api/skusThe catalog. ?q= searches SKU/name · ?supplier= · `?active=01 · ?limit=` (default 500, max 1000)
GET /api/reorder-reportThe money endpoint. `?status=below\near\healthy\flagged\all (default flagged` = below + near, exactly what the report screen shows)
POST /api/sales-historyIngest sales history as JSON rows — the CSV import's equivalent
POST /api/stock-levelsSet on-hand stock per SKU — what a till sends after a sale (3.2.0)
GET /api/openapi.jsonOpenAPI 3 spec — no auth

The reorder report

curl -H "Authorization: Bearer apk_..." \
  "https://your-install/api/reorder-report"
{
  "today": "2026-08-06",
  "forecast_window_days": 90,
  "target_cover_days": 30,
  "counts": { "below": 5, "near": 3, "healthy": 6 },
  "est_cost_cents": 677880,
  "skus": [
    {
      "sku": "HTS-0002", "name": "Galvanized Deck Screws 4x40 (box)",
      "current_stock": 88, "lead_time_days": 7, "safety_stock_days": 5,
      "avg_daily_demand": 8.4111, "reorder_point": 100.9333,
      "days_of_stock": 10.46, "suggested_qty": 275,
      "est_cost_cents": 189750, "status": "below",
      "moq": 200, "case_pack": 25, "forecast_model": "window",
      "needed_qty": 268, "model_used": "window", "event_extra_units": 0, "is_kit": false
    }
  ]
}

Since 3.2.0 each SKU also carries its supplier's order rules and the forecast it planned with. Since 3.3.0 it also carries price_cents, the selling price you set on the product (0 when none), so your own agent can work out the margin on what it suggests. suggested_qty is what to order: needed_qty (the cover you aim for, less stock and what is on order) raised to the supplier's minimum moq and rounded up to whole cases of case_pack. forecast_model is the model chosen for the product (window, weighted or last_year); model_used is the one applied, which is window when a product set to last_year has no sales a year back yet. event_extra_units is the demand that planned events add inside the lead time and cover. A kit (is_kit) is never suggested itself: its sales count as demand on its parts.

Results are ordered by urgency — below first, then near, and within each band the SKU with the fewest days of cover first. That is the order you should buy in, and it is the same order the report screen shows.

status is below (at or under the reorder point), near (within 25% above it), or healthy. days_of_stock is null when there is no measurable demand — infinite cover, not zero.

Ingest sales history

curl -X POST https://your-install/api/sales-history \
  -H "Authorization: Bearer apk_..." -H "Content-Type: application/json" \
  -d '{"rows": [{"date": "2026-08-01", "sku": "HTS-0002", "qty": 12},
                {"date": "2026-08-02", "sku": "HTS-0002", "qty": 9}]}'

Validation is deliberately identical to the CSV importer's — same strict YYYY-MM-DD calendar check, same refusal of unknown SKUs, same per-row reasons — so a row the CSV path would reject cannot slip in through the API. Good rows are written even when others fail; the response tells you exactly which failed and why:

{
  "imported": 30, "skipped": 2,
  "errors": [{ "row": 3, "reason": "Unknown SKU \"NOPE-999\" — add the product first." }],
  "crossed_reorder_point": [ { "sku": "HTS-0009", "status": "below", "suggested_qty": 102 } ]
}

crossed_reorder_point is the useful bit: the SKUs that this ingestion pushed under the line. Feed it straight into whatever raises purchase orders.

Rows are added by default. Send "replace": true and the request's rows are totaled per SKU per date and replace what is stored for those SKU-days — the CSV importer always works this way (since 3.1.4), so re-sending an overlapping export counts it once. The response carries replaced_days: how many stored SKU-days were replaced. With "replace": true a qty of 0 is accepted and clears that SKU-day (a till sends it when a refund takes a day back to nothing); without it, 0 is refused. A negative qty is always refused. When **Settings → Stock follows sales** is on, the response also carries stock_changed: how many products' stock moved.

The key's user needs permission to import sales (a member or an administrator). A viewer's key is refused with 403, as it is in the browser and over MCP (3.2.0; before, the REST door did not check the role).

Set stock levels (3.2.0)

curl -X POST https://your-install/api/stock-levels \
  -H "Authorization: Bearer apk_..." -H "Content-Type: application/json" \
  -d '{"rows": [{"sku": "HTS-0002", "on_hand": 88}, {"sku": "HTS-0009", "on_hand": 14}]}'
{ "updated": 1, "unchanged": 1, "skipped": 0, "errors": [], "crossed_reorder_point": [] }

Each known SKU's current stock is set to on_hand — not added to. on_hand is a whole number from 0 to 100,000,000; unknown SKUs and bad figures are refused per row with a reason, and good rows are written. Each product whose figure changed gets a stock.set line in the audit trail naming the key's user, and a SKU this pushes under its reorder point fires sku.reorder_point, as a hand correction does. Same permission as a hand correction: a member or an administrator.

This is what Vendra 3.2.0 sends after every sale, refund, delivery and correction once it is connected; any till or script can do the same.

Webhooks

Add receiver URLs in API & Webhooks. One event:

  • sku.reorder_point — a SKU crossed under its reorder point

It is a crossing, not a state. It fires on the ingestion that takes a SKU from fine to below the line, and does not fire again on later ingestions while it stays below. One alert per problem instead of a stream of noise. Both ingestion paths fire it: the CSV import and POST /api/sales-history.

Each delivery is an HTTP POST with a JSON body and:

X-Restock-Event: sku.reorder_point
X-Restock-Signature: sha256=<hex HMAC-SHA256 of the RAW body, keyed with the endpoint's secret>

Verify the signature over the raw body before trusting a payload:

$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, $YOUR_SECRET);
if (!hash_equals($expected, $_SERVER['HTTP_X_RESTOCK_SIGNATURE'] ?? '')) { http_response_code(401); exit; }

Delivery is one attempt with a 5-second timeout. The Recent deliveries log on the settings page shows every attempt and response code. Design receivers to be idempotent.

Power Automate (and Logic Apps)

  1. Instant trigger via webhook: create a flow with the **"When an HTTP request is received"** trigger, paste its URL into Restock's webhooks, and raise a purchase task from body('...')?['product'].
  2. Custom connector: Power Automate → Data → Custom connectors → **Import an OpenAPI file** → point it at /api/openapi.json. Set security to Bearer.

MCP — the agent endpoint (new in 3.0)

Restock speaks MCP (Model Context Protocol) on one route, so an assistant can watch your stock and prepare your buying without anyone building a bridge first.

POST /mcp
Authorization: Bearer apk_...          ← the SAME revocable key the REST API uses
Content-Type: application/json

Stateless streamable-HTTP: one JSON-RPC message per request, no session, no SSE. A GET gets 405 with Allow: POST, as the transport requires — and that answer is unauthenticated, so a client probing for MCP support learns you speak it before it has a key.

Connect it to an assistant

Mint the key in the app first: Settings → API keys. Choose Read only when the assistant should answer questions but never change anything — the endpoint then lists only the read tools and refuses the rest by name, so a careless prompt cannot write. Full access behaves as before.

Every client needs the same three facts, and nothing in the handshake is vendor-specific:

The addresshttps://your-install/mcp
The keyheader Authorization: Bearer apk_xxxx
The transportMCP over streamable HTTP, stateless

Claude — one command, or the same URL and header as a custom connector in the desktop and web apps:

claude mcp add --transport http restock https://your-install/mcp \
  --header "Authorization: Bearer apk_xxxx"

ChatGPT and the OpenAI API — one entry in the Responses API's tools array (in ChatGPT itself, the same URL and key go in as a connector):

{
  "type": "mcp",
  "server_label": "restock",
  "server_url": "https://your-install/mcp",
  "authorization": "apk_xxxx",
  "require_approval": "never"
}

**Every other client spells the same three facts differently — copy the shape from its own documentation, not from here.** VS Code is the clearest example of why: its configuration reference (read 6 September 2026) puts servers in .vscode/mcp.json under a "servers" object — *"an object that maps server names to their configurations"*. Pasted as-is, a block written for another client will not load there. The id, the URL and the token are what travel; the JSON around them belongs to whichever client you are configuring.

A local model, n8n, or your own code — n8n's MCP Client node takes the URL and the same Authorization: Bearer header; a model running on your own machine reaches it through any MCP client, so nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:

curl -X POST https://your-install/mcp \
  -H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet: Restock implements the protocol, not an integration with a particular vendor.

The tools

ToolWrites?What it does
list_productsnoEvery product with Restock's own forecast: reorder point, days of stock left, suggested order quantity, status. Filter by status, below_reorder, supplier, q.
product_detailnoOne product with its forecast and recent sales history.
adjust_stockYESCorrects a stock level — set_to for a counted figure or delta to add/remove.
ingest_salesYESAdds daily sales rows so the forecast reflects reality.
reorder_reportnoWhat to buy now, in the report screen's urgency order, with the estimated cost in integer cents.

status is below (at or under the reorder point), near (within 25% above it) or healthy — the same three words the report screen, the REST API and this document use. An agent that asks for a status Restock actually returns gets an answer, and below_reorder: true means what it says: only stock at or under the reorder point, never the near band.

What the endpoint refuses, and why

  • No tool computes demand. Every forecast figure comes from the same Forecast::metrics() the report screen uses, so an agent can never quote a reorder point Restock disagrees with.
  • adjust_stock writes through one guarded path. It calls the same set_product_stock() the product form, the REST API and PO receiving call — one audit entry and one reorder-crossing alarm, whichever door the change came through.
  • Arguments must be single values. Anything nested is rejected, except rows on ingest_sales, which is the one allowlisted nested argument — the same rule Api::body() applies to the REST API.
  • Unknown arguments are refused by name, so an agent that typos stauts is told so rather than silently getting unfiltered results.
  • Out-of-enum values are refused, with the valid options listed.
  • Negative stock is refused, as is sending both set_to and delta.
  • Roles apply exactly as they do in the browser. A key belonging to a viewer is refused adjust_stock and ingest_sales — a different answer for the same role would make the agent path a privilege hole.

Every MCP write lands in the audit trail beside the browser and API writes, naming the key's user as the actor and recording via: mcp.

Example

curl -X POST https://your-install/mcp \
  -H "Authorization: Bearer apk_..." -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"list_products","arguments":{"below_reorder":true}}}'

Limits, honestly

  • API keys carry their owner's role (viewer / member / admin), so a key is never allowed to do something its owner could not do in the browser. A viewer's key reads and nothing more.
  • Purchase orders are raised and progressed in the UI, not over MCP or REST. Committing money to a supplier is a decision worth a human, and the reorder report already hands an agent everything needed to recommend one.
  • One delivery attempt per webhook event (log + idempotent receivers, not a retry queue).
  • POST /api/sales-history accepts at most 50,000 rows per request, matching the CSV cap.
  • There is no POST /api/skus: products are created through the UI or the CSV import, where the dry run can show you what you are about to change. The API is for reading the plan and feeding it demand.
  • GET /api/skus caps at 1,000 rows.

Connect it to Zapier, Make or n8n

Restock talks to other tools without a plugin: it sends a webhook (an HTTPS message) the moment something happens, and it accepts calls to the API above. Zapier, Make and n8n can use both.

Before you start

  • In Restock, open API & Webhooks. Mint an API key (choose Read only if the other tool only reads), and add a webhook when you have its URL (below).
  • Use your install's full address, for example https://restock.yourcompany.com.

When something happens in Restock: send it to another tool

  1. In the other tool, create the trigger that gives you a URL to receive webhooks: - Zapier: the "Webhooks by Zapier" app, trigger Catch Hook (Webhooks by Zapier needs a paid Zapier plan). - Make: module Webhooks → Custom webhook. - n8n: the Webhook node.
  2. Copy that URL into Restock under API & Webhooks, and choose the event or events below.
  3. Make the event happen once in Restock, so the other tool receives a sample and you can map its fields.
EventFires when
sku.reorder_pointA SKU crossed under its reorder point

Every delivery carries X-Restock-Signature, an HMAC-SHA256 signature over the raw body. Zapier and Make accept the delivery without checking it, so keep the webhook URL private. In n8n you can check it in a Code node before using the data.

When something happens elsewhere: act in Restock

  • Zapier: "Webhooks by Zapier", action POST (or Custom Request).
  • Make: module HTTP → Make a request.
  • n8n: the HTTP Request node.

Send the request to your install's address plus the path from the Endpoints table above, with the header Authorization: Bearer <your API key> and a JSON body. A read-only key cannot write.

The endpoints that change something:

  • POST /api/sales-history: Ingest sales history as JSON rows — the CSV import's equivalent
  • POST /api/stock-levels: Set on-hand stock per SKU — what a till sends after a sale (3.2.0)

Example recipes

  • sku.reorder_point → a row in Google Sheets: webhook trigger on sku.reorder_point (A SKU crossed under its reorder point), then the Google Sheets "Create row" action with the fields you want.
  • sku.reorder_point → a message in Slack or Teams: webhook trigger on sku.reorder_point (A SKU crossed under its reorder point), then the Slack or Microsoft Teams "Send message" action.
  • A new form entry or row elsewhere → Restock: the other tool's trigger (for example a new Google Sheets row or a form submission), then an HTTP action calling POST /api/sales-history (Ingest sales history as JSON rows — the CSV import's equivalent).

These recipes use only the events and endpoints documented above. We have not run them inside Zapier, Make or n8n ourselves; Restock's own automated tests check its API.

← Back to Restock · Manual · Quickstart · Test run