Build your own
storefront on top.
Every order, product, and tracking event VATFlow holds is reachable over a plain JSON REST API. Point a custom UI at it and keep the classification, invoicing, and fulfilment running underneath — you write the front, we settle the VAT.
Keys are scoped and revocable. Every webhook is signature-verified.
$ curl -s api.vatflow.com/v1/orders -d @order.json
201 Created
number 1042
vat.placeOfSupply "DE"
vat.scheme "oss"
vat.rate "0.19"
vat.total "24.10" # EUR, decimal-exact
invoice.url "/v1/invoices/inv_8f2a.pdf"One endpoint per thing you already manage.
The same data the dashboard reads, addressable over HTTP. List, fetch, and write with predictable JSON, cursor pagination, and idempotency keys on every create.
/v1/ordersOrders
Create an order and the VAT engine classifies it inline — place of supply, scheme, rate, totals. List, fetch, fulfil, and refund.
/v1/productsProducts
Products with the full size/colour variant matrix, prices in minor units, and slugs for your storefront routes.
/v1/inventoryInventory
Per-variant stock across every warehouse. Read levels, post adjustments — each write lands in the audit log.
/v1/vatVAT
Classify a hypothetical order without persisting it. Pure read of the engine: scheme, rate, and the resolved place of supply.
/v1/shippingShipping
Rate-shop your configured carriers, create a label, and pull tracking events — the same events that render on your tracking page.
/v1/invoicesInvoices
Compliant invoice and credit-note PDFs, addressable by id. Generated the moment an order is classified or returned.
Post an order, get the VAT back in the response.
You send the ship-to country, the buyer type, and the lines. The engine resolves place of supply, commits to one of the four schemes, applies the rate, and returns it — before you ever take payment.
$ curl https://api.vatflow.com/v1/orders \
-H "Authorization: Bearer sk_live_3Qf9...c7a1" \
-H "Content-Type: application/json" \
-d '{
"shipTo": { "country": "DE", "postcode": "10115" },
"buyer": { "type": "consumer" },
"currency": "EUR",
"lines": [
{ "sku": "TEE-BLK-M", "qty": 1, "unitNet": "20.25" }
]
}'201 Created
{
"id": "ord_8f2a41",
"number": "1042",
"status": "classified",
"currency": "EUR",
"vat": {
"placeOfSupply": "DE",
"scheme": "oss",
"rate": "0.19",
"net": "20.25",
"amount": "3.85",
"total": "24.10"
},
"invoice": { "url": "/v1/invoices/inv_8f2a.pdf" }
}Create and update from your own systems.
The write endpoints reuse the exact same services the dashboard does — same VAT classification, same inventory ledger, same audit log. Each is gated on a scoped key; a wrong or missing scope is a 403, a missing key a 401.
| Endpoint | Scope | |
|---|---|---|
POST /api/public/v1/ordersCreate an order from { customer, lines, shippingAddress }. The VAT engine classifies it inline and returns the order with scheme, rate, and totals. | orders:write | Create an order from { customer, lines, shippingAddress }. The VAT engine classifies it inline and returns the order with scheme, rate, and totals. |
POST /api/public/v1/productsCreate a product with its full size/colour variant matrix and net prices. | products:write | Create a product with its full size/colour variant matrix and net prices. |
PATCH /api/public/v1/products/:idUpdate an existing product — title, status, pricing, or variants. | products:write | Update an existing product — title, status, pricing, or variants. |
POST /api/public/v1/inventory/adjustPost a stock adjustment for a variant at a warehouse. Each write lands in the audit log. | inventory:write | Post a stock adjustment for a variant at a warehouse. Each write lands in the audit log. |
Mint a key with the matching write scope in Settings → API keys. Keep write keys server-side.
Read the catalogue, render it however you like.
List endpoints return a consistent envelope — an object: "list" with cursor pagination via has_more and a starting_after cursor. Every variant carries its size, colour, and net price, so your storefront grid maps straight from the payload.
$ curl "https://api.vatflow.com/v1/products?limit=2" \
-H "Authorization: Bearer sk_live_3Qf9...c7a1"
{
"object": "list",
"has_more": true,
"data": [
{
"id": "prod_7c10",
"slug": "organic-cotton-tee",
"title": "Organic Cotton Tee",
"variants": [
{ "sku": "TEE-BLK-M", "size": "M", "colour": "Black", "priceNet": "20.25" },
{ "sku": "TEE-BLK-L", "size": "L", "colour": "Black", "priceNet": "20.25" }
]
}
]
}Scoped API keys, revocable in a click.
Every request is a bearer token in the Authorization header. Mint as many keys as you need, scope each to the resources it touches, and revoke any one without rotating the rest.
Two prefixes
Live keys start sk_live_, test keys sk_test_. The prefix tells you which environment you are about to hit.
Least privilege
Scope a key to read:products or write:orders. A storefront key never needs to touch inventory writes.
Rotate cleanly
Issue the replacement, deploy, then revoke the old key. Both work during the overlap.
Server-side only
Secret keys never ship to the browser. Your custom UI talks to your backend; your backend holds the key.
Authorization: Bearer sk_live_3Qf9...c7a1Fixed ceilings, honest headers.
Limits are per key, per minute, with a short burst allowance. Every response carries your remaining budget so you can back off before you hit 429.
| Plan | Sustained | Burst |
|---|---|---|
| Free | 60 req / min | 120 req |
| Growth | 600 req / min | 1,200 req |
| Scale | 3,000 req / min | 6,000 req |
Read X-RateLimit-Remaining and X-RateLimit-Reset on every response. When you exhaust the budget you get a 429 with a Retry-After header.
HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1718900042
{ "error": "rate_limited",
"message": "Slow down — retry in 12s." }Signature-verified, or rejected with 401.
VATFlow pushes order and shipment events to your endpoint, signed with your endpoint secret. The X-VATFlow-Signature header is an HMAC-SHA256 of the raw body; the X-VATFlow-Event header names the event. Verify the signature before you do any other work — an unverified webhook is a 401, full stop.
POST /your/webhook/endpoint
X-VATFlow-Event: shipment.status.updated
X-VATFlow-Signature: 4f0c9b1e7a2d...e9b2 # HMAC-SHA256 of the raw body
{
"event": "shipment.status.updated",
"data": {
"orderId": "ord_8f2a41",
"awb": "JD0002182734",
"carrier": "dhl_express",
"status": "in_transit",
"occurredAt": "2026-06-20T11:32:04Z"
}
}Compute the HMAC over the exact bytes you received — do not re-serialize the JSON first. A non-2xx response makes us retry with exponential backoff, so make your handler idempotent. Anything you cannot verify is a 401 — never act on the payload.
import crypto from "node:crypto";
// Compute HMAC-SHA256 of the EXACT raw body string with your endpoint secret.
const expected = crypto
.createHmac("sha256", process.env.VATFLOW_WEBHOOK_SECRET)
.update(req.rawBody) // the bytes as received — do not re-serialize
.digest("hex");
const signature = req.headers["x-vatflow-signature"];
const ok =
signature &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
// Reject anything we can't prove came from VATFlow — return 401.
if (!ok) return res.status(401).end();
// Safe to act on now.
if (req.headers["x-vatflow-event"] === "shipment.status.updated") {
await recordTrackingEvent(JSON.parse(req.rawBody).data);
}order.createdAn order was placed — through the storefront, the dashboard, or a POST to the API. The VAT is already classified on the payload.
order.paidAn order was paid: a storefront checkout confirmed, or an order marked paid in the back office. It now moves to fulfilment.
shipment.status.updatedA carrier reported a new tracking event — in transit, out for delivery, delivered — the same event that renders on your tracking page.
The whole surface, machine-readable.
Every read and write endpoint — with its security scheme, request bodies, and response schemas — is described in a single OpenAPI 3.1 document. Point your generator at it for a typed client, or load it into Postman and start poking.
$ curl -s api.vatflow.com/api/public/v1/openapi.json | jq .openapi
"3.1.0"
{
"openapi": "3.1.0",
"info": { "title": "VATFlow Public API", "version": "1.0.0" },
"components": {
"securitySchemes": {
"apiKey": { "type": "http", "scheme": "bearer" }
}
},
"paths": { "/orders": { ... }, "/products": { ... } }
}Your front end, our tax engine underneath.
Create an account, mint a test key, and post your first order. The VAT comes back in the response — no carrier credentials, no card required.