relay
Developers

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
POST/merchant-api/v1/orders
create-order.sh
request
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 }
    ]
  }'
200 OKresponse~42 ms
{
  "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.

For merchants reading this

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.

  1. 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.

  2. 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.

  3. 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.

Skip to developer docs →

The API

Six endpoints that cover the whole round trip.

POST/v1/orders

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.

POST/v2/quotedraft

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.

POST/v2/stock/lookupdraft

Check stock across FCs

Bulk-read availability for a seller’s mapped SKUs across every linked fulfillment center in one round trip.

GET/v1/orders

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.

GET/v1/products

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.

GET/v1/areas

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.

Retry without fear

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
idempotency.http
// 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
}
In plain English

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.

  1. 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.

  2. 02When the FC accepts

    Your dashboard updates

    ‘Accepted’ shows up on your side without you opening anything. No polling, no clicking refresh.

  3. 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.

  4. 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.

verify-relay.tsnode
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();
});
Webhooks, signed

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.

eventorder.status_changed

Fires 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.

codehttpwhat it means
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.
Keys, managed

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
API keysNew key
Production
rlk_live_4f8b••••••••
2m ago
Staging
rlk_test_9a2c••••••••
3h ago
Jobs worker
rlk_live_1d7e••••••••
yesterday
Migration — old
rlk_live_c4a9••••••••
revoked

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.