Ownware
Home›Deliora›API
Deliora · API

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

MethodPathNotes
GET/api/pingVerify 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/customersBuyers with order counts
GET/api/licenses`?status=active\revoked · ?sku=<slug>`
GET/api/redeem-codesBatch summary: [{batch, status, n}]
POST/api/redeem-codesMint codes for a deal site (below)
GET/api/statsStore totals in one call
GET/api/openapi.jsonOpenAPI 3.0.3 — imports as a Power Automate / Zapier custom connector
GET/healthzUnauthenticated {"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:

EventFires when
order.paidA payment is confirmed (any provider)
order.refundedA refund is processed
code.redeemedA deal code is exchanged for a licence
license.activatedA 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

StatusMeaning
400Body is not a JSON object
401Missing, malformed or revoked key
404No such order / licence
422A 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":{}}}'
ToolWrites?What it does
list_ordersnoOrders newest first (status/provider filters)
get_ordernoOne order by reference, items included
list_customersnoCustomers with paid-order counts
list_licensesnoLicenses (optional sku filter)
revenue_statsnoRevenue, orders, customers, active licenses, unredeemed codes
mint_redeem_codesyesMints 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 addresshttps://store.example.com/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 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)

EndpointGuardWhat 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 sessionAccountant-ready monthly statement PDF
GET /admin/orders/{id}/receipt.pdfstaff sessionOrder receipt PDF
GET /portal/me/receipt/{id}buyer portal sessionThe customer's own receipt
GET /admin/customers/{id}/gdpr.jsonadmin sessionSubject-access export

← Back to Deliora · Manual · Quickstart · Test run

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →