← API documentation

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:

  1. transport (or the corridor price);
  2. collection;
  3. delivery;
  4. service-level adjustment;
  5. liability cover;
  6. negotiated discount;
  7. surcharge;
  8. minimum-charge top-up;
  9. 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}
  • 201 returns the waybill and a tracking URL.
  • Sending the same request again returns 200 with "already_booked": true and 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_reference for a client, the same external reference twice answers 409 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}/versions publishes new terms as the next version, from effective_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-cards lists 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_event and a stable reference. 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 recoverable and kept apart from charges. They are not revenue.
  • Currency: a conversion records its rate and source. Without a rate the entry stays unresolved_fx and 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_reference with any state. One reference is kept per state, the first one reported for that state. The event returns external_references (by state), reported_state (the furthest state reported) and external_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-levels takes, 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:

    1. an open exception;
    2. something the courier owns that is past due;
    3. customs papers the courier must supply;
    4. waiting on customs;
    5. waiting on the customer;
    6. at risk;
    7. 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_health gives 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/report gives 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}/assign with {"to": "Thabo"}
  • POST /business/courier/alerts/{id}/acknowledge
  • POST /business/courier/alerts/{id}/resolve with {"resolution": "…", "evidence": "…"} or an evidence_file upload. 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 with facility_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, with country, to_country and border_post.
  • POST /business/network-runs/{run}/{depart|arrive|unload|reorder|transfer|report|close|waiting|expenses}.
    • waiting needs billing:administer.
    • expenses needs a receipt file and an idempotency_key.
  • POST /business/network-runs/{run}/stops/{stop}/{border|cross|deliver|collect|fail}.
    • border: the driver records presented; only customs:approve records cleared or refused (with a reason), exit before entry.
    • collect: label each piece; taking more than was reserved needs runs:override and a reason.
    • Every stop action accepts an idempotency_key. A replay after lost signal returns the first answer.
  • 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:override records 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.