Lexina Delivery

Order API

Integration guide for partner systems — POS, ordering sites, aggregators and dispatch platforms. If you were given an access key, you are in the right place.

What it does: you send us an order with a single HTTP request and it appears instantly to our available couriers. We then push every status change back to you, until the order is delivered.

What it does not do: it does not read menus and it does not process payments. The order arrives fully formed from your system.

Field names are in Romanian and stay that way — linii, zona, metodaPlata, and so on. This page is translated; the API is not. Copy the field names exactly as written here.

Authentication

Each location has its own key, sent in the X-API-Key header on every request. Keys look like lxk_ followed by 48 hex characters, and are issued by our dispatch team.

Keep the key server-side, never in a web page. If it ends up in front-end code, anyone can read it and place orders in your name. Call us from your backend.

Creating an order

POST https://europe-west1-lexina-delivery.cloudfunctions.net/apiComanda

# curl example
curl -X POST https://europe-west1-lexina-delivery.cloudfunctions.net/apiComanda \
  -H "X-API-Key: lxk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "idExtern": "ORD-10432",
    "sursa": "glovo",
    "zona": "Brașov",
    "client": {
      "nume": "Maria Popescu",
      "telefon": "0722123456",
      "adresa": "Str. Lungă 24, bl. 3, ap. 12",
      "lat": 45.6579,
      "lng": 25.6012
    },
    "linii": [
      { "nume": "Pizza Diavola", "pret": 42.00, "cantitate": 2 },
      { "nume": "Coca-Cola 0.5", "pret": 9.00, "cantitate": 1 }
    ],
    "taxaLivrare": 9.00,
    "metodaPlata": "cash",
    "observatii": "Intercom is broken, please call on arrival",
    "esteGata": false
  }'

Fields

FieldDescription
client.nume requiredCustomer name, at least 3 characters.
client.telefon requiredRomanian number, 10 digits, starting with 07, 02 or 03. Non-digits are stripped before validation.
client.adresa requiredStreet, number and any detail that helps. At least 8 characters — the courier has to find it.
client.lat
client.lng
Optional WGS84 coordinates. If present, we match the delivery zone against our drawn zone boundaries instead of parsing the address text — the most reliable way to get the tariff right. We use them to pick the zone and do not store them.
liniiOrder lines: nume, pret, cantitate. The order value is computed from these. Quantity is clamped to 1–50. The courier sees the lines on their phone and checks the bag against them.
valoareMancareAlternative to linii if your system cannot send the lines. Total food value, as a number.
taxaLivrareThe delivery fee you charge the customer. It stays yours.
zonaDelivery locality name, e.g. Brașov, Săcele. Used when no coordinates are sent. If omitted, the location's default zone applies.
zonaIdOur internal zone id, if you have been given one. Overrides both of the above.
metodaPlatacash or card. With card the courier collects nothing.
idExternYour own order reference. Strongly recommended — it is what makes retries safe.
sursaWhere the order came from: telefonic, site, glovo, wolt, bolt. Shows up in reports.
observatiiFree text for the courier.
esteGatatrue if the food is already prepared. Couriers see the label and come straight over.

Response

HTTP 201
{
  "comandaId": "aBcD1234...",
  "status": "asteptare",
  "zona": "Brașov",
  "total": 102.00
}

Idempotency

Send idExtern and retries are safe. If an order with the same idExtern already exists for your location, you get 200 with "duplicat": true and the original comandaId — no second order is created. Without idExtern, a retried request creates a duplicate order and a second courier run.

Errors

CodeMeaning
401Missing or invalid key.
400Validation failed. The body names the exact field.
403Account deactivated.
405Wrong method — use POST.
500Our side. Retry in a few seconds.

Error bodies look like { "eroare": "client.telefon invalid (10 cifre)." }. The message is in Romanian; the HTTP code is what you should branch on.

Checking status

GET .../apiStareComanda?comandaId=... or ?idExtern=..., same header.

{
  "comandaId": "aBcD1234...",
  "status": "spre_client",
  "curier": "Andrei M.",
  "esteGata": true,
  "livratLa": null
}

Use this for reconciliation or spot checks. For live tracking, use webhooks below — polling gives you stale data and costs both of us requests.

Status values

StatusMeaning
neconfirmatWaiting for the location to confirm
asteptareLooking for a courier
preluatPicked up by a courier
la_restaurantCourier is at the location
spre_clientOn the way to the customer
livratDelivered
anulatCancelled
returGoing back to the location
returnatReturned to the location
de_ridicatWaiting for customer pickup
livrare_restaurantDelivered by the location's own driver

Webhooks

Rather than you asking us what happened, we tell you. Give us an https:// endpoint and we POST to it on every status change, from "looking for a courier" through to "delivered".

We configure the endpoint on our side — send it to us together with your key request. You then receive a signing secret (prefixed lws_), shown once.

By default we notify only orders that arrived through this API. Orders typed into our own portal are not pushed to you unless you ask us to enable that.

What you receive

POST https://your-system.com/lexina-status
Content-Type: application/json
User-Agent: LexinaDelivery-Webhook/1
X-Lexina-Eveniment: stare_comanda
X-Lexina-Semnatura: 9f2c...   // HMAC-SHA256 of the raw body

{
  "eveniment": "stare_comanda",
  "comandaId": "aBcD1234...",
  "idExtern": "ORD-8841",
  "sursa": "glovo",
  "status": "livrat",
  "statusEticheta": "Livrată",
  "restaurant": { "id": "rest_x", "nume": "Pizza Oscar" },
  "curier": { "nume": "Andrei M.", "telefon": "07XXXXXXXX" },
  "client": { "nume": "Ioana P.", "telefon": "07XXXXXXXX", "adresa": "Str. Lungă 12, Brașov" },
  "zona": "Brașov oraș",
  "sume": { "mancare": 84.5, "livrare": 15, "total": 99.5 },
  "metodaPlata": "cash",
  "incident": null,
  "timp": {
    "creata":     "2026-09-25T17:02:11.000Z",
    "confirmata": "2026-09-25T17:03:40.000Z",
    "preluata":   "2026-09-25T17:09:02.000Z",
    "laClient":   "2026-09-25T17:24:55.000Z",
    "livrata":    "2026-09-25T17:26:10.000Z"
  },
  "trimisLa": "2026-09-25T17:26:11.412Z"
}

curier is null until someone picks the order up. Each field inside timp is null until that moment happens. statusEticheta is the same status written in Romanian, ready to display. incident is non-null when something went wrong in the field — customer unreachable, wrong address, missing items — with tip, eticheta and rezolvat.

Verifying the signature

Anyone can POST to your endpoint. To know a message is really from us, compute HMAC-SHA256 over the raw request body using your secret and compare it to the X-Lexina-Semnatura header.

// Node.js
const signature = crypto.createHmac("sha256", SECRET)
  .update(rawBody)             // the exact bytes received, not re-serialised JSON
  .digest("hex");
if (signature !== req.headers["x-lexina-semnatura"]) return res.sendStatus(401);

// PHP
$signature = hash_hmac('sha256', $rawBody, $SECRET);
if (!hash_equals($signature, $_SERVER['HTTP_X_LEXINA_SEMNATURA'])) { http_response_code(401); exit; }

Re-serialising the JSON before hashing is the usual reason a signature check fails — key order and whitespace change, and so does the hash.

What we expect from your endpoint

We can fire a test event at your endpoint from our admin panel — same structure, with a test comandaId — so you can verify the integration before a real order exists. We also keep a delivery log: if something never arrives, we can tell you exactly what your server responded and how many attempts we made.

Current limitations

Worth knowing before you design around us:

Practical recommendations

Infrastructure

Google Cloud, europe-west1 (Belgium). Typical round trip from Romania is a few tens of milliseconds. No IP allowlisting is required on your side.

Technical contact: lexinadelivery@gmail.com · +40 773 796 693
Lexina Delivery · Brașov, Romania