This document describes how to call Livra integration endpoints from your app.
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> }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> }order_not_foundorder_update_not_permittedhttps://livra.mofavo.com/change_requestPOSTx-api-key + x-signature flow as other endpoints{
"orderId": 1234,
"changes": [
{
"type": "PHONE_CHANGE",
"oldValue": "+971500000000",
"newValue": "+971511111111"
}
],
"comment": "Customer requested phone correction",
"makeRegular": false
}
orderId must be a positive integer.changes must be a non-empty array.type and must be one of:
PHONE_CHANGEPHONE2_CHANGEAMOUNT_CHANGEADDRESS_CHANGEALLOW_OPEN_CHANGEDELIVERY_DATE_CHANGEoldValue and newValue are optional and can be any JSON value.comment is optional string.makeRegular is optional boolean.inDepot or inTransit status.deliveryDate is set (exchange already happened), request creation is blocked.{ "ok": true }order_not_foundorder_status_not_eligible_for_change_requestexchange_already_completed_change_request_not_allowedno_changes_providedWhen you include callback_link on create order, Livra calls that URL with an outbound webhook on every meaningful change to the order.
There are two webhook flavours — simple and advanced — each independently versioned. You choose which one you want when you register your endpoint. The right choice depends on what you want to do with the data:
| Simple | Advanced | |
|---|---|---|
| Payload | Pre-computed deliveryStatus, orderStatus, comment, settled |
Raw event with full snapshot and previous values |
| Driver events | Surfaced via comment field |
Dedicated driver.* events |
| Invoice settlement | Surfaced via settled field |
Dedicated invoice.settled event |
| Best for | Straightforward status sync | Full auditability, custom logic, richer context |
Every request carries two headers that identify exactly what you are receiving:
X-Webhook-Type: simple # or: advanced
X-Webhook-Version: 1
Use them to route parsing logic if you ever handle more than one flavour or version on the same endpoint, or to guard against unexpected changes.
Current version: 2 — what changed from v1
A compact, pre-processed payload. The platform resolves deliveryStatus, orderStatus, comment, and settled for you. One payload shape covers all event types.
Order events — fired when order state changes
| Trigger | Fired when |
|---|---|
status.changed |
status changed |
destination.changed |
finalDestination changed |
position.updated |
Current position changed |
delivery_date.changed |
Delivery date set or updated |
Driver events — fired when a driver records a last-mile outcome
| Trigger | comment value |
|---|---|
| Customer refused delivery | declined |
| Driver could not reach customer | unreachable |
| Delivery rescheduled | rescheduled |
Invoice events — fired when the delivery partner settles the order’s invoice
| Trigger | settled value |
|---|---|
| Delivery partner settlement recorded | true |
POST <your-callback-url>
Content-Type: application/json
X-Webhook-ID: <delivery-uuid>
X-Webhook-Signature: <hmac-hex>
X-Webhook-Type: simple
X-Webhook-Version: 2
{
"ok": true,
"timestamp": "2026-05-05T11:23:00Z",
"orders": [
{
"id": 1234,
"deliveryStatus": "pending",
"orderStatus": "inTransitToCustomer",
"comment": null,
"settled": false
}
]
}
timestamp is when the event was detected, in UTC ISO 8601. On retries it reflects the original event time, not the retry time.
comment is null for order and invoice events. For driver events it is one of unreachable, declined, or rescheduled.
settled is false for all events except the invoice settlement notification, where it is true.
deliveryStatus| Value | Meaning |
|---|---|
pending |
The order is still in play. |
delivered |
Delivered to the final recipient. |
cancelled |
The order is being returned to the merchant. |
orderStatus| Value | Meaning |
|---|---|
readyForPickUp |
Created, waiting to be collected. |
inDepot |
Held at a depot. |
inTransitToDepot |
On the road toward an intermediate depot. |
inTransitToCustomer |
On the road toward the customer. |
inTransitToMerchant |
On the road back to the merchant. |
delivered |
Delivered to the customer. |
returned |
Returned to the merchant. |
exchange-returned |
Exchange declined; parcel returned to merchant. |
exchange-completed |
Exchange completed; collected goods returned to merchant. |
cancelled |
Order voided. |
| (other values) | Treat unknown values gracefully. |
SECRET="your-secret"
BODY='{"ok":true,"timestamp":"2026-05-05T11:23:00Z","orders":[{"id":1234,"deliveryStatus":"pending","orderStatus":"inTransitToCustomer","comment":null,"settled":false}]}'
SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -X POST https://your-endpoint.example.com/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-ID: test-$(uuidgen)" \
-H "X-Webhook-Signature: $SIG" \
-H "X-Webhook-Type: simple" \
-H "X-Webhook-Version: 2" \
-d "$BODY"
Hooks on v1 continue to work without any changes. The differences in v2 are:
| v1 | v2 | |
|---|---|---|
settled field |
not present | always present (false by default, true on invoice settlement) |
orderStatus when in transit to a depot |
inTransitToCustomer |
inTransitToDepot |
| Invoice settlement notification | delivered as a status snapshot, no settled field |
delivered with settled: true |
To migrate a registered endpoint from v1 to v2, contact the platform team.
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 both simple and advanced 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.