What you can build
One API for rides, deliveries and car rental — plugged into your own website, app or back-office. A few of the ways companies use Areye Business:
Hotels & lodges
Offer guests airport transfers, city rides and self-drive car rental — booked from your own website or front desk, without owning a single vehicle.
POST /business/bookPOST /hire/{vehicle}/bookGET /airport/terminals Airlines & travel
Issue airport pickup / drop-off vouchers and bundle them with tickets. Your passenger redeems the ride; you settle centrally.
POST /couponsPOST /business/book Insurers
Move policyholders to and from hospital on demand or on a recurring schedule, and stand up a fleet for a claim — all from your own CMS or staff tools.
POST /business/bookPOST /subscriptionsPOST /hire/{vehicle}/book Corporate transport desks
Let your booking or calendar system order staff rides with expense codes, and reconcile spend from monthly statements.
POST /business/bookGET /business/tripsGET /business/billing/statements Retail & wholesale
Trigger Areye the moment an order lands — instant pickup for same-hour delivery, or queue a next-morning batch of up to 50 stops.
POST /business/bookPOST /batches Restaurants & takeaways
Hand delivery to Areye the moment an order is ready — dispatched to the nearest driver, with a public tracking link and a webhook when it is done.
POST /business/bookGET /trip-track/{reference}trip.completed Grocers & pharmacies
Dispatch every order to a geo-available, crowd-sourced driver pool — one-off for instant delivery or batched for a scheduled run. COD + age-check supported.
POST /business/bookPOST /batchescod.collected Authentication
The Areye Business API uses API key authentication. Include your key in the X-Business-API-Key header on every request. You can find and rotate your key in the Business Portal under Settings & API.
X-Business-API-Key: your_api_key_here
Keep your API key secret. Never expose it in client-side code or public repositories. Use environment variables. Rotate your key immediately from the Business Portal if you suspect it has been compromised.
Endpoints
All paths are relative to the base URL https://api.areye.app/api. Every request is authenticated with your X-Business-API-Key unless marked public.
Bookings & Trips
/business/book
Book a ride, parcel, cargo or airport transfer on behalf of an employee or guest — supports scheduling and up to 3 stops
/business/trips
List all trips (filter by date range, employee, status, service type)
/business/trips/active
List trips currently in progress
/business/trips/{id}
Retrieve a single trip with receipt and route details
/business/trips/{id}/cancel
Cancel a trip before driver assignment (full refund to wallet)
/business/trips/{id}/waybill
Download the printable waybill for a delivery
/trip-track/{reference}
Public shareable tracking link — no authentication required
Merchant Orders & Delivery
/business/book
Same booking endpoint — add order_payload, prep_minutes, cod_amount or shop_deliver to turn a delivery into a merchant order
/business/trip/{reference}/ready
Signal the order is packed and ready for collection — releases the waiting driver immediately
/business/book
Age-restricted goods: add age_verification_required to force a photo-ID check at the door before handover
/trip-track/{reference}
Customer-facing live tracking — no auth, safe to embed in your own app or SMS
/trip-chat/{reference}
Read the customer/driver chat thread for an order (token-authed by reference)
/trip-chat/{reference}
Post a message into the order chat as the customer
Batches & Subscriptions
/business/batches
Submit a bulk delivery batch (segment: parcel or cargo, up to 50 stops)
/business/batches
List batches with per-stop status
/business/batches/{id}
Retrieve a single batch with all stops
/business/subscriptions
Create a recurring delivery (parcel/cargo, days of week, pickup time)
/business/subscriptions
List recurring delivery subscriptions
/business/subscriptions/{id}/{action}
Pause, resume or cancel a subscription
Car Rental & Fleet
/business/rentals/vehicles
List your rental fleet with rates, deposits and availability
/business/rentals/vehicles
Add a vehicle to your rental fleet (photos, daily rate, km limits)
/business/rentals/vehicles/bulk
Bulk-add vehicles to your fleet
/business/rentals/vehicles/mode
Bulk-set listing mode (rental / ride-hailing / both)
/business/rentals/vehicles/{id}
Update a vehicle — rates, deposit, marketplace publishing
/business/rentals/vehicles/{id}
Remove a vehicle from your fleet
/business/rentals/bookings
List rental bookings against your fleet
/business/rentals/bookings
Create a rental booking (self-drive or chauffeured)
/business/rentals/bookings/{id}/{action}
Confirm, complete or cancel a rental booking
Rent-to-Own & Driver Applications
/business/rto
List rent-to-own agreements against your vehicles
/business/rto
Create a rent-to-own agreement (earnings-skim or direct-pay, chosen cadence)
/business/rto/{id}/payments
Record an instalment payment toward an agreement
/business/fleet-applications
List driver applications for your marketplace-published vehicles
/business/fleet-applications/{id}/{action}
Approve or reject a driver application (assigns the vehicle / opens the agreement)
Wallet, Payouts & Billing
/business/wallet
Get current wallet balance and billing type
/business/wallet/transactions
List wallet transactions (debits, credits, top-ups)
/business/wallet/topup
Initiate a wallet top-up via a payment gateway
/business/wallet/topup/{ref}/status
Poll the status of a wallet top-up
/business/payment-gateways
List available payment gateways for your zone
/business/payouts
List earnings payout requests
/business/payouts
Request a payout of positive earnings
/business/payouts/{id}
Cancel a pending payout request
/business/billing/statements
List monthly billing statements
People, Groups & Coupons
/business/people
List all employees and their spending limits
/business/people
Invite an employee to your business account
/business/people/{id}
Update employee details, group or spending limit
/business/people/{id}
Remove an employee from your account
/business/groups
List groups for organising employees
/business/groups
Create a group
/business/coupons
List employee discount coupons
/business/coupons
Issue a new coupon (fixed or percent)
/business/coupons/{id}/deactivate
Revoke a coupon
Reference Data
/business/vehicle-types
List vehicle types with live pricing and availability
/business/parcel-sizes
List parcel size tiers with rates
/business/zones
List active service zones
Account
/business/profile
Retrieve your business account profile and settings
/business/profile
Update company name, billing contact, webhook URL or low-balance threshold
/business/api-key
Retrieve API key metadata
/business/api-key/regenerate
Rotate your API key (previous key invalidated immediately)
/business/dashboard
Aggregate account metrics — trips, spend, active employees
Example: Book a ride for an employee
curl -X POST https://api.areye.app/api/business/book \
-H "X-Business-API-Key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"type": "ride",
"passenger_name": "Jane Smith",
"passenger_phone": "+267XXXXXXXX",
"pickup_address": "Plot 64271, Block 3 Industrial, Gaborone",
"pickup_lat": -24.6282,
"pickup_lng": 25.9231,
"dropoff_address": "Molapo Crossing Mall, Gaborone",
"dropoff_lat": -24.6540,
"dropoff_lng": 25.9110,
"vehicle_type_id": 1, // optional override — prefer declaring the goods (see below)
"expense_code": "TRAVEL-Q3"
}'
Choosing the vehicle
Describe what you are sending and Areye dispatches a vehicle that can carry it. You do not
need to know our fleet, and you should not hard-code vehicle_type_id
— a refrigerator booked as a sedan job is exactly the failure this prevents.
Call GET /business/parcel-sizes first. It returns
the live size ladder — size_key, the kilogram range,
the eligible vehicle types and the rates for each band. It is the authoritative answer to
"which size is my package?" and it changes without notice to this page, so read it rather
than hard-coding the bands below.
All four fields are optional. Send whichever you have. Send none and the
booking behaves exactly as it always did. They combine freely: a weight and a body type
together are both honoured. If parcel_weight_kg and
parcel_size disagree — 2 kg declared as
oversized, say — the larger of the two wins,
so a consignment is never under-served because two declarations contradicted each other.
| Field | Values | Use it when |
|---|---|---|
| parcel_weight_kg | any number | You know the weight. Simplest option. |
| parcel_size | small · medium · large · oversized | Shoebox · shopping bag · car boot · TV, bicycle, appliance. |
| cargo_body_type | closed · open · refrigerated | The load needs a particular body — an open bakkie for building materials, a closed body for anything weatherproof. Refrigerated is commercial cold-chain for bulk consignments; a retail order with frozen items does not need it and should not declare it. |
| cargo_capacity_class | half_ton · one_ton · three_ton · eight_ton | A minimum capacity, not an exact match. |
You are billed for the vehicle your goods require. Areye always dispatches the smallest vehicle that can do the job, so declaring accurately never costs more than the job needs — but over-declaring dispatches and bills a larger vehicle. On a typical route the oversized band costs around four times the small band. Under-declaring causes a failed collection; over-declaring is simply expensive. Declare what you are actually sending.
vehicle_type_id remains an override. When the goods
need more vehicle than the class you named, your account decides what happens: by default Areye
upgrades and bills the larger class. Accounts can instead cap upgrades at one size, or refuse the
order and return 422 with the reason. Declare nothing and nothing
changes — existing integrations are unaffected.
Example — a refrigerator
curl -X POST https://api.areye.app/api/business/book \
-H "X-Business-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "parcel",
"passenger_name": "Recipient Name",
"passenger_phone": "+267XXXXXXXX",
"pickup_address": "Store address",
"pickup_lat": -24.6282, "pickup_lng": 25.9231,
"dest_address": "Delivery address",
"dest_lat": -24.6531, "dest_lng": 25.9089,
"parcel_weight_kg": 85,
"cargo_body_type": "closed"
}'
No vehicle_type_id is sent. Areye resolves 85 kg to the
oversized band, sees that a closed body is required, and dispatches a cargo van or bakkie
— never a sedan or a motorbike.
Batches — one driver, one vehicle
A batch submitted to POST /business/batches is collected by a
single driver in a single vehicle, so Areye fits that vehicle to the
largest stop in the run. One oversized item among twenty small ones sends a
bakkie, not a motorbike.
Set each stop's size to
small, medium,
large or oversized
(s / m /
l / xl are accepted too).
Each stop is priced on its own size; the vehicle is chosen for the whole run.
cargo_body_type and
cargo_capacity_class may be set at the top level to apply to
the entire batch — a closed-body run, for example.
The per-stop field is size (not
parcel_size — that name belongs to
/business/book). A stop may also carry
weight_kg, and where a stop gives both, the larger of the two
decides that stop — for its own price and for the batch's vehicle.
curl -X POST https://api.areye.app/api/business/batches \
-H "X-Business-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"segment": "parcel",
"stops": [
{
"recipient_name": "First Recipient",
"recipient_phone": "+267XXXXXXXX",
"pickup_address": "Store", "pickup_lat": -24.6282, "pickup_lng": 25.9231,
"dest_address": "Drop 1", "dest_lat": -24.6531, "dest_lng": 25.9089,
"size": "small"
},
{
"recipient_name": "Second Recipient",
"recipient_phone": "+267XXXXXXXX",
"pickup_address": "Store", "pickup_lat": -24.6282, "pickup_lng": 25.9231,
"dest_address": "Drop 2", "dest_lat": -24.6402, "dest_lng": 25.9155,
"weight_kg": 85
}
]
}'
The first stop is priced as a small parcel and the second as oversized, but the whole run is collected by a vehicle able to carry the 85 kg item.
If no vehicle can satisfy the batch, the request is rejected with
422 and nothing is created — a batch fails whole, never
part-way through.
Verifying what Areye decided
The 201 response, GET /business/trips
and GET /business/trips/{reference} all echo what you declared
and what we resolved, so you never have to infer it from the fare:
{
"parcel_size": "oversized",
"parcel_size_label": "Oversized",
"parcel_weight_min_kg": 30.0,
"parcel_weight_max_kg": 500.0,
"cargo_body_type": "closed",
"cargo_capacity_class": null,
"volumetric_weight_kg": null,
"vehicle_class": {
"id": 5, "name": "Cargo Van",
"body_type": "closed", "capacity_class": "one_ton"
},
"vehicle_upgraded": true,
"vehicle_upgrade_reason": "Budget cannot carry this consignment (Oversized) — dispatched Cargo Van instead.",
"test": false
}
vehicle_class is the class the order will actually be sent.
vehicle_upgraded tells you whether we sent something larger
than your request implied, and why.
Custody scanning — proving which bag
A collection photo and a driver signature prove that a handover happened. They do not prove which bag moved — so an unlabelled bag handed to the wrong driver still produces a perfect-looking record. Custody scanning closes that gap, and it is a premium add-on: ask Areye to enable it on your account.
When it is enabled, every merchant order comes back with a scan_code
and a scan_label_url. Nothing else about your integration changes.
{
"reference": "BIZ-1A7DED",
"scan_code": "4EY2-Y6GD",
"scanned_at": null,
"scan_label_url": "https://api.areye.app/api/business/trips/BIZ-1A7DED/label"
}
What your picker app does
- Finish picking and packing the order as you do today.
- Fetch
scan_label_url— an A6 PDF label carrying the QR (encodingscan_code) with your order number printed underneath it. Print it and stick it on the bag. - Show the same code on your handover screen, so the person releasing the bag can match the physical label to the electronic one before the driver takes it.
The QR is Areye's custody token; the order number is for your staff and the driver reading it
by eye. Two audiences, one sticker. If you would rather generate the label yourself, encode
scan_code verbatim as a QR — the driver's scanner accepts it
with or without the hyphen, and in any case.
What the driver does
The driver scans the label (or types the code beneath it) before collecting. They cannot mark the order collected until that scan matches. Scanning a label from a different order is rejected with “that label belongs to a different order” — which is the whole point: the mismatch surfaces while the driver is still standing in your store, not at a customer's door.
Labels do get torn and printers do jam, so a driver may force collection through. That is never
silent: the override and its reason are recorded on the order, and
scanned_at stays null so a forced
collection is always distinguishable from a real scan.
| Field | Meaning |
|---|---|
| scan_code | The custody token to encode as a QR. Absent when the add-on is off. |
| scan_label_url | A ready-made A6 PDF label. Same auth header as any other call. |
| scanned_at | When the driver matched the bag. Null until then — and stays null on a forced collection. |
Sandbox bookings
A booking with "test": true moves no money and is dispatched to
nobody. Every trip representation carries "test": true|false, and
GET /business/trips?test=0 hides sandbox trips from your live
list (?test=1 shows only them; omit it for everything).
{
"success": true,
"data": {
"reference": "ARY-8F3A21",
"status": "pending",
"type": "ride",
"fare": 85.00,
"currency": "BWP",
"tracking_url": "https://api.areye.app/api/trip-track/ARY-8F3A21"
}
}
Booking a delivery instead? Set "type": "parcel" or "cargo" and add recipient details. Track any booking via the returned reference.
Webhooks
Set your Webhook URL and reveal your webhook secret in Business Portal → Settings & API. Every POST carries X-Areye-Event (event name), X-Areye-Signature (sha256=HMAC-SHA256(raw_body, secret) — verify constant-time before trusting it) and X-Areye-Delivery (a UUID, also id/event_id in the body). Delivery is at-least-once with retries: a 2xx (5s timeout) succeeds; otherwise Areye retries with exponential backoff — 6 attempts over ~8.5h, then exhausted. Build your receiver idempotent (dedupe on id), tolerant of out-of-order arrival. Match events to your order on data.reference or your own data.external_reference — not the top-level id.
| Event | Description |
|---|---|
trip.accepted |
A driver accepted the job. data: {..., driver_name, driver_phone, vehicle} |
trip.picked_up |
Collected from your store, en route to the customer (data.status: active) |
trip.completed |
Delivered. Full manifest: amount (final settled), picked_up_at, completed_at, pod_photo_url, pod_signature_url, pod_at, age_verification_outcome, cod_status, cod_collected_amount |
trip.cancelled |
Cancelled. data: {..., cancelled_by, reason} |
trip.age_verified |
Age-restricted: buyer passed the ID check (age_verification_outcome: verified) |
trip.age_verification_failed |
Age-restricted: buyer failed. Goods return to store, no COD. data: {reason, age_verification_outcome: refused} |
cod.collected |
COD cash collected at the door. data: {declared_amount, collected_amount} |
cod.refused |
COD order: recipient did not pay (collected_amount: 0) |
cod.settlement_ready |
A batch of your collected cash is prepared for payout |
cod.settled |
Areye has paid your collected COD cash out to you |
coupon.redeemed |
An employee redeemed a discount coupon on a trip |
wallet.low_balance |
Wallet balance fell below your configured threshold |
wallet.auto_topup |
Your wallet was auto-topped-up from a saved card |
fleet.application.received |
A driver applied for one of your marketplace-published vehicles |
Rate Limits
The Business API operates on a fair-use basis suitable for normal booking and fleet-management workloads. If you are planning high-volume or bulk automation, contact our team so we can provision the right limits for your account. Endpoints that trigger external payment gateways may apply stricter per-minute limits.
Integrating a merchant platform
If you run a marketplace, restaurant group, grocery chain or online store and want Areye drivers to
deliver your orders, you do not need a separate product. You use the same
/business/book endpoint with extra
fields. Your customers stay in your app; Areye handles dispatch, the driver relationship and
proof of delivery.
What the driver actually sees
When you send order_payload, the driver
gets a full order card — your store, your item list with photos, and the money to handle — instead of
a bare pickup/drop-off pair. Every field below maps to something on that screen, so send what you
want the driver to know.
| You send | Driver sees |
|---|---|
| order_payload.store_name | Store name as the card heading |
| order_payload.external_reference | Your order number, so staff and driver can talk about the same order |
| order_payload.items[] | Itemised list — name, qty, price, options (e.g. "no onions", "sliced") |
| order_payload.items[].image | Product photo beside each line. Must be a publicly reachable URL; a broken one degrades to a placeholder rather than failing the booking |
| order_payload.order_note | Highlighted note ("leave at the gate") |
| order_payload.order_amount | "Order P348.50" chip — the value of goods carried |
| cod_amount | "Collect P348.50 cash" chip |
| bring_change_amount | "Bring change for P500" chip — send it when your customer says what note they will pay with |
| prep_minutes | Countdown so the driver is timed to your kitchen or picking floor |
The flow
- Customer orders and pays in your app, however you already collect payment.
- Your backend calls
POST /business/bookwith pickup (your store), drop-off (your customer) and the order payload. - Areye dispatches a nearby driver and starts firing webhooks.
- You call
POST /business/trip/{reference}/readywhen the order is packed. - Your customer follows the live map at
/trip-track/{reference}and can message the driver. - Driver hands over against a delivery code, plus a proof-of-delivery photo if your account requires one. Delivery fees are debited from your Areye wallet.
Important — the handover gate. If you send
handover_required: true, the driver is
blocked from collecting until you call
/ready. Use it when nothing may leave
the counter early (restaurants, prescription collection, high-value e-commerce) — but you must wire the
/ready call, or orders will sit
uncollected. Leave it false (the default)
and the driver may collect as soon as they arrive;
prep_minutes still paces them.
Creating a merchant order
POST /api/business/book
X-Business-API-Key: your_api_key_here
Content-Type: application/json
{
"type": "parcel",
"pickup_address": "Choppies Riverwalk, Gaborone",
"pickup_lat": -24.6541, "pickup_lng": 25.9237,
"dest_address": "Plot 22, Phakalane",
"dest_lat": -24.5821, "dest_lng": 25.9412,
"passenger_name": "Kagiso M.",
"passenger_phone": "+26771234567",
"prep_minutes": 15,
"handover_required": false,
"age_verification_required": false,
"cod_amount": 348.50,
"bring_change_amount": 500.00,
"order_payload": {
"store_name": "Choppies Riverwalk",
"store_phone": "+2673901234",
"external_reference": "CHP-88421",
"module_type": "grocery",
"order_amount": 348.50,
"order_note": "Leave at the gate if no answer",
"items": [
{ "name": "Milk 2L", "qty": 2, "price": 24.00,
"image": "https://cdn.example.com/milk.jpg" },
{ "name": "Brown bread", "qty": 1, "price": 12.50,
"options": "Sliced" }
]
}
}
Choosing how money moves
| Model | Send | What Areye does |
|---|---|---|
| Prepaid | order_payload only |
Customer already paid you. Driver delivers; your wallet is debited the delivery fee. |
| Cash on delivery | cod_amount
(+ optional bring_change_amount) |
Driver collects cash into a ring-fenced ledger held in custody, then Areye settles it to you and fires cod.settled. |
| Shop & deliver | shop_deliver: true + purchase_amount |
The driver is advanced a float to buy the goods, uploads the receipt, and the difference is reconciled — order.receipt_reconciled. Restricted to trusted drivers, so dispatch may take longer. |
Age-restricted orders (alcohol, tobacco)
Send age_verification_required: true on any
order containing alcohol, tobacco or anything else with a minimum age. The driver is then
blocked from completing the delivery until they have checked the buyer's photo ID and
recorded the outcome. The proof-of-delivery photo and signature captured at that moment are the evidence.
| Outcome | What happens |
|---|---|
verified |
Buyer proved they are of age. Delivery completes normally. |
refused |
No valid ID, underage, or the buyer declined to show it. The driver is instructed not to hand over and to return the goods to your store. Handle the refund on your side. |
Read the result back from any trip response —
age_verification_required and
age_verification_outcome — and use it to
decide whether to refund. A return leg is exempt from the check, since nothing is being handed to a buyer.
The licence obligation stays with you. Areye gives you an ID check at the door and a durable record of it. That is a control, not a transfer of liability — the duty to sell lawfully (licence conditions, permitted hours, refusing intoxicated or underage buyers) remains with the licensed seller. Make sure delivery windows for these orders fall inside your permitted trading hours, and only flag orders you are licensed to sell.
Webhooks you will receive
Set your endpoint on PATCH /business/profile.
Every payload carries reference — match it to the
external_reference you sent. Each request also
carries X-Areye-Event and
X-Areye-Signature
(sha256=HMAC-SHA256(raw_body, your_webhook_secret)) — your webhook_secret and Webhook URL are on the portal Settings & API page (separate from your API key) —
verify it with a constant-time comparison before trusting the payload.
| trip.accepted | Driver took the order — includes driver_name. Show "driver on the way". |
| trip.picked_up | Collected from your store, en route to the customer |
| trip.completed | Delivered — close the order on your side |
| trip.cancelled | Includes cancelled_by and reason |
| cod.settlement_ready | Collected cash reconciled and ready to settle |
| cod.settled | Cash settled to your account |
| order.receipt_reconciled | Shop & deliver: driver receipt matched against the float |
| wallet.low_balance | Top up before deliveries start failing |
Before you go live
- Fund your wallet, or ask us to enable invoiced billing — delivery fees are debited per order, and a flat wallet stops dispatch.
- Send us your store list and service areas so we can confirm driver coverage around each branch.
- Ask us to enable proof-of-delivery photos if you need them for disputes.
- Selling alcohol or tobacco? Flag those orders with
age_verification_required, keep delivery windows inside your licensed trading hours, and handle refunds for refused handovers. - Serve item images over HTTPS from a public URL — signed or expiring links will show as placeholders.
- Store our
referenceagainst your order. It is the key for tracking, chat, webhooks and support. - Dedicated driver pools, default prep times and shop-and-deliver access are configured per account — tell us your volumes and we will set them.
Sandbox & test mode
Add "test": true (or "dry_run": true) to POST /business/book to book without dispatching a real driver or moving money. You get a real reference, and the full signed webhook lifecycle (trip.accepted → picked_up → completed) fires to your Webhook URL just after the response returns — so you can verify your receiver end to end. Flip it to false for live orders.
Fetch, cancel & the delivery manifest
GET /business/trips/{reference} returns one order by reference or id; GET /business/trips?reference=… (or ?search=…) does server-side lookup. POST /business/trips/{reference}/cancel (body reason) cancels a not-yet-picked-up order and refunds any charge. The manifest — on both the record and the trip.completed webhook — carries the timeline (accepted/picked_up/arrived/completed), proof of delivery (pod_photo_url, pod_signature_url — absolute HTTPS, downloadable, your dispute evidence — and pod_at), the age result and COD status.
Account billing mode, final amount & /ready
Read billing_type on GET /wallet. Prepaid blocks a booking the wallet can't cover; postpaid dispatches on credit up to credit_limit — balance stays 0 and the amount owed is the positive amount_owed. This account billing mode is separate from the per-order money model (prepaid / COD / shop_deliver). billing_amount on the 201 is a provisional quote; the final settled charge is amount on trip.completed. POST /trip/{reference}/ready is only required for orders booked handover_required: true; a per-booking prep_minutes overrides your account default.
Money-field glossary & test button
Same value, different name by surface: declared COD is cod_amount (trip) / declared_amount (cod.collected); collected COD is cod_collected_amount / collected_amount. The authoritative delivery charge is amount on trip.completed (billing_amount is the provisional quote; final_fare_amount is set only if a completion recalc changed it). On Settings & API, Send test webhook POSTs a signed sample to your URL so you can confirm your HMAC check (and catch a stale secret) in one click.
Rewards & Gift Cards
Areye is the parent registry for coupons and gift cards across the whole suite. One code is minted once, and every platform that accepts it — Areye itself and any merchant integrated with these endpoints — validates and redeems it against the same ledger. That means a customer cannot spend the same code twice on two different platforms, and you get one reconciliation trail instead of several.
Areye is the primary redemption surface. A code minted here is, first and foremost, spend-on-Areye value — rides, deliveries, cargo and car hire. Dijo, Mmaraka and other participating merchants are secondary surfaces that the same code also happens to work on. When you issue a batch you are issuing value your customer can spend on Areye today, and in your own store as a bonus — not the other way round.
Authentication. Every merchant endpoint in this section is authenticated with your business
API key in the X-Business-API-Key header —
not an Authorization: Bearer token.
All paths below are relative to https://api.areye.app/api.
The public storefront endpoints at the end of this section take no key at all.
Two instruments, two very different money models
Read this before you issue anything. The difference is not cosmetic — it decides when your wallet is debited and what happens to value the customer does not use.
Reward coupon (discount) |
Gift card (gift_card) |
|
|---|---|---|
| Spending behaviour | Consumed in full, once. A P20 coupon against a P15 fare gives P15 and the remaining P5 is simply lost — the code is spent either way. One live use per code. | Stored value that draws down. A P500 card against a P120 order leaves P380 on the card, and that balance follows the customer across further orders and across platforms until it is gone. |
| When your wallet is debited | Depends on the discount type. A percentage reward costs nothing to mint — each code a customer actually uses debits the issuing merchant’s wallet for the discount granted, and unused codes never cost you a thebe. A fixed-amount reward is different: it is minted as stored value, so the whole batch face value is debited at issuance, exactly like a gift card, and the recipient keeps any unspent balance. | At issuance. Gift cards are prepaid stored value: the whole batch face value (unit value × quantity) is debited the moment the batch is minted. The float then sits with Areye and each redemption draws it down. |
| Solvency check at issue | Wallet must cover the batch at maximum exposure (fixed value, or max_discount for percent codes), else 422. |
Wallet must cover the full face value, else 422 telling you exactly how much to top up. |
| PIN | Not used. | Bearer cards carry a 4-digit PIN, returned at issuance and never retrievable again. Send pin on validate, redeem and balance. A card issued straight to a named Areye account carries no PIN — the account itself is the proof of ownership. |
| Activation | Live as soon as it is issued (subject to starts_at). |
Not spendable until activated — either activate: true at issuance, or at the till via /gift-cards/activate. |
| Currency | No FX conversion, ever. A code prices only in its own currency; a mismatch is refused with “This coupon can only be used in BWP”. Run multi-currency campaigns as sibling batches. | |
Three ways a gift card is redeemed
All three are live at the same time, and all three move the same balance on the same card. You do not choose one at issuance — the holder chooses at spend time.
1. Drawn down at checkout
The customer types the code (and PIN) at your checkout. You call /validate then /redeem, and only the order amount comes off the balance. The rest stays on the card.
2. Loaded into the Areye wallet
In the Areye app the holder can load the card whole into their wallet. The entire remaining balance is redeemed in one move and becomes wallet credit, spendable on any Areye service without re-entering the code.
3. Assigned to an account
A card can be issued directly against an existing Areye account. It then appears in that customer’s Promotions screen with nothing to type and no PIN to remember — they simply tap it at checkout.
Settlement: who bears the discount, who gets paid back
How a code is consumed
Every code also carries a redemption mode,
which decides what happens the first time it is used. You rarely set this yourself — it is derived
from the instrument type — but it is returned on every code and is worth understanding.
| Mode | Behaviour |
|---|---|
| once | Consumed whole on first use, whatever the order is worth. A percentage discount works this way: use it and it is spent. |
| balance | Spends down. Each redemption draws only what the order needs and the remainder stays on the code for next time. Gift cards and fixed-amount rewards work this way, which is why both are prepaid at issuance. |
| Funding | Where the money comes from | Is the honouring merchant credited? |
|---|---|---|
| gift_card | The buyer already prepaid Areye and Areye holds the float. Nothing is debited at redemption — the card’s stored value is simply drawn down. | Yes. Your wallet is credited the amount you honoured. |
| platform | Areye’s own marketing budget. No merchant wallet is touched to fund it. | Yes. Areye is running the promotion, so Areye pays for it. |
| issuer | The issuing merchant’s wallet is debited at the moment of redemption. If the balance cannot cover it the whole redemption fails rather than granting an unfunded discount. | Yes — unless the issuer is also the merchant honouring it. A shop redeeming its own promo owes itself nothing, so both the debit and the credit are skipped and it nets to zero. |
| merchant | The merchant honouring the code bears the discount out of their own margin. | No. No reimbursement is written. |
Batches minted through POST /business/coupons/issue
are issuer-funded reward coupons, or prepaid
gift_card stored value when
instrument_type says so. Reversing a redemption
claws back both sides: the reimbursement is debited off the merchant who no longer gave anything
up, and a wallet-funded issuer gets their money back. A gift card gets its drawn-down value returned to the
balance. That is why /reverse is not optional.
Redemption lifecycle
Three calls, in this order. /validate is a dry run you make
while the customer is still on the checkout screen; /redeem is the
committing call and the only place a usage counter or a stored balance ever moves;
/reverse puts the value back if the order later fails.
| Endpoint | Body | What it does |
|---|---|---|
| POST /business/coupons/validate | code, currency, cart_total, optional user_key, merchant_ref, platform, pin |
Prices the code against the cart and returns the value that would apply. Changes nothing. Safe to call as often as you like. |
| POST /business/coupons/redeem | As above, plus order_amount and external_order_ref (both required) instead of cart_total |
Commits the redemption under a row lock, re-checking every cap. Idempotent on external_order_ref — retrying the same order reference returns the original redemption rather than double-spending. Always send your own order id. Because you honoured the value, you are the party reimbursed — see settlement above. |
| POST /business/coupons/reverse | redemption_id, or coupon_id + external_order_ref (with platform) |
Releases a redemption on refund or cancellation. A discount code becomes usable again; a gift card gets its value back on the balance; settlement is unwound on both sides. Idempotent — reversing twice is a safe no-op. If you never call this, the value stays consumed. |
/validate returns
valid,
reason (a message safe to show the customer when
invalid), discount_amount,
discount_type,
coupon_id,
currency,
instrument_type,
stackable, and for gift cards
remaining_value plus
remaining_after so you can show the customer what
will be left on the card. /redeem answers
200 with
redemption_id and
idempotent, or
422 with a reason (expired, already used, below
min_spend, wrong currency, out of scope, bad PIN,
issuer wallet short). Treat any non-200 as “no discount applied” and charge the full amount.
POST /api/business/coupons/redeem
X-Business-API-Key: your_api_key_here
Content-Type: application/json
{
"code": "AREYE-7K2MQX",
"currency": "BWP",
"order_amount": 120.00,
"external_order_ref": "SHOP-88421",
"user_key": "+26771234567",
"merchant_ref": "riverwalk",
"pin": "4821"
}
Issuing a batch
POST /business/coupons/issue mints up to
1,000 codes at once and returns them, along with one-time PINs when the batch is gift cards. Set
instrument_type to
discount (default) or
gift_card. Codes are generated from an
unambiguous character set with no 0/O/1/I/L, so they survive being printed and read over the phone.
| Field | Notes |
|---|---|
| quantity | Required. 1–1000 codes in the batch. |
| currency | Required. The wallet debited is the wallet for this currency. |
| instrument_type | discount (default) or gift_card. |
| discount_type / discount_value | Required for discount batches. fixed or percent; percent cannot exceed 100. |
| max_discount | Caps a percent code — and is what your exposure is calculated against. An uncapped percent batch is treated as zero exposure, so cap it. |
| value | Required for gift-card batches. Face value per card. Debited in full at issuance. |
| activate / activation_channel | Gift cards activate immediately by default; send activate: false to leave them dormant for till activation. Channel is one of web, retail, ecommerce, admin. |
| prefix / title / batch_label | Your own branding on the codes and your reference for the batch. |
| scope_platforms / scope_merchants | Arrays restricting where the codes may be spent. Omit for suite-wide (["*"]). |
| min_spend / starts_at / expires_at / stackable | Standard promo controls. expires_at must be in the future. |
POST /api/business/coupons/issue
X-Business-API-Key: your_api_key_here
Content-Type: application/json
{
"instrument_type": "gift_card",
"quantity": 50,
"currency": "BWP",
"value": 200.00,
"prefix": "STAFF",
"batch_label": "Q3 staff rewards",
"activate": true,
"activation_channel": "admin",
"expires_at": "2027-12-31T23:59:59+02:00"
}
201 Created
{
"success": true,
"data": {
"batch_id": "BATCH-XXXXXXXXXX",
"issued": 50,
"coupons": [
{ "id": 9001, "code": "STAFF-4H8P2Q", "pin": "8140", "value": 200 }
],
"note": "PINs are shown once and cannot be retrieved later ..."
}
}
Store the PINs on the 201. They are hashed on our side and cannot be recovered. The batch above debits your BWP wallet P10,000 (50 × P200) immediately, before the first card is ever spent. If minting fails after the debit, the float is credited straight back.
Managing batches and cards
| Endpoint | What it does |
|---|---|
| GET /business/coupon-batches | Lists your batches with total, active, redeemed, outstanding_value and issued_at — your reconciliation view over the float you are carrying. |
| PATCH /business/coupon-batches/{batch}/revoke | Kills the codes in a batch (leaked print run, cancelled campaign) and returns how many were disabled; 404 if the batch is not yours or does not exist. Already-redeemed value is not clawed back. |
| POST /business/gift-cards/activate | Till activation. Body: code, optional activation_channel and purchase_reference. Cards you print and rack unactivated are worthless until this call, so a stolen card off the shelf is not spendable. Only the issuing business may activate its own cards; idempotent on an already-live card. |
| POST /business/gift-cards/balance | Body: code, optional pin. Returns currency, initial_value, remaining_value, activated, expires_at, status. Wrong PIN is a 403; unknown card a 404. |
Public gift-card storefront
Consumers buy Areye-issued cards on the storefront at
areye.app/gift-cards, which uses the
unauthenticated, rate-limited /api/gift-cards/*
family. You can drive the same flow from your own site. Nothing spendable is minted until the gateway
confirms the money — never from the browser.
| Endpoint | What it does |
|---|---|
| GET /gift-cards/options | Optional ?currency=. Returns the offered denominations, supported currencies, the gateways that can actually take money in that currency, and the terms. |
| POST /gift-cards/purchase | Records an intent to buy. Body: amount, currency (BWP or ZAR), buyer_name, buyer_email, optional buyer_phone, recipient_name, recipient_email, recipient_phone, message, gateway. Returns a reference. No card exists yet. |
| POST /gift-cards/pay | Body: reference, gateway, optional msisdn for mobile money. Returns either a redirect_url for card gateways or on-screen instructions for a mobile-money push. |
| POST /gift-cards/confirm | Body: reference. Asks the gateway whether the money actually arrived and, if so, mints and delivers the card. Safe to call repeatedly — the browser return, the webhook and the cron sweep all call it and only one card is ever minted. 202 while payment is still unconfirmed. |
| GET /gift-cards/purchase/{reference} | Buyer-facing status poll. Returns status, amount, currency, and the code once paid. The PIN is delivered to the recipient by email and SMS and is never re-exposed here. |
| POST /gift-cards/balance | Body: code, optional pin. The public “how much is left on my card?” lookup. Wrong PIN is a 403. |
Stored value can only be backed by money we can see. A gift card is minted only against a gateway confirmation that carries a positive collected amount matching the order. Cash and deferred gateways — the ones that report “paid” because a driver will collect on arrival — are refused outright at both payment initiation and confirmation. A gift card has no delivery leg to collect on, so accepting them would hand out free spendable value. Underpayments and zero-amount confirmations are rejected and logged.
Legacy promos and the unified code box
Areye had its own promotions system before the suite-wide registry existed. The two now resolve through one path: a passenger types a code into a single box, and Areye checks the suite registry first and the legacy promo table second. The customer never has to know which system their code lives in, and the booking screen and the Promotions screen can never disagree about what a code is worth.
| Family | Endpoints | Reach |
|---|---|---|
| Suite registry (use this) | POST /business/coupons/validate POST /business/coupons/redeem POST /business/coupons/reverse POST /business/coupons/issue |
Suite-wide. Coupons and gift cards, one ledger, settlement and reversal built in. |
| Legacy per-business promos | POST /business/coupons GET /business/coupons GET /business/coupons/{promo} PATCH /business/coupons/{promo}/deactivate |
Areye trips only. Still supported for existing integrations, and still resolved by the same unified code box — but they carry no stored value, no gift-card behaviour and no cross-platform settlement. |
Note the shapes: POST /business/coupons is the
legacy single-promo create, while POST /business/coupons/issue
mints a suite-wide batch. New integrations should use the /coupons/*
family throughout.
Errors
Areye uses standard HTTP status codes. Responses are JSON with a success flag and, on failure, a human-readable message.
| Status | Meaning |
|---|---|
| 200 | Success |
| 201 | Created — resource was successfully created |
| 401 | Unauthorized — missing or invalid API key |
| 403 | Forbidden — your account does not have this feature enabled |
| 404 | Not Found — resource does not exist |
| 422 | Unprocessable — validation failed, insufficient wallet balance, or credit limit exceeded |
| 429 | Too Many Requests — rate limit exceeded |
| 500 | Internal Server Error — contact support |
SDKs & Libraries
Official SDKs are under development. In the meantime, use the REST API directly with your preferred HTTP client.
Ready to build?
Request an API key and sandbox access. Our team will set you up within 24 hours.
Request API Access