Your checkout. Relay's fulfillment. One clean handoff.
Your site makes a sale, Relay picks it up, ships it, and every status lands back in your system on its own. No one on your team refreshing, copying, or messaging anyone.
For developers: REST + JSON, x-api-key auth, idempotent writes, HMAC-SHA256 webhooks.
- Auth
- x-api-key
- Format
- JSON
- Webhooks
- HMAC-SHA256
curl -X POST https://api.relayapp.ng/merchant-api/v1/orders \
-H "x-api-key: $RELAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"fc_id": "f4c7c1c2-1b19-4c6b-9b9d-2d3e1c9a6c4f",
"customer_name": "Amaka Okafor",
"customer_phone": "08030000000",
"delivery_address": "12 Allen Ave, Ikeja",
"delivery_area_id": "2d3e1c9a-6c4f-4abc-9b9d-f4c7c1c21b19",
"external_ref": "shop-order-6c2a",
"items": [
{ "sku": "rice-5kg", "quantity": 2 }
]
}'{
"order_id": "33333333-3333-3333-3333-333333333333",
"order_number": "RLY-000231",
"source_order_number": "shop-order-6c2a",
"total_amount": 20500,
"status": "pending",
"idempotent_replay": false
}Your live base URL is issued with your first API key.
Hand this page to your developer. They'll do the rest.
You don't need to read the code on this page. You only need to do three things — all in plain language, none of them technical.
- 01
Make an API key in the merchant app
Open the Relay merchant app. Tap Account → API Keys → New key. Give it a label like “My website.” Copy the key — it’s shown once, then hidden forever. Treat it like a password.
- 02
Send your developer this page + the key
Paste the URL of this page and the key you just copied into a single message. That’s the entire brief — every endpoint, every example, every failure case is on this page.
- 03
They integrate. You watch orders flow.
A working integration usually takes a day or two. After that, orders from your site land in Relay the instant they’re placed, and each delivery status lands back in your system the second it changes.
No cost to integrate. The API, the keys, and the webhooks are all free for merchants.
The API
Six endpoints that cover the whole round trip.
Create an order
Your checkout hits one endpoint and Relay takes it from there — the FC sees it, a rider gets assigned, the customer gets tracking. Idempotent via external_ref.
Quote a delivery
Send an address and items. Relay returns eligible FCs, the fee for each, and whether stock exists — so you can rank by price or speed before you commit.
Check stock across FCs
Bulk-read availability for a seller’s mapped SKUs across every linked fulfillment center in one round trip.
Fetch order status
By Relay order id, or by your own external_ref. Returns every timestamp from received to delivered — use it for your own order page.
List products + stock
Products stocked at an FC, with live stock levels. Map your catalog to Relay SKUs once, then query whenever you render a cart.
List delivery areas
All active delivery zones for an FC, with fees. Surface them in your checkout so buyers pick an area Relay actually serves.
Draft endpoints are in active development for the multi-FC marketplace path. V1 order creation is the live path today.
Replay the same POST. Get the same order.
Networks flake. Jobs retry. Lambdas double-fire. Relay dedupes on the external_ref you pass in. The second request doesn't create a second order — it returns the original with idempotent_replay: true so your code can tell.
- Dedup window is per-merchant, not global
- Works across SKU changes — the ref wins
- Safe to use in background workers with retry queues
- Original order_id returned on every replay
// First call — order created.
POST /merchant-api/v1/orders
{
"external_ref": "shop-order-6c2a",
...
}
→ 201 Created
{
"order_id": "3333…",
"order_number": "RLY-000231",
"idempotent_replay": false
}
// Network flake. Your job retries the same request.
POST /merchant-api/v1/orders
{
"external_ref": "shop-order-6c2a",
...
}
→ 200 OK
{
"order_id": "3333…", // same order
"order_number": "RLY-000231",
"idempotent_replay": true // ← you were replayed, not double-charged
}How orders land in your system. Without anyone on your team refreshing anything.
Your developer wires this up once. After that, every order your site sells shows up inside Relay the same second, and every status it goes through shows up back in your dashboard the same second it happens.
- 01The moment a customer checks out
Your site pings Relay
Relay sees the order in the same second — same SKUs, same address, same total. Nothing retyped.
- 02When the FC accepts
Your dashboard updates
‘Accepted’ shows up on your side without you opening anything. No polling, no clicking refresh.
- 03When the rider picks up
‘Out for delivery’ lands
You (and, if you want, the customer) see the rider’s move in real time — not after someone remembers to message.
- 04When the package arrives
‘Delivered’ with proof
Your system gets the timestamp and proof of delivery. Close the order, email the customer, issue a receipt — all on autopilot.
The technical term for this is webhooks. The human experience is “it’s just there.” Your developer sets it up once, from the section below.
import crypto from "node:crypto";
// Relay now sends a timestamped v2 signature and keeps the
// legacy signature header during migration.
export function verifyRelay(
rawBody: string,
timestamp: string,
signatureV2: string,
secret: string,
) {
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (!Number.isFinite(Number(timestamp)) || ageSeconds > 300) {
return false;
}
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signatureV2, "hex"),
);
}
// In your handler:
app.post("/webhooks/relay", (req, res) => {
const sig = req.header("x-relay-signature-v2");
const timestamp = req.header("x-relay-timestamp");
const raw = req.rawBody; // raw buffer, not parsed JSON
if (!sig || !timestamp || !verifyRelay(raw, timestamp, sig, process.env.RELAY_WEBHOOK_SECRET!)) {
return res.status(401).end();
}
const { event, data } = JSON.parse(raw);
// event === "order.status_changed"
// data.status in: accepted | assigned | picked_up | in_transit | delivered | failed | cancelled
handleRelayEvent(event, data);
res.status(200).end();
});If it didn't come from Relay, you don't trust it.
Every webhook body is signed with HMAC-SHA256 using the secret you set when you created the subscription. Verify x-relay-signature-v2 together with x-relay-timestampbefore you trust the payload. The legacy x-relay-signature header remains for migration compatibility.
order.status_changedFires on every transition — accepted, assigned, picked up, in transit, delivered, failed, cancelled. Multiple endpoints per merchant are supported. Return 2xx or Relay will log the failure for you to inspect.
Failures, named
Every error is a code you can branch on.
INSUFFICIENT_STOCK409An item ran out between quote and commit.QUOTE_EXPIRED409The quote you referenced is past its TTL.QUOTE_MISMATCH409The fee you passed doesn’t match the area’s current fee.OUT_OF_AREA409The customer address isn’t in any zone this FC serves.INVALID_FC_LINK400The merchant isn’t linked to that FC.INVALID_ITEMS400An item is missing a quantity, SKU, or price.INVALID_KEY401API key is missing, malformed, or revoked.RATE_LIMITED429Too many requests from this key — back off and retry.A key for staging. A key for prod.
Generate as many keys as you have environments or services. Label each one. See the last time it was used. Revoke a leaked key in one tap — takes effect immediately.
- Multiple keys per merchant with custom labels
- Shown in plaintext once on creation, then hidden forever
- Last-used timestamp on every key
- Revoke immediately — no propagation delay
- All key management lives in the merchant app
Ship the integration this week.
Get the app, create a key from the merchant app's API Keys screen, and your first delivery is a POST away. We'll send full endpoint reference docs with your key.
Always free for merchants. Your FC covers platform billing. Available on iOS and Android.