Areye courier platform — integrator guide
For courier companies, marketplaces, and their accounting systems (e.g. Sage). Everything here is a documented API. Nothing is built for one integrator alone. Two integrators call the same endpoints with their own keys, secrets and data, and neither can see the other's.
Availability. The courier platform is enabled per courier by Areye. Until it is enabled for you, every endpoint below returns
403.
Version: courier platform API v1 · Last changed: 11 October 2026 · see Changelog and deprecation policy at the end.
Base URL: https://api.areye.app/api
1. Who you are and what you may do
| Caller | Header | Typical user |
|---|---|---|
| Courier (business) | X-Business-API-Key: <key or staff session token> |
The courier's office and systems |
| Partner key | X-Business-API-Key: ak_… |
A system acting for the courier with limited rights |
| Courier's client | X-Client-API-Key: <client session token> |
A shop or pharmacy booking with the courier |
| Driver app | Authorization: Bearer <driver token> |
The courier's driver |
Authority comes only from who you are and the scopes Areye stores for you. It never comes from the endpoint you call or from anything you put in the request. A request that says "role": "operations" changes nothing.
| Scope | Allows |
|---|---|
shipments:create |
Book shipments |
shipments:view-own |
Read your own shipments, the operations board and reports |
customs:submit-documents |
Supply customs documents |
customs:approve |
Clear, release or refuse at customs; clear or refuse the vehicle at the border |
runs:operate |
Run network runs: stops, scans, deliveries, collections, alerts, close |
runs:override |
Override capacity; approve laboratory-sample exceptions; edit checklists and calendars |
claims:approve |
Approve claims |
billing:view-own |
Read billing |
billing:administer |
Publish rate cards, invoice, record payments, issue notes, top up, export, acknowledge billing events |
Who gets which scopes:
- The courier's own API key: without stored scopes it can create, view, submit documents and operate, but never approve, override or administer billing. Areye grants more on request.
- Staff sessions: admins get every scope. Coordinators can create, view, submit documents and operate.
- Partner keys: only the scopes stored on the key.
- Courier clients: own shipments, own documents, own billing. Never courier operations.
A refused scope answers:
HTTP 403
{"success": false, "code": "scope_denied", "message": "This login is not allowed to do that (customs:approve)."}
2. Prices and booking (retail checkout, not "request a quote")
2.1 Delivery options
POST /client/prices (alias POST /client/delivery-options), called by the courier's client.
{
"sender_name": "Bokamoso Health", "sender_address": "Main Mall, Gaborone", "sender_lat": -24.65, "sender_lng": 25.91,
"recipient_name": "Lancet Laboratories", "recipient_phone": "+27115550000",
"recipient_address": "Richmond, Johannesburg", "recipient_lat": -26.18, "recipient_lng": 28.01,
"parcels": [{"weight_kg": 12, "length_cm": 40, "width_cm": 30, "height_cm": 20}],
"contents": "Laboratory consumables", "declared_value": 4000, "liability_cover": false,
"from_country": "BW", "to_country": "ZA",
"external_customer_code": "BOK001", "external_billing_reference": "SI-2026-0901",
"sort": "cheapest"
}
The response lists every eligible service. Each one has:
- its dates;
- every price line, with tax and the total;
- for cross-border routes, what may be charged later;
- what the client's account can cover.
{"success": true, "data": {
"prices_reference": "P-7KQ2M9XA", "valid_until": "2026-10-12T09:30:00+02:00",
"options": [{
"code": "XB", "name": "Cross-border road", "chargeable_kg": 12,
"lines": [
{"code": "transport", "label": "Gaborone → Johannesburg (12 kg chargeable)", "amount": 850.0},
{"code": "tax", "label": "VAT 14%", "amount": 119.0, "included": false, "tax": 119.0}
],
"tax": 119.0, "total": 969.0, "currency": "BWP",
"collection_on": "2026-10-12", "earliest_delivery": "2026-10-22", "latest_delivery": "2026-10-26",
"possible_later_charges": "Duties and exceptional customs charges (storage, extra waiting), if incurred, are billed separately with evidence."
}],
"account": {"settlement_mode": "credit", "currency": "BWP", "available": 12500.0}
}}
Price lines, in this order:
- transport (or the corridor price);
- collection;
- delivery;
- service-level adjustment;
- liability cover;
- negotiated discount;
- surcharge;
- minimum-charge top-up;
- tax (added on top, or shown as included).
The lines always add up to the total.
The prices are held for the minutes Areye sets (default 30). The record behind them is immutable: you can only book what was shown.
2.2 Book shipment
POST /client/book
{"prices_reference": "P-7KQ2M9XA", "service_level": "XB", "dangerous_goods_confirmed": true}
201returns the waybill and a tracking URL.- Sending the same request again returns
200with"already_booked": trueand books nothing new. - Expired prices:
422 "These prices have expired. Check prices again to book." - Prepaid balance or credit limit exceeded:
422, and nothing is booked or charged. These checks run under a lock: in a test of 100 simultaneous bookings against a balance or limit for 20, exactly 20 were booked.
The shipment keeps a frozen pricing snapshot:
- the rate-card version;
- the inputs;
- the service;
- every price line;
- tax and currency;
- who pays;
- the delivery promise.
Nothing later rewrites it.
Compatibility: POST /client/quote, POST /client/quotes (a formal quotation, held 7 days) and POST /client/quotes/{reference}/book remain available.
2.3 The courier's office books for a client
POST /business/shipments takes the same fields, plus courier_client_id, external_customer_code and external_billing_reference.
- It is priced on that client's card and settled according to that client's settlement mode (§4).
- The Areye waybill is the shipment's identifier. Your references are searchable secondary references.
- If the courier sets
unique_external_referencefor a client, the same external reference twice answers409 duplicate_order.
Find a shipment by any reference: GET /business/courier/shipments?q=SI-2026-0901.
3. Rate cards: immutable versions
POST /business/courier/rate-cards(billing:administer) publishes version 1. Its terms:- base charge, per-kg and included kg;
- volumetric divisor;
- minimum charge;
- collection and delivery fees;
- discount % and surcharge %;
- tax % (inclusive or exclusive) and tax label;
- corridors (
[{from, to, base_charge, per_kg, label}]); - liability cover;
- redelivery, return and claim terms;
- customs-administration, errand, failed-collection, cold-chain and waiting fees.
POST /business/courier/rate-cards/{card}/versionspublishes new terms as the next version, fromeffective_from. Fields you leave out carry over.- A published version can never be edited or deleted. A shipment and a price offer keep the version they were priced on.
GET /business/courier/rate-cardslists every version and marks the one in force.
4. How the courier's clients settle
Set per client with PATCH /business/courier/clients/{client}: settlement_mode, credit_limit, payment_days, billing_currency, external_system, unique_external_reference.
| Mode | What Areye does |
|---|---|
prepaid |
Books only what the client's prepaid balance covers (checked under a lock). Later approved charges post afterwards. |
credit |
Books within the credit limit (checked under a lock). The courier invoices from Areye: invoices, statements, payments, ageing. |
external |
The courier invoices in its own accounting system (e.g. Sage). Areye records what it calculated for audit and profit, but posts no receivable: no Areye invoice, no wallet, no payment allocation. It sends every charge and every completed shipment to the accounting system (§5). |
In every mode:
- The booked price is never rewritten. Each later charge is a separate itemised entry with
payer,reason,evidence_path,source_eventand a stablereference. Sending the same event twice books it once. - Each extra has its own entry:
- customs administration (per crossing cleared);
- duties;
- errands;
- waiting;
- cold chain;
- failed collection;
- redelivery and returns.
- Duties paid on the client's behalf are
recoverableand kept apart from charges. They are not revenue. - Currency: a conversion records its rate and source. Without a rate the entry stays
unresolved_fxand is left out of balances. It is never converted at one-to-one. - Corrections are debit or credit notes pointing at the original entry. An issued invoice is never rewritten; later entries go on a supplementary invoice.
- COD reduces what a client owes only after the courier confirms the cash was remitted.
Courier endpoints (billing:administer for writes):
GET /business/courier/clients/{client}/billing: balances, credit use, ageing (current, 1–30, 31–60, 61–90, 90+ days), statement, COD awaiting remittance, unresolved conversions, payments, invoices.POST /business/courier/clients/{client}/billing/{action}:top-up(amount, currency, reference);payment(amount, currency, reference, optional allocations by invoice id);correct(entry_reference, amount ±, reason);cod-remitted(waybill, evidence file);resolve-fx.
POST /business/courier/clients/{client}/invoice(period: YYYY-MM): one invoice per currency.
The client sees only its own account: GET /client/account.
5. Sending billing to your accounting system (external billing)
5.1 Signed events
Areye POSTs each event to the billing endpoint Areye configures for you (or else your business webhook URL).
Headers:
Content-Type: application/json
X-Areye-Event-Id: 3f1c… (stable; the same on every retry)
Idempotency-Key: 3f1c… (same value)
X-Areye-Signature: sha256=<hex> (HMAC-SHA256 of the raw body, keyed with the courier's webhook secret)
Verify the signature over the raw body before parsing it:
$ok = hash_equals('sha256=' . hash_hmac('sha256', $rawBody, $secret), $request->header('X-Areye-Signature'));
Signature V3 (recommended): X-Areye-Signature-V3: t={timestamp},sha256={hex}, hex = HMAC-SHA256 (your webhook secret) of a canonical string of five lines joined by \n, in order: api_version, X-Areye-Delivery-Timestamp, event_id, X-Areye-Attempt-Id, base64 of the SHA-256 digest of the exact raw body. Check that X-Areye-Event-Id and X-Areye-Api-Version equal the body's event_id and api_version. X-Areye-Key-Id names the signing secret, so you can hold the old and new secret during a rotation. Reject delivery timestamps more than 5 minutes off; a replay is a fresh delivery (new timestamp and attempt id, same event and body), so it verifies. X-Areye-Signature (body only) is still sent.
Versioning: every body carries api_version (v1), event_type, event_id and occurred_at; headers carry X-Areye-Api-Version: v1 and X-Areye-Attempt-Id (new on every attempt — dedupe on X-Areye-Event-Id, never on the attempt id).
Retries: after 1, 5, 30, 120 and 720 minutes, then the event is marked failed and the courier's operations team is alerted. Treat X-Areye-Event-Id as your idempotency key.
Event types:
- One per charge:
shipment.booked,customs.cleared:{id},customs.duties:{id},run.collected:{stop},run.waiting:{stop},sample.cold_chain,run.collection_failed:{stop},debit_note.issued,credit_note.issued,cod.remitted,charge.resolved. - One per shipment milestone:
shipment.delivered(with POD),shipment.returned,shipment.cancelled.
Each is sent once.
Charge event body:
{"event_id": "…", "type": "customs.cleared:41",
"client": {"id": 7, "code": "BOK", "name": "Bokamoso Health"},
"entry": {"reference": "ADJ-CUSTOMS-41", "kind": "customs_admin", "amount": 40.0, "currency": "BWP", "status": "external",
"payer": "client", "recoverable": false, "reason": "Crossing cleared (SAD500-…)", "source_event": "customs.cleared:41",
"original_amount": null, "original_currency": null, "fx_rate": null, "corrects": null, "shipment": "AWVF4PKXN9QT", "created_at": "…"},
"shipment": {"waybill": "AWVF4PKXN9QT", "external_customer_code": "BOK001", "external_billing_reference": "SI-2026-0901", "client_order_reference": null}}
Completion event body (shipment.delivered):
{"event_id": "…", "type": "shipment.delivered", "client": {…},
"shipment": {"waybill": "AWVF4PKXN9QT", "external_customer_code": "BOK001", "external_billing_reference": "SI-2026-0901",
"booked_on": "2026-10-12", "service": "XB", "base_charge": 969.0, "currency": "BWP", "tax": 119.0, "price_lines": [ … ],
"adjustments": [{"reference": "ADJ-CUSTOMS-41", "kind": "customs_admin", "amount": 40.0, "currency": "BWP", "reason": "…", "recoverable": false, "correction": false}],
"status": "delivered", "delivered_at": "2026-10-26T10:00:00+02:00", "pod": true}}
5.2 Tell Areye what your system did with it
POST /business/courier/billing/events/{event_id}/acknowledge
{"state": "acknowledged | invoiced_externally | reconciled", "external_reference": "SAGE-JNL-88"}
Each entry's export state only moves forward: pending → exported → acknowledged → invoiced_externally → reconciled.
- Exported never means paid. There is no "paid" state, and
"state": "paid"is refused. - A late or repeated acknowledgement never moves an entry back.
- Send
external_referencewith any state. One reference is kept per state, the first one reported for that state. The event returnsexternal_references(by state),reported_state(the furthest state reported) andexternal_reference(the reference of that furthest state).
5.3 CSV export
POST /business/courier/billing/export with client_id, from, to returns one row per entry. Columns:
- date, client, waybill;
- external customer code, external billing reference;
- reference, kind, description;
- amount, currency, tax;
- status, payer, recoverable;
- reason, source event, corrects;
- original amount and currency, FX rate;
- invoice id;
- shipment status, delivered at;
- export state, event state, external reference.
The POST is the export: it marks pending entries exported. GET /business/courier/billing/export?client_id=&from=&to= returns the same file as a preview. It shows each entry's stored state and changes nothing.
GET /business/courier/billing/events?status= lists the event outbox and its state.
POST /business/courier/billing/events/{event_id}/requeue with {"reason": "..."} sends a failed or pending event again from the first attempt. The reason is recorded. With no billing endpoint set, an event stays pending and is retried hourly. If its entry has not gone out in a CSV export either, an alert billing_endpoint_missing is raised.
6. Long-running shipments: lifecycle, health, alerts
A courier shipment can be open for 1–14 days across several legs, drivers, depots, countries and customs waits. It is not a trip. Ordinary ride timeouts (no driver, stale trip) never cancel, release or complete it. A leg waiting for a driver raises an alert instead.
6.1 Calendars and service timings
-
POST /business/courier/calendars(runs:override):{"code": "BWZA", "name": "Botswana & South Africa", "timezone": "Africa/Gaborone", "working_days": [1,2,3,4,5], "holidays": ["2026-12-25"]} -
POST /business/courier/service-levelstakes, besides transit days and cut-off:calendar_id;collection_due_hours,depot_dwell_hours,border_presentation_hours,customs_wait_hours,linehaul_hours,destination_dwell_hours,delivery_attempt_hours,redelivery_due_hours,no_scan_hours;at_risk_pct(default 75).
Hours count on working days only, in the calendar's timezone. Each shipment freezes these terms at booking.
6.2 Milestones
Each obligation has its own deadline and owner:
| Milestone | Opens | Closes | Owner |
|---|---|---|---|
collection |
booking | collected / scanned at depot | courier |
depot_dwell |
received at a depot | loaded / out for delivery | courier |
linehaul |
run departed | arrived / received | courier |
border_presentation |
run departed toward a crossing | presented to customs | courier |
customs_clearance |
presented | cleared / released | customs |
customs_documents |
customs holds for papers | supplied and cleared | courier |
delivery_attempt |
out for delivery | delivered / attempted | courier |
redelivery |
attempt failed | out again | recipient |
promise |
booking | delivered | courier |
6.3 Headline and health
-
Headline, what customers see:
booked,awaiting_collection,collected,at_origin_facility,in_transit,at_border,customs_processing,customs_hold,cleared,at_destination_facility,out_for_delivery,delivery_attempted,awaiting_redelivery,delivered,returning,returned,cancelled,lost_or_damaged. -
Health, what operations sees:
on_schedule,at_risk,late,waiting_customer,waiting_customs,waiting_courier,exception,delivered,closed.Health is the first of these that applies:
- an open exception;
- something the courier owns that is past due;
- customs papers the courier must supply;
- waiting on customs;
- waiting on the customer;
- at risk;
- on schedule.
Every shipment carries next_milestone, next_due_at, next_owner and estimated_delivery_on. When the estimate moves, the client's webhook receives shipment.estimate_changed.
6.4 The operations board
-
GET /business/courier/operations?health=late&next_owner=courier&client_id=&facility_id=&q=&overdue=1&open_alerts=1&include_closed=0&view=<saved name>One row per shipment:
- waybill and references;
- headline and health, with the reason;
- custody (holder and facility);
- last scan (event, time, facility, note);
- next milestone, due time and owner;
- estimate and promise;
- pieces (how many, in how many places);
- open alerts;
- age.
meta.by_healthgives the counts. -
GET /business/courier/operations/{waybill or external reference}: obligations, every piece's custody, the full trail, alerts, frozen service terms. A live position appears only while a road leg is moving; otherwise rely on the last verified scan. -
Saved filters:
GET|POST /business/courier/operations/views,DELETE /business/courier/operations/views/{name}. -
Daily report:
GET /business/courier/operations/reportgives open shipments by health and next owner, ageing (0–2, 3–5, 6–9, 10–14, 15+ days), open alerts by kind (unacknowledged, older than 24 h), and today's deliveries, alerts raised and alerts resolved.
6.5 Alerts
Areye checks every open shipment regularly and raises each condition once, owned by whoever must act.
| Kind | Raised when |
|---|---|
milestone_at_risk, milestone_overdue |
a milestone nears or passes its deadline |
customs_wait_exceeded |
customs runs over (once per working day over) |
no_scan |
no physical scan for longer than allowed |
pieces_separated |
a shipment's pieces are in different places with no exception explaining it |
documents_missing |
customs documents are still missing |
leg_unassigned, leg_trip_ended |
a leg has no driver, or its trip ended without telling the shipment |
released_awaiting_collection |
customs released a shipment after the vehicle left |
notification_failed |
a client webhook could not be delivered |
billing_export_failed |
a billing event could not be sent |
Endpoints:
GET /business/courier/alerts?status=&kind=&owner=POST /business/courier/alerts/{id}/assignwith{"to": "Thabo"}POST /business/courier/alerts/{id}/acknowledgePOST /business/courier/alerts/{id}/resolvewith{"resolution": "…", "evidence": "…"}or anevidence_fileupload. Both are required: an alert is never silently dismissed.
6.6 Customer tracking
/s/{token} (secure link) shows:
- the current milestone;
- the last verified place and scan time;
expected_by;- a plain-language
delay(for example "With customs at the border. Clearance times are set by customs."); - delivery attempts;
- the next step;
- the POD after delivery.
It never shows customs notes, declared values, specimen data or financial information.
7. Network runs (cross-border shuttles, collection circuits)
Business endpoints (runs:operate unless stated):
POST /business/network-runs: create a run withfacility_id,kind,countries, capacities (kg, m³, pieces, cold slots), vehicle and driver.POST /business/network-runs/{run}/stops: add a stop.deliver, with a waybill;collect, with a work order (sender, recipient,courier_client_id) and reserved capacity;checkpoint, withcountry,to_countryandborder_post.
POST /business/network-runs/{run}/{depart|arrive|unload|reorder|transfer|report|close|waiting|expenses}.waitingneedsbilling:administer.expensesneeds a receipt file and anidempotency_key.
POST /business/network-runs/{run}/stops/{stop}/{border|cross|deliver|collect|fail}.- border: the driver records
presented; onlycustoms:approverecordsclearedorrefused(with a reason), exit before entry. - collect: label each piece; taking more than was reserved needs
runs:overrideand a reason. - Every stop action accepts an
idempotency_key. A replay after lost signal returns the first answer.
- border: the driver records
- Customs:
GET /business/shipments/{reference}/customs;POST /business/customs/{crossing}/supply(documents);POST /business/customs/{crossing}/move.- Holds, refusals and requests for documents need a reason.
- A clearance needs the customs reference.
- A release needs evidence.
- Duties need an amount, currency, payer and evidence.
- Laboratory samples:
POST /business/shipments/{reference}/lab/{profile|tick|temperature|approve}.- Temperature readings take value, unit (C or F), source, device and time, and are append-only.
- A late sample, an out-of-range reading or a broken seal blocks handover until someone with
runs:overriderecords the laboratory's decision.
- Checklists:
GET|POST /business/compliance-checklists. They are versioned and supplied by the courier's clearing agent and laboratories. Areye assumes no requirements of its own.
A run closes only when every item has an explicit end state: delivered, laboratory handover, customs hold (held by customs), returned to origin, transferred, or missing or damaged with an exception.
A driver signed in with their Areye driver account can call the matching /driver/network-runs/…, /driver/customs/… and /driver/shipments/{reference}/lab/{tick|temperature} endpoints, limited to runs assigned to that driver; the driver's view of a run never shows declared or customs values. The Areye driver app does not have a network-run screen yet, so until it does, operations records these steps through the business endpoints above.
7a. Your clients' API (/api/client)
Every client of your courier service (a shop, a retailer) has its own login and API key. Every call sends X-Client-API-Key: either the session token from POST /client/login (email and password; a first, sent password must be changed with POST /client/password before anything else) or the client's API key. A client sees only its own shipments, paper, people and money. Every call is refused while your courier network is switched off.
| Method | Path | What it does |
|---|---|---|
| POST | /client/login |
Sign in (email, password); returns a session token |
| POST | /client/logout |
End this session |
| GET | /client/me |
Who is signed in, and the client account |
| POST | /client/password |
Change the signed-in person's password |
| GET | /client/shipments |
The client's shipments (paged) |
| POST | /client/shipments |
Book one shipment |
| POST | /client/shipments/bulk |
Book up to 200 shipments in one call |
| POST | /client/shipments/csv |
Book from a CSV upload |
| GET | /client/shipments/{waybill} |
One shipment with its custody timeline |
| GET | /client/shipments/{waybill}/label |
Label PDF (one per piece) |
| GET | /client/shipments/{waybill}/waybill |
Waybill PDF |
| GET | /client/shipments/{waybill}/packing-slip |
Packing slip PDF, one page per piece |
| POST | /client/prices |
Instant delivery options and prices (alias: /client/delivery-options) |
| POST | /client/delivery-options |
Same as /client/prices |
| POST | /client/book |
Book a priced option from /client/prices |
| POST | /client/quote |
Compatibility alias of /client/prices |
| GET | /client/quotes |
Saved quotes |
| POST | /client/quotes |
Save a quote |
| POST | /client/quotes/{reference}/book |
Book a saved quote |
| GET | /client/claims |
The client's claims and their outcome |
| GET | /client/collection-manifest |
Today's collection manifest |
| GET | /client/stores |
The client's stores (retail packet programme) |
| POST | /client/stores/{store}/packets/{action} |
Store packet actions: stock, sell, refund, activate, accept, hand-over |
| GET | /client/addresses |
Saved addresses |
| POST | /client/addresses |
Save an address |
| GET | /client/users |
The client's users |
| POST | /client/users |
Invite a user (client administrators only) |
| POST | /client/api-key |
Rotate the client's API key (shown once) |
| POST | /client/webhook |
Set the client's webhook URL (public HTTPS only); returns its signing secret, shown once |
| GET | /client/account |
Money with the courier: balance, credit, invoices |
The client's own webhook
A client that sets a webhook URL receives shipment.<stage> events for its own shipments only, signed with its own secret (never the courier's): the same headers as business webhooks (X-Areye-Signature, X-Areye-Signature-V2 with X-Areye-Timestamp, X-Areye-Event, X-Areye-Delivery), the same retries, at-least-once (dedupe on id). The envelope carries id, event_id, event, client (the client code), timestamp and data:
{"id": "8b6e…", "event_id": "8b6e…", "event": "shipment.delivered", "client": "BOK", "timestamp": "2026-10-17T10:04:00+02:00",
"data": {"waybill": "AW-7Q2K9", "client_order_reference": "ORD-1001", "stage": "delivered", "status": "delivered", "condition": "good", "at": "2026-10-17T10:03:51+02:00"}}
shipment.estimate_changed is sent when a delivery estimate moves. Only customer-safe fields are sent.
8. Testing your integration
- Ask Areye for a sandbox courier with the network switched on, a test client and a test webhook secret.
- Verify your signature check against a sample event before going live.
- Areye never runs automated tests against live dispatch. Test bookings belong in the sandbox.
Changelog and deprecation policy
Versioning. This guide describes v1. Within v1 we only make additive changes: new endpoints, new optional request fields, new response fields, new event types and new enum values you can safely ignore. A field is never repurposed and never changes type or meaning.
Breaking changes (removing or renaming a field, changing a meaning or a status flow) only ever come as a new version, visible in api_version and X-Areye-Api-Version. The old version keeps working at least 90 days after the new one is announced, and both run side by side. For events that carry money or custody, the old version is not switched off until every integrator using it has demonstrably moved — even after 90 days. Deprecations are announced by email to each integrator's technical contact and listed here.
Your side: ignore fields and event types you do not recognise; never depend on the order of JSON keys; treat amounts as decimals in the stated currency.
| Date | Change |
|---|---|
| 2026-10-11 | v1 published: instant prices and booking, immutable rate-card versions, client settlement modes, signed billing events and export lifecycle, external references, long-running shipment lifecycle and alerts, network runs, scopes, driver work permissions. |