Aexir owner data API

Read-only access to your own restaurant data: orders, daily totals, locations and menu. Version 1.

Getting started

Base URL: https://aexir.com/api (also https://europe-west2-aexir-website.cloudfunctions.net/api). Every endpoint lives under /v1.

Create an API key yourself from the POS settings (Settings, Data & API, API access; admins with access to all locations), or ask Aexir support to create one for you. A key looks like axk_ followed by 48 characters. It is shown once; keep it somewhere safe. Then:

curl -H "Authorization: Bearer axk_YOUR_KEY" \
  "https://aexir.com/api/v1/orders?from=2026-10-01&to=2026-10-07"

Add &format=csv to /v1/orders or /v1/summary/daily to get a spreadsheet-ready file instead of JSON. A CSV call returns the whole date range in one file, with no paging.

Authentication

Send the key in the Authorization header on every request:

Authorization: Bearer axk_YOUR_KEY

Endpoints

Method and pathWhat it returns
GET /v1/meWhich restaurant the key belongs to, its label, whether it includes customer data, and the limits.
GET /v1/ordersOrders placed between two dates, newest last. JSON comes one page at a time; CSV returns the whole range in one file. See parameters below.
GET /v1/orders/{id}One order by its id, or 404.
GET /v1/summary/dailyOne row per day: order count, gross, discounts, delivery fees, service charges, tips, VAT, refunds, net, and breakdowns by payment method, order type and location.
GET /v1/locationsYour branches: id, name, postcode, address, phone, opening state, which order types are on, delivery mode.
GET /v1/menuMenu items as the POS stores them: id, name, category, price, sizes with prices, inStock, the branches that list the item, description, VAT amount, meal deal flag.
GET /v1/docsThis page.

Parameters for /v1/orders

NameMeaning
from, toInclusive local days, YYYY-MM-DD. Both default to the last 7 days. Give one and the other is set to the same day. Maximum 366 days.
tzTime zone the days are measured in, default Europe/London. Any IANA name works, for example Europe/Paris. Timestamps in the response carry this offset.
statusOne of pending, Accepted, KitchenCompleted, Completed, Declined, pickedUp, awaiting_payment. Not case sensitive. Completed also matches the legacy pickedUp. Without a status filter, awaiting_payment orders (app checkouts that never paid) are left out.
orderTypeDelivery, Collection or DineIn.
locationBranch name exactly as it appears in /v1/locations (not case sensitive).
limitJSON only: orders per page, 1 to 500, default 100. Ignored with format=csv.
cursorJSON only: copy nextCursor from the previous page to get the next one. nextCursor is null on the last page. Ignored with format=csv.
formatjson (default) or csv. A CSV call returns every matching order in the range in one file.

/v1/summary/daily takes from, to, tz and format. It reads every order in the range, so keep ranges to a few months at most.

The order object

{
  "id": "Qm3kZ9...",
  "placedAt": "2026-10-08T11:50:37+01:00",
  "status": "Completed",
  "effectiveStatus": "Completed",
  "orderType": "Delivery",
  "channel": "ios-7.1.1",
  "location": "Main",
  "customer": {
    "name": "Sam Example",
    "phone": "07700 900000",
    "email": "sam@example.com",
    "address": "1 High Street, M1 1AA"
  },
  "note": "Ring the bell",
  "redacted": false,
  "lines": [
    {
      "name": "Chicken Burger",
      "size": "Large",
      "unitPrice": 8.5,
      "qty": 2,
      "extras": [
        {
          "name": "Cheese",
          "price": 0.5
        }
      ],
      "mealDeal": {
        "drink": "Coke",
        "side": "Fries",
        "price": 2
      },
      "notes": "no mayo",
      "lineTotal": 22
    }
  ],
  "rawItems": {
    "items": [
      "Chicken Burger: (8.5), Extras: Cheese(0.5),"
    ],
    "size": [
      "Large"
    ],
    "mealDeal": [
      "[\"Coke\"] [\"Fries\"]"
    ],
    "mdPrice": [
      2
    ],
    "itemNotes": [
      "no mayo"
    ]
  },
  "totals": {
    "subtotal": 22,
    "discount": 0,
    "discountRaw": 0,
    "discountType": "amount",
    "deliveryFee": 2.5,
    "serviceCharge": 0,
    "tip": 0,
    "vat": 3.67,
    "total": 24.5,
    "refund": 0
  },
  "payment": {
    "method": "Card",
    "reference": "pi_3Q..."
  }
}
FieldMeaning
placedAtWhen the order was placed, ISO 8601 with the offset of the requested tz. For app orders this is the moment payment was confirmed.
status / effectiveStatusstatus is exactly what is stored. effectiveStatus applies the POS rule that a pending, accepted or kitchen-completed order older than 12 hours counts as Completed, and maps the legacy pickedUp to Completed.
orderTypeNormalised to Delivery, Collection or DineIn whatever the app that wrote it used.
channelWhere the order came from: ios-x, android-x, web-x (customer apps), webpos-x or pos-x (your till).
customer, noteOnly present on keys with customer data on. address is filled for Delivery orders; Dine-in orders carry table instead. Till orders have no phone or email. Email is only recorded by the web ordering site and some app versions, so it is often empty.
redactedtrue when the restaurant's data retention has already blanked the personal details on this order (by default after 180 days where retention is enabled).
linesParsed from the stored item strings the same way the POS screens read them. Identical units are merged into qty. unitPrice already includes the size. lineTotal = (unitPrice + meal deal price + extras) x qty. mealDeal is null when there is none.
rawItemsThe untouched stored arrays, in case you want to parse them yourself.
totalstotal is what the customer paid (after any refund, see below). On app orders subtotal = total - deliveryFee - serviceCharge - tip + discount; on till orders subtotal is the sum of the line totals before the discount. discount is always a money amount; discountRaw and discountType show what the till stored: the POS till stores a percentage (discountType: "percent") and the customer apps store an amount. serviceCharge is only present on a few orders and is 0 otherwise. vat is the VAT amount included in the total.
refundThe amount of the most recent refund on this order. The POS reduces total by the refund (a full refund leaves 0 and marks the order Declined). If an order was refunded twice only the last amount is kept.
paymentmethod is Card, Apple Pay, Google Pay, Pay in person, Cash and so on, as written by the app or till. reference is the Stripe payment id when there is one. Till orders also carry status (Paid or Not Paid).

CSV output

format=csv returns UTF-8 with a byte order mark, so Excel opens accented names correctly. /v1/orders gives one row per order line (an order with three different items is three rows; the order-level columns repeat). /v1/summary/daily gives one row per day with the breakdowns flattened as Card=120.50; Cash=40.00.

curl -H "Authorization: Bearer axk_YOUR_KEY" \
  "https://aexir.com/api/v1/orders?from=2026-10-01&to=2026-10-31&format=csv" -o october.csv

A CSV call returns every order in the requested range in one file: there is no paging and limit and cursor are ignored. The X-Order-Count response header says how many orders the file holds. If the range holds more than 20,000 orders the call fails with 400 too_many_orders instead of returning a shortened file; ask for fewer days at a time (one month per call is plenty for most restaurants).

Excel (Power Query)

Data tab, Get Data, From Other Sources, Blank Query, then open the Advanced Editor and paste:

let
    Key = "axk_YOUR_KEY",
    Url = "https://aexir.com/api/v1/orders?from=2026-10-01&to=2026-10-31&format=csv",
    Raw = Web.Contents(Url, [Headers = [Authorization = "Bearer " & Key]]),
    Table = Csv.Document(Raw, [Delimiter = ",", Encoding = 65001, QuoteStyle = QuoteStyle.Csv]),
    Promoted = Table.PromoteHeaders(Table, [PromoteAllScalars = true])
in
    Promoted

The query pulls every order in the date range in one go (no paging needed). When Excel asks about privacy levels choose Anonymous for the connection (the key is already in the header). Refresh the query to pull fresh data. For the daily summary swap the URL for /v1/summary/daily?from=...&to=...&format=csv.

Google Sheets

Sheets' built-in IMPORTDATA cannot send an Authorization header, and this API never accepts a key in the URL (it would end up in logs and in the sheet's formula history). Use a short Apps Script function instead: Extensions, Apps Script, paste this, save, then use =AEXIR("orders", "2026-10-01", "2026-10-31") in a cell. Each call returns the whole date range (no paging).

const AEXIR_KEY = 'axk_YOUR_KEY';
const AEXIR_BASE = 'https://aexir.com/api/v1/';

/**
 * Pull Aexir data into the sheet.
 * @param {string} what  "orders", "summary", "locations" or "menu"
 * @param {string} from  start day YYYY-MM-DD (orders and summary)
 * @param {string} to    end day YYYY-MM-DD
 * @customfunction
 */
function AEXIR(what, from, to) {
  const path = what === 'summary' ? 'summary/daily' : what;
  let url = AEXIR_BASE + path + '?format=csv';
  if (from) url += '&from=' + from;
  if (to) url += '&to=' + to;
  const res = UrlFetchApp.fetch(url, { headers: { Authorization: 'Bearer ' + AEXIR_KEY }, muteHttpExceptions: true });
  if (res.getResponseCode() !== 200) throw new Error(res.getContentText());
  return Utilities.parseCsv(res.getContentText().replace(/^\uFEFF/, ''));
}

Sheets gives a custom function only a short time to run, so if a very busy month is slow to load, ask for a week at a time or use the daily summary. locations and menu are JSON only; for those return JSON.parse(...) and map the fields you need, or ask Aexir support for a template sheet. Scripts in a shared sheet can be read by anyone who can edit it, so use a key with customer data off for shared sheets.

Managing keys

Keys are created and revoked in the POS settings by admins with access to all locations, or by Aexir support on request. The POS also asks for the owner PIN before it opens the screen, but the server only accepts key changes from an admin account with access to all locations. The same operations are available over HTTP for those apps with a signed-in Firebase ID token rather than an API key:

CallBody / result
GET /v1/keysLists this restaurant's keys: id, prefix, label, customerData, createdAt, createdBy, lastUsedAt, revokedAt. Never the key itself.
POST /v1/keysBody {"label": "Head office spreadsheet", "customerData": true}. Returns {id, key, prefix}. The full key is returned once and never again. Up to 20 active keys.
DELETE /v1/keys/{id}Revokes the key.

A POS token must belong to an admin user of that restaurant. Any admin can list keys; only an admin with access to all locations can create or revoke one (a branch-only admin gets 403 owner_only). Keys cannot be created for a restaurant that is switched off on Aexir (403 tenant_disabled), and its existing keys stop working with the same error. A few accounts share their data with another brand; keys for them are set up through Aexir support (403 shared_project). An Aexir staff token adds ?tenant=slug. lastUsedAt is updated at most once an hour.

Errors and limits

Every error is JSON in the shape {"error": {"code": "...", "message": "..."}}.

HTTPcodeWhen
400bad_date, bad_tz, bad_limit, bad_cursor, bad_status, bad_order_type, bad_format, range_too_large, too_many_orders, bad_label, bad_json, too_many_keysA parameter or request body did not validate. The message says which. too_many_orders means a CSV range holds more than 20,000 orders: narrow the dates.
401unauthorizedMissing, unknown or revoked key (or token).
403forbidden, owner_only, tenant_disabled, shared_projectA valid sign-in that is not allowed to manage keys for that restaurant, an admin limited to one branch trying to create or revoke a key, a restaurant that is switched off (its keys stop working too), or an account that shares its data with another brand (contact Aexir support).
404not_found, unknown_tenantNo such order, key, tenant or endpoint.
405method_not_allowedWrong HTTP method.
429rate_limitedMore than 120 requests in a minute on one key. The Retry-After header says how many seconds to wait.
503tenant_not_readyThe API has not yet been switched on for this restaurant. Contact Aexir support.

Data notes

Questions: support@aexir.com