Deliora API, as shipped in the download
Deliora API (v2.0)
Deliora's REST API is the store's own read model: the orders, customers and licences it has already fulfilled, plus the two write operations a shop actually automates — minting redemption codes for a deal site, and receiving signed webhooks when money moves. Every number it returns is computed by the same code the admin pages use, so a nightly report and the dashboard cannot disagree.
Authentication
Create a key in Admin → API & webhooks (/admin/api-settings). It is shown once.
Authorization: Bearer apk_<40 hex chars>
A key acts as the user it belongs to. /api/* is CSRF-exempt by design: the key is the credential, and browsers never send it implicitly.
curl -H "Authorization: Bearer apk_xxxx" https://store.example.com/api/ping
# {"ok":true,"user":"Admin","app":"deliora","version":"2.0.0"}
An absent or revoked key returns 401 {"error":"unauthorized"}. Nothing else is exposed unauthenticated except /healthz.
Endpoints
| Method | Path | Notes | |||||
|---|---|---|---|---|---|---|---|
| GET | /api/ping | Verify a key | |||||
| GET | /api/orders | `?status=paid\ | refunded\ | pending · ?provider=lemonsqueezy\ | stripe\ | demo\ | redeem` |
| GET | /api/orders/{ref} | One order by its reference, with customer + items | |||||
| GET | /api/customers | Buyers with order counts | |||||
| GET | /api/licenses | `?status=active\ | revoked · ?sku=<slug>` | ||||
| GET | /api/redeem-codes | Batch summary: [{batch, status, n}] | |||||
| POST | /api/redeem-codes | Mint codes for a deal site (below) | |||||
| GET | /api/stats | Store totals in one call | |||||
| GET | /api/openapi.json | OpenAPI 3.0.3 — imports as a Power Automate / Zapier custom connector | |||||
| GET | /healthz | Unauthenticated {"ok":true,"app":"deliora","version":"2.0.0"} |
Orders
curl -H "Authorization: Bearer apk_xxxx" 'https://store.example.com/api/orders?status=paid'
Each order carries `order_ref, provider, status, amount_cents, currency, created_at, customer, items`. Money is always in cents as an integer — never a float, so nothing rounds in transit.
Stats
{
"revenue_cents": 99400,
"orders_paid": 10,
"orders_refunded": 1,
"customers": 10,
"licenses_active": 13,
"codes_unredeemed": 0,
"notify_confirmed": 0
}
revenue_cents counts paid orders and excludes refunds — the same rule the dashboard applies.
Minting redemption codes
For AppSumo-style deals: mint a batch, upload the CSV to the deal site, and let buyers redeem at /redeem. Codes are AS-XXXX-XXXX-XXXX from an alphabet with no 0/O/1/I/L, so a code read aloud over the phone cannot be mistyped.
curl -X POST https://store.example.com/api/redeem-codes \
-H "Authorization: Bearer apk_xxxx" -H 'Content-Type: application/json' \
-d '{"sku":"bookingkit","edition":"single","count":2}'
{ "batch": "api-20260806", "count": 2,
"codes": ["AS-YNTJ-85CA-XV4A", "AS-XZ93-HACM-GNQ3"] }
The edition must exist for that SKU or the request is refused — a code that cannot be fulfilled is worse than no code. Redemption itself is race-proof: the claim is a single UPDATE … WHERE status='new', so two people submitting the same code at the same instant cannot both receive a licence, and a buyer re-submitting their own code gets the same order back rather than a second one.
Webhooks
Add a receiver in Admin → API & webhooks. Events:
| Event | Fires when |
|---|---|
order.paid | A payment is confirmed (any provider) |
order.refunded | A refund is processed |
code.redeemed | A deal code is exchanged for a licence |
license.activated | A licence key is activated on an install |
Each delivery carries the event name and an HMAC of the raw body:
X-Deliora-Event: order.paid
X-Deliora-Signature: <hex hmac-sha256 of the exact body, keyed with your webhook secret>
Verify it before trusting the payload — recompute over the raw body, not a re-encoded copy:
$raw = file_get_contents('php://input');
$mine = hash_hmac('sha256', $raw, $secret);
if (!hash_equals($mine, $_SERVER['HTTP_X_DELIORA_SIGNATURE'] ?? '')) { http_response_code(401); exit; }
One attempt per delivery with a 5-second timeout: Deliora will not retry, because a store that retries a payment notification into a slow endpoint eventually sends it twice. Failed deliveries are listed in Admin → Outbox for you to replay deliberately.
Errors
| Status | Meaning |
|---|---|
| 400 | Body is not a JSON object |
| 401 | Missing, malformed or revoked key |
| 404 | No such order / licence |
| 422 | A field is wrong — the message names it |
A field given as a list or an object where a single value belongs is refused with `422 {"error":"validation","detail":"<field> must be a single text or number value, not a list or object."} rather than being coerced. This is deliberate: silently storing the string Array` would corrupt your records in a way no error message would ever reveal.
What the API deliberately does not do
- No order creation. Orders come from a payment provider's webhook or a redeemed code — never from a bare API call, so revenue always traces back to a payment or a deal code.
- No secrets. SMTP passwords, provider webhook secrets, password hashes and key hashes are never serialised, including in the admin JSON backup.
MCP — the agent door (new in 3.0)
Deliora speaks the Model Context Protocol at POST /mcp, so Claude, ChatGPT agents, n8n's AI nodes or your own code can use your store instead of merely reading it. Same bearer key, same roles: a support key sees; only an admin key can mint codes.
curl -s -X POST https://store.example.com/mcp \
-H "Authorization: Bearer apk_xxxx" -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"revenue_stats","arguments":{}}}'
| Tool | Writes? | What it does |
|---|---|---|
list_orders | no | Orders newest first (status/provider filters) |
get_order | no | One order by reference, items included |
list_customers | no | Customers with paid-order counts |
list_licenses | no | Licenses (optional sku filter) |
revenue_stats | no | Revenue, orders, customers, active licenses, unredeemed codes |
mint_redeem_codes | yes | Mints deal codes — admin keys only; codes are worth money |
Deliberately absent: refunds (they live at the payment processor), product/price editing (the catalog stays a human decision), and any tool that emails a customer.
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 address | https://store.example.com/mcp |
| The key | header Authorization: Bearer apk_xxxx |
| The transport | MCP 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 deliora https://store.example.com/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": "deliora",
"server_url": "https://store.example.com/mcp",
"authorization": "apk_xxxx",
"require_approval": "never"
}
A client that keeps its servers in a config file — Own Your AI, editors like VS Code and Cursor, and most desktop clients read a list like this:
{
"mcpServers": [
{ "id": "deliora", "name": "Deliora", "url": "https://store.example.com/mcp",
"token": "apk_xxxx", "enabled": true }
]
}
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://store.example.com/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: Deliora implements the protocol, not an integration with a particular vendor.
Operational endpoints (new in 3.0)
| Endpoint | Guard | What it does |
|---|---|---|
GET /backup/scheduled?t=<token> | ops token (Admin → Security) | Nightly redacted JSON into data/backups/, keeps 14 |
GET /admin/statement/{YYYY-MM} | admin session | Accountant-ready monthly statement PDF |
GET /admin/orders/{id}/receipt.pdf | staff session | Order receipt PDF |
GET /portal/me/receipt/{id} | buyer portal session | The customer's own receipt |
GET /admin/customers/{id}/gdpr.json | admin session | Subject-access export |