← API documentation

Live: https://api.areye.app/api · Sandbox: https://sandbox.areye.app/api · Sandbox guide

In-store shopping fulfilment — integrator guide

A generic Areye Business API capability: any merchant platform can send one order that a single Areye shopper buys across several stores and delivers in one trip. Everything here is public; the same text (shorter) is in the developer docs at /developers#shopping-fulfilment.

Underneath, a shopping order is an ordinary shop & deliver merchant order: the shopper is fronted the shelf money (purchase float), and the float reconciles against the till receipts when the basket is complete. It is not part of the courier network.

1. Switching it on

PUT  /api/business/shopping-fulfilment               {"webhook_url": "https://partner.example/webhooks/areye-shopping"}
POST /api/business/shopping-fulfilment/secret/rotate → {"data": {"webhook_secret": "shf_…"}}   (shown once)
GET  /api/business/shopping-fulfilment               → enabled, webhook_url, webhook_secret_set, sending, ask_timeout_minutes, receipt_url_ttl_minutes
GET  /api/business/shopping-fulfilment/deliveries    → your last 100 outbound events and their state

webhook_url must be public HTTPS (no private/loopback/metadata addresses, no credentials in the URL); it is re-checked at every attempt and redirects are never followed. Each integrator has its own URL and secret; no account sees another's orders or events. Rotation takes effect immediately, including for retries of older events.

2. Authentication

X-Business-API-Key: <your key> (or a scoped partner key), as for the rest of the Business API.

3. Create a shopping order

POST /api/business/shopping-orders

Same pickup/drop-off fields as POST /api/business/book (pickup_address, pickup_lat, pickup_lng, dest_address, dest_lat, dest_lng; optional scheduled_at, notes, delivery_code, payment_method), plus:

Field Req. Meaning
recipient_first_names yes Recipient's first names
recipient_surname yes Recipient's surname
recipient_phone yes International format
purchase_amount no Shelf money fronted to the shopper. Defaults to Σ estimated_amount; may not be lower
external_reference no Idempotency key for the booking; defaults to shopping.external_order_id
shopping.external_order_id yes Your order id ([A-Za-z0-9._:-]{1,64})
shopping.external_location_id no Your id for the shopping centre/location
shopping.substitution_preference no refund (default) | match | ask
shopping.stores[] yes 1–20 stores
stores[].external_fulfilment_id yes Your id for this store's part of the order; unique in the order
stores[].external_store_id yes Your store id
stores[].store_name no Shown to the shopper
stores[].sequence no Shopping order (default: array order)
stores[].estimated_amount yes Expected spend at this store
stores[].items[] yes 1–200 lines
items[].external_item_id yes Your line id, unique across the order. Areye never invents ids
items[].name yes Product name
items[].qty yes Whole units
items[].unit_price no Expected shelf price per unit
items[].barcode no Lets the shopper scan to confirm
items[].image no Product photo URL

External ids are stored exactly as received and echoed with the JSON type you sent (a numeric id comes back as a number, a string as a string).

Response: as /business/book (201 with the trip reference; a repeat with the same external_reference replays). Live state: GET /api/business/shopping-orders/{reference}.

4. Substitution preferences

POST /api/business/shopping-orders/{reference}/items/{external_item_id}/decision   {"decision": "accept" | "reject"}

The window is 5 minutes by default (ask_timeout_minutes). No answer in time → unavailable, decided_by: "preference"; a late answer gets 409 "The decision window closed; the item was refunded."

decided_by ∈ shopper | customer | preference.

5. Events

Store status ∈ collected | unavailable. Line outcome ∈ found | substituted | unavailable, with qty_requested, qty_found, unit_price_paid, optional substitute {name, barcode, unit_price}, and decided_by.

Example (shopping.store_collected), exact bytes used in §6:

{"event":"shopping.store_collected","event_id":"areye-shop-41-store-87","external_order_id":123456,"trip_reference":"T8F3A21QZ","store":{"external_fulfilment_id":9001,"external_store_id":2,"status":"collected","estimated_amount":56.0,"collected_amount":54.5,"currency":"BWP","receipt_photo_url":"https://signed.example/receipt.jpg"},"items":[{"external_item_id":7001,"outcome":"substituted","qty_requested":2,"qty_found":2,"unit_price_paid":27.25,"substitute":{"name":"Store-brand 2L","barcode":"600xxxx","unit_price":27.25},"decided_by":"customer"}],"at":"2026-10-10T09:00:00+02:00"}

6. Signature (HMAC-V2)

Headers on every request:

X-Areye-Timestamp: <unix seconds at sending>
X-Areye-Delivery: <event_id>          (identical on every retry)
X-Areye-Event: <event>
X-Areye-Signature-V2: t=<timestamp>,sha256=<hex>

hex = HMAC-SHA256(secret, "<timestamp>.<raw body bytes>"), lowercase. Verify on the raw bytes before parsing, compare in constant time, and reject a timestamp more than ±5 minutes from your clock.

Worked example: secret TEST_ONLY_example_secret, timestamp 1791621174, body = the exact bytes in §5 → 4bb3c00dcf528d4f53795e87c5e1d958175d9d33b7496e3bbc8f2f9fc78d7e47

$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_AREYE_TIMESTAMP'] ?? '';
$ok  = ctype_digit($ts) && abs(time() - (int) $ts) <= 300
    && hash_equals("t={$ts},sha256=" . hash_hmac('sha256', $ts . '.' . $raw, getenv('AREYE_SHOPPING_SECRET')),
                   $_SERVER['HTTP_X_AREYE_SIGNATURE_V2'] ?? '');

7. Idempotency and retries

8. Receipt URLs

receipt_photo_url is a signed link valid for at least 10 minutes (receipt_url_ttl_minutes; 24 h by default). Copy the image when you receive the event; an expired or altered link answers 403.

9. Errors

Status When
403 SHOPPING_FULFILMENT_OFF Capability not enabled for your account
422 Invalid shopping block (message names the first problem); nothing is booked. Or a non-public webhook_url
404 No shopping order with that reference for your account
409 Decision for a line not awaiting one, or after the window closed
401 / 429 Bad key / rate limit, as for the Business API

10. Testing

Check your verifier against the worked example first. Real shopping orders are handled only by real shoppers, so end-to-end runs are arranged with Areye operations as a sandbox session — never fired at the live network. Watch GET /api/business/shopping-fulfilment/deliveries for state, attempts and last error.