Orders API & Webhooks
Connect a warehouse, fulfilment service, or AI agent to your orders — read them, mark them paid or shipped, and get a signed webhook for every change.
Tiny Shops gives external systems two things:
- An HTTP API to read an order, change its status, and ship it with tracking.
- A webhook that POSTs every new order and every status or tracking change to a URL you choose.
Both use the same order shape, so anything a webhook told you can be re-read with the API field for field.
Читаете по-русски? See the Russian version of this page.
Want an agent to manage more than orders? See Connect an AI agent.
1. Create an API token
Open API tokens
In the dashboard, go to Settings → API tokens (/settings/api-tokens). Only the store owner sees this page.
Create a token with the right scope
- Read —
get_orderonly. - Write — everything on this page:
get_order,set_order_status,ship_order.
Copy the token (tsh_live_…) when it's shown. It isn't shown again.
Every request sends it as a Bearer token:
Authorization: Bearer tsh_live_…2. The three calls
All calls are POST https://mcp.tiny-shops.com/v1/<call> with a JSON body. The same calls are available as MCP tools with the same names on https://mcp.tiny-shops.com/mcp.
Every response has the same envelope:
{ "ok": true, "data": { … } }
{ "ok": false, "error": { "code": "not_found", "message": "Order '42' not found" } }| HTTP | error.code | Meaning |
|---|---|---|
| 400 | invalid_input | The body doesn't match the call's input |
| 401 | invalid_token | Missing, malformed, expired, or revoked token |
| 403 | forbidden | The token's scope is too narrow (for example a read token calling ship_order) |
| 404 | not_found | No such order in your store — orders of other stores are never visible |
| 404 | unknown_tool | No such call |
| 429 | rate_limited | Too many calls; wait for Retry-After seconds |
Rate limits are per token: 100 read calls and 30 write calls per minute.
get_order — read one order
curl -X POST https://mcp.tiny-shops.com/v1/get_order \
-H "Authorization: Bearer $TINYSHOPS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"orderId": 1042}'data is the order object.
set_order_status — change the status
curl -X POST https://mcp.tiny-shops.com/v1/set_order_status \
-H "Authorization: Bearer $TINYSHOPS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"orderId": 1042, "status": "PROCESSING"}'data is { "orderId", "status", "order" }. Status changes run the same store rules as the dashboard — for example, cashback configured to be granted at a given status.
| Status | Meaning |
|---|---|
PENDING | New order, not yet confirmed or paid |
PROCESSING | Paid / being prepared. "Mark paid" means setting this status. |
SHIPPED | Handed to the carrier — prefer ship_order, which also saves tracking |
DELIVERED | Received by the shopper |
CANCELLED | Cancelled |
ship_order — mark shipped with tracking
curl -X POST https://mcp.tiny-shops.com/v1/ship_order \
-H "Authorization: Bearer $TINYSHOPS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"orderId": 1042, "carrier": "DHL", "trackingNumber": "JD014600003SE", "trackingUrl": "https://dhl.com/track?id=JD014600003SE", "notify": true}'| Field | Required | Notes |
|---|---|---|
orderId | yes | |
trackingUrl | one of these three | Must start with http:// or https:// |
carrier | one of these three | Up to 100 characters |
trackingNumber | one of these three | Up to 100 characters |
notify | no, default false | true sends the shopper the same Telegram message as the dashboard's Ship & Notify |
The status becomes SHIPPED and the tracking fields are saved. With notify: true the shopper gets your store's shipping message; the Track Order button appears only when there is a trackingUrl, otherwise the carrier and tracking number are written in the message.
data is { "order", "notified", "notifyFailureReason" }. notified is absent when you didn't ask to notify, and false with a reason (bot_not_configured, unreachable, send_failed) when the message couldn't be sent — the order is shipped either way.
3. Webhooks
Set it up
Go to Settings → Order webhooks, enter a public https:// URL, and save. You get a signing secret (whsec_…) once — store it. Lost it? Click Rotate secret; the old one stops working immediately. The page also shows recent deliveries.
Use Send test event to POST a signed sample order.created synchronously. Test envelopes contain "test": true and sample data; they never create an order. Your endpoint should still verify the signature and return a 2xx.
Each store has one webhook URL.
Events
| Event | When |
|---|---|
order.created | Every new order, from any checkout or payment method |
order.updated | The status, tracking, or items changed — in the dashboard, through the API, or through MCP. Setting an order to the status it already has sends nothing |
The request
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: TinyShops-Webhooks/1
X-TinyShops-Event: order.updated
X-TinyShops-Delivery: 3b0c4f7e-5a41-4d8e-9f0e-2d6b1c1a9e27
X-TinyShops-Signature: 5f1c…e9a2
{
"id": "3b0c4f7e-5a41-4d8e-9f0e-2d6b1c1a9e27",
"event": "order.updated",
"createdAt": "2026-09-23T10:15:02.114Z",
"data": { …the order object… }
}X-TinyShops-Signature— lowercase hex HMAC-SHA256 of the raw request body, keyed with your signing secret.X-TinyShops-Deliveryand the body'sid— the delivery id. It is the same on every retry of that delivery.
Verify the signature
Compute the HMAC over the raw bytes you received, before any JSON parsing, and compare in constant time.
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';
const app = express();
app.post(
'/tinyshops/orders',
express.raw({ type: 'application/json' }),
(req, res) => {
const expected = createHmac('sha256', process.env.TINYSHOPS_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const received = req.get('X-TinyShops-Signature') ?? '';
const valid =
received.length === expected.length &&
timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!valid) return res.status(401).end();
const event = JSON.parse(req.body.toString('utf8'));
res.status(204).end(); // answer fast, then do the work
handleOrderEvent(event);
},
);import hashlib, hmac, json, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["TINYSHOPS_WEBHOOK_SECRET"].encode()
@app.post("/tinyshops/orders")
def tinyshops_orders():
expected = hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()
received = request.headers.get("X-TinyShops-Signature", "")
if not hmac.compare_digest(received, expected):
abort(401)
event = json.loads(request.get_data())
handle_order_event(event)
return "", 204Retries
- A delivery succeeds when your endpoint answers any 2xx within 10 seconds.
- Otherwise it is retried up to 5 more times, after 1 minute, 5 minutes, 30 minutes, 2 hours, and 6 hours. After the sixth failed attempt it is marked failed and shown on the settings page.
- Events are sent within about a minute of the change.
- Redirects are not followed; the URL must answer directly.
Idempotency and ordering
- Use the delivery ID (
X-TinyShops-Deliveryor bodyid) to ignore retries. For order state, keep the newestdata.updatedAt. Do not deduplicate only by order ID and status—tracking or items can change without a status change. - Setting an order to the status it already has sends no event.
- Missed something? Call
get_order— it returns exactly the object a webhook would have carried.
The order object
{
"id": 1042,
"status": "SHIPPED",
"paymentMethod": "Stripe",
"totalPrice": 459000,
"tipAmount": null,
"currency": "USD",
"telegramUserId": "5566778899",
"shippingAddress": { "name": "Ivan Petrov", "phone_number": "+15551234567" },
"checkoutData": [{ "id": "f1", "label": "Address", "type": "text", "value": "Main st. 1" }],
"items": [
{
"productId": "sneaker-01",
"title": "Running Sneakers",
"quantity": 1,
"price": 459000,
"size": "42",
"variantOptionTitle": null,
"variantValue": null
}
],
"trackingUrl": "https://dhl.com/track?id=JD014600003SE",
"carrier": "DHL",
"trackingNumber": "JD014600003SE",
"createdAt": "2026-09-23T09:58:40.512Z",
"updatedAt": "2026-09-23T10:15:02.101Z"
}| Field | Notes |
|---|---|
totalPrice, items[].price, tipAmount | Minor units of currency (cents; whole stars for XTR) |
currency | The store's currency code |
telegramUserId | The shopper's Telegram user id, as a string (it can exceed 2^53) |
shippingAddress | Shipping details collected by the payment flow (for example Telegram's order info), or null |
checkoutData | The checkout form rows the shopper filled in, or null |
trackingUrl, carrier, trackingNumber | null until set |
Подключение ИИ-агента (RU)
Подключите Cursor, Claude или другой MCP-клиент к одному магазину Tiny Shops через OAuth либо используйте API-токен для скрипта.
API заказов и вебхуки (RU)
Подключите склад, службу фулфилмента или ИИ-агента к заказам — читайте их, отмечайте оплаченными или отправленными и получайте подписанный вебхук о каждом изменении.