This document describes how to call Livra integration endpoints from your app.
Are you a delivery partner working with your own depots? Those endpoints have their own pages: Accept In Depot to check a parcel in when a truck arrives, Transfers Between Depots to send parcels on to the next depot, and Get Order to read one of your orders back (status, dates, and the slip hash for printing).
Use these headers for all Livra integration endpoints:
Content-Type: application/jsonx-api-key: <apiKey>x-signature: <hexHmac>x-signature must be HMAC-SHA256(rawRequestBody, apiSecret) encoded as lowercase hex (optionally prefixed with sha256=).
https://livra.mofavo.com/create_merchantPOST{
"merchant": {
"name": "Example Merchant LLC",
"state": "Dubai",
"city": "Dubai",
"street": "Example Street 1",
"phoneNumber": "+971500000000",
"zipcode": "00000",
"TRN": "100000000000003",
"CIN": 12345678
},
"sender": {
"name": "Example Sender LLC",
"state": "Dubai",
"city": "Dubai",
"street": "Business Bay",
"phoneNumber": "+971511111111"
},
"contract": {
"deliveryPartnerId": 10,
"deliveryFee": 12.5,
"exchangeFee": 4.25,
"cancellationFee": 3.0
}
}
merchant.CIN must be an 8-digit integer.contract.deliveryPartnerId must be a positive integer.{ "merchantId": <number> }https://livra.mofavo.com/create_orderPOST{
"products": [
{ "name": "string", "quantity": 1, "price": 12.5 },
{ "name": "string", "quantity": 2 }
],
"productsToRetrieve": [
{ "name": "string", "quantity": 1 },
{ "name": "", "quantity": 0 }
],
"merchantId": 1,
"deliveryPartnerId": 1,
"primaryName": "string",
"primaryPhone": "string",
"primaryPhone2": "",
"primaryStreet": "",
"primaryZone": "",
"primaryCity": "string",
"primaryState": "string",
"primaryZipcode": "",
"deliveryInstructions": "",
"amount": 12.5,
"allowOpen": true,
"isExchange": false,
"isFragile": false,
"callback_link": "https://your-app.example.com/livra/webhook"
}
callback_link is optional. Omit it or leave off to disable webhooks for that order.
products is required and must be a non-empty array.products[] item needs non-empty name, positive integer quantity, optional non-negative price.merchantId and deliveryPartnerId must be positive integers.primaryName, primaryPhone, primaryCity, primaryState must be non-empty.amount must be non-negative.allowOpen, isExchange, isFragile must be booleans.isExchange=true, productsToRetrieve is expected. Invalid/missing entries still proceed with fallback name "unknown".callback_link (optional): when present, Livra sends a signed POST to this URL whenever the order undergoes a meaningful status change (see Order status webhooks). Must be a valid absolute HTTP or HTTPS URL that accepts JSON POSTs from Livra’s infrastructure.{ "orderId": <number>, "hash": "<string>" }
hash is the order-slip QR hash. A slip’s QR code must encode "<orderId>#<hash>" to be accepted by depot scanners. The hash covers the order’s content (recipient, products, amount, …), so after any order update the previous hash is stale — always use the latest one returned.merchant_not_foundno_active_contractsender_not_foundhttps://livra.mofavo.com/update_orderPOSTPatch-style payload. Only orderId is required; all other fields are optional.
{
"orderId": 1234,
"products": [{ "name": "string", "quantity": 1 }],
"productsToRetrieve": [{ "name": "string", "quantity": 1 }],
"primaryName": "string",
"primaryPhone": "string",
"primaryPhone2": "",
"primaryStreet": "",
"primaryZone": "",
"primaryCity": "string",
"primaryState": "string",
"primaryZipcode": "",
"deliveryInstructions": "",
"amount": 12.5,
"allowOpen": true,
"isExchange": false,
"isFragile": false
}
orderId must exist.readyForPickUp; otherwise update is rejected.{ "orderId": <number>, "hash": "<string>" }
hash is the recomputed order-slip QR hash after the update (see Create Order). Any slip printed with an older hash must be reprinted.order_not_foundorder_update_not_permittedUse this to fetch the current order-slip QR hash for an order — for example right before printing (or reprinting) a delivery slip. The QR must encode "<orderId>#<hash>". The hash covers the order’s content, so it changes whenever the order changes; a slip carrying an outdated hash is rejected at scan time.
Create Order and Update Order already return the same hash in their responses; this endpoint is for when you need it again later for an existing order.
https://livra.mofavo.com/order_hashPOST{
"orderId": 1234
}
orderId is required and must be a positive integer.{ "orderId": <number>, "hash": "<string>" }order_not_found or validation errorshttps://livra.mofavo.com/change_requestPOSTx-api-key + x-signature flow as other endpoints{
"orderId": 1234,
"changes": [
{
"type": "PHONE_CHANGE",
"oldValue": "50000000",
"newValue": "51111111"
},
{
"type": "AMOUNT_CHANGE",
"oldValue": 110,
"newValue": 125.5
}
],
"comment": "Customer requested phone and amount correction",
"makeRegular": false
}
orderId must be a positive integer.changes must be a non-empty array. One request can carry one change or several of
different types (e.g. a phone and an amount together, as in the example above).Each change requires type, one of the types below, and a newValue of the type shown.
Values are checked strictly: "79" is not an amount, and "true" is not a boolean.
type |
newValue |
Example |
|---|---|---|
AMOUNT_CHANGE |
number, >= 0 |
79, 125.5 |
ALLOW_OPEN_CHANGE |
boolean | true |
PHONE_CHANGE |
string: optional +, then 8 to 15 digits. Spaces and dashes are allowed and removed before saving ("51 111 111" and "51-111-111" are saved as "51111111"). Dots, brackets, tabs and other characters are rejected. |
"51111111" |
PHONE2_CHANGE |
same as PHONE_CHANGE, or "" to remove the second phone. null is rejected: send "" instead. |
"51111111", "" |
DELIVERY_DATE_CHANGE |
the planned delivery day, as a "YYYY-MM-DD" date that exists, today or later (Tunisia time). A past date is rejected. null clears the planned date. |
"2026-10-02", null |
ADDRESS_CHANGE |
an object with any of street, zone, city, state, zipcode. Each value is a string or null; strings are trimmed before saving and may be at most 255 characters after trimming. A value of only spaces is rejected. At least one field must be non-empty. Any other key is rejected. |
{ "street": "12 Rue X", "zipcode": "2036" } |
ADDRESS_CHANGE, a null or "" field is ignored when the request is approved, not
cleared: the order keeps its current value. Send only the fields that change.oldValue is optional, is not checked, and is only shown to whoever reviews the request.
Use the same type as newValue so it displays well.type may appear only once per request. To change several fields, put all of them in
the same request rather than sending them one by one: a new request for the order replaces
its pending one, so sending them separately would keep only the last.comment is optional string.makeRegular is optional boolean.order_not_found, exactly as if it didn’t exist.inDepot or inTransit status.deliveryDate is set (exchange already happened), request creation is blocked.{ "ok": true }order_not_found (also for an order that belongs to another delivery partner)order_status_not_eligible_for_change_requestexchange_already_completed_change_request_not_allowedno_changes_providedbody is not valid JSONchanges[0].newValue must be a number, not a string: send 79, not "79"changes[0].newValue must be true or false without quotes: send true, not "true"changes[0].newValue must be a phone number like "51111111"changes[0].newValue is in the past (today is 2026-10-02)changes[0].newValue.primaryStreet is not an address field (use street, zone, city, state, zipcode)changes[1] repeats AMOUNT_CHANGE from changes[0]When you include callback_link on create order, Livra calls that URL with an outbound webhook on every meaningful change to the order.
Every request carries two headers that identify exactly what you are receiving:
X-Webhook-Type: advanced
X-Webhook-Version: 1
Use X-Webhook-Version to guard your parsing logic against future changes.
Current version: 1
The raw event payload as recorded by the platform. Each delivery represents one discrete change, with an explicit event name, a full snapshot of the current field values, and their previous values for comparison. Driver outcomes are separate driver.* events rather than a comment on an order event.
Full documentation: Livra Webhooks — Advanced
The following applies to all Livra webhooks.
Every request includes an X-Webhook-Signature header containing an HMAC-SHA256 of the raw request body, hex-encoded, using your Livra API secret (the same secret you use to sign requests to Livra).
Always verify this header before processing the payload.
Node.js
const crypto = require('crypto');
function verifySignature(secret, rawBody, signature) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
// Express example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-webhook-signature'];
if (!verifySignature(process.env.WEBHOOK_SECRET, req.body, sig)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// process event...
res.sendStatus(200);
});
Python
import hmac, hashlib
def verify_signature(secret: str, raw_body: bytes, signature: str) -> bool:
expected = hmac.new(
secret.encode(),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
Go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func verifySignature(secret, signature string, body []byte) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
Important: always read the raw request body for signature verification. Parsing the JSON first and re-serialising it may produce a different byte sequence and cause verification to fail.
Reply with any 2xx status code to acknowledge successful delivery. The response body is ignored.
If your endpoint returns a non-2xx status or does not respond within 10 seconds, the delivery is retried automatically.
| Attempt | Delay before retry |
|---|---|
| 1 | 30 seconds |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 8 hours |
After 5 failed attempts the delivery is marked permanently failed and no further retries are made. The platform team can manually re-queue a delivery on request.
Each delivery has a unique UUID in the X-Webhook-ID header. Use it to deduplicate events if your endpoint receives the same delivery more than once.