Relay for developers

Create the delivery with one POST.

Your store's orders go straight to a fulfilment centre — and every status streams back, signed, to the PIN at the door.

REST + JSON · x-api-key auth · idempotent writes · HMAC-signed webhooks

Live contract — exactly as deployed
POST /merchant-api/v1/orders
x-api-key: rlk_9f2c4e81…
{
"customer_name": "Amaka Okafor",
"delivery_address": "12 Allen Ave, Ikeja",
"external_ref": "shop-6c2a",
"items": [
{ "sku": "rose-glow", "quantity": 2 }
]
}
→ 201 Created · "order_number": "RLY-000231"
Status streams back · signed sha256
accepted → assigned
picked_up → in_transit
delivered · PIN 2·6·9·4
The contract

Create orders. Read stock and areas. Track every status.

Base URL https://api.relayapp.ng/merchant-api — JSON in, JSON out, your key in x-api-key on every request.

POST/v1/orders

Create an order. Priced from your Relay catalogue, idempotent on external_ref, straight onto the centre's board.

GET/v1/orders

Fetch one order — by Relay id or your own external_ref — with every timestamp from created to delivered.

GET/v1/fcs

The fulfilment centres your store is linked to.

GET/v1/products

Products stocked at a centre, with live stock counts.

GET/v1/product_groups

The same catalogue grouped by variant — options and stock per SKU.

GET/v1/areas

Every delivery area a centre serves, with the fee and ETA for each.

Draft/v2/quote · /v2/stock/lookup

Fee, ETA and stock across up to ten centres in one call — in development. v1 is the live order path today.

Idempotency

Retry the same POST. Get the same order.

Networks flake and jobs double-fire. Relay dedupes on the external_ref you pass: the retry returns the original order with idempotent_replay: true — a 200, not a second parcel. Safe from any retry queue.

Same ref, same order
POST /v1/orders "external_ref": "shop-6c2a"
→ 201 Created "order_number": "RLY-000231"
# the network flakes — your worker retries
POST /v1/orders "external_ref": "shop-6c2a"
→ 200 OK "idempotent_replay": true
Verify every delivery
// x-relay-timestamp unix seconds
// x-relay-signature-v2 hex hmac
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
timingSafeEqual(expected, signatureV2)
Webhooks

Every status change arrives as a signed webhook.

One event — order.status_changed — fires on every transition of every order your key created. Verify the signature before you trust the payload.

  • HMAC-SHA256 over timestamp.body, hex-encoded, in x-relay-signature-v2
  • Up to three attempts with exponential backoff — every attempt logged
  • HTTPS endpoints only; secrets stay in your hands

pending → accepted → assigned → picked_up → in_transit → deliveredfailed · cancelled · declined

Errors

Errors are machine codes, not prose.

Every error body is { error, code } — a sentence for your logs, a constant for your code.

INVALID_KEY401

Key missing, malformed or revoked.

RATE_LIMITED429

Past 60 requests a minute on this key — back off and retry.

INVALID_FC_LINK400

Your store isn't linked to that centre.

INVALID_ITEMS400

An item is missing its SKU or quantity.

INSUFFICIENT_STOCK409

An item sold out between quote and commit.

OUT_OF_AREA409

The address isn't in any area the centre serves.

QUOTE_EXPIRED409

The delivery quote passed its ten-minute window.

QUOTE_MISMATCH409

The fee you passed no longer matches the area's fee.

ORDER_NOT_FOUND404

No order with that id or external_ref.

Keys

Keys live in your merchant dashboard.

Settings → API keys, self-serve. No sales call, no invoice — the API is free for merchants, at 60 requests a minute per key.

Labelled

One key per service or environment — name each for what it does.

Shown once

The full rlk_ key appears exactly once at creation. After that, only the prefix.

Accounted for

Every key shows the last time it was used.

Revocable

Revoke takes effect on the very next request. No propagation delay.

Merchant, not a developer? Send yours this page and a key — it's the whole brief.

Your first delivery is one POST away.

Create a key, point your checkout at the API, and watch the statuses land — through to the PIN at the door.