Call centre 9828 Integration questions? Ask your account manager or call. Your portal
Integration guide

Delivery, without building dispatch

Send your customer to a page we host. They set where it goes, pick a vehicle and see the price. We find a rider, run the delivery and tell you what happened.

You never handle a map, a route, a price list or an address.

1Your server creates a checkout session and gets a URL
2You redirect your customer to it
3They choose the address and vehicle, and confirm
4We book it, send them back to you, and post you events

Quick start

Keys come from your company portal. Create an integration and you get a wk_test_… key immediately. A wk_live_… key needs our approval first — press Ask to go live and we usually answer the same day. The mode is in the key itself, so nobody has to remember which environment they are pointing at.

Test keys book nothing. They price on real roads, create real sessions and complete honestly — and never dispatch a driver. Build the whole integration before a single motorbike moves.

Create a checkout

curl https://your-waga-host/api/v1/checkout/sessions \
  -H "Authorization: Bearer wk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "sender": {
      "name": "Bole Shop", "phone": "+251911234567",
      "address": "Shewa Supermarket, Bole",
      "lat": 9.0108, "lng": 38.7613,
      "locked": true
    },
    "receiver": {
      "name": "Abel", "phone": "+251922345678",
      "address": "Mexico Square, Addis Ababa",
      "lat": 9.0103, "lng": 38.7468
    },
    "customer_role": "receiver",
    "driver_note": "Call on arrival; blue gate",
    "package_desc": "Groceries",
    "weight_kg": 4,
    "reference": "ORDER-10432",
    "success_url": "https://shop.example/thanks",
    "cancel_url": "https://shop.example/basket"
  }'

You get back a session. Redirect the browser to url:

{
  "id": "cs_test_YJLUFqibJp8AUQ4N1HPt",
  "object": "checkout.session",
  "status": "open",
  "url": "https://your-waga-host/checkout/qX9...",
  "expires_at": "2026-09-01T15:04:05.000Z"
}
Lock what you know, leave the rest. Send sender and receiver with names, phones, addresses and map coordinates. Set customer_role to "sender" or "receiver" to identify the person opening the page. A shop can send sender with "locked": true and the page cannot move it. It rarely knows where the shopper actually wants the parcel — which is the part the page is for. Send nothing at all and the customer sets both ends. The former pickup, dropoff and notes names remain supported.

What your customer sees

Your name at the top, a map, a price for every vehicle, and one button. They do not need an account with us and are never asked to make one.

Who pays

payerWhat happens
"company"
default
The fare comes off your company wallet, at your negotiated rate. Your customer is told the delivery is paid for. Top up and watch the balance in your company portal. ("merchant" still works — it is what the first version of this API called it.)
"recipient" Cash to the driver on arrival. The page says so plainly, before they confirm.

When they finish

The checkout URL becomes a live tracking page. Reopening it shows only the map, current driver position, delivery status, route and confirmation code; it never creates a second order.

We book the delivery and send them to your success_url with two parameters added, so you can match the return visit to the checkout:

https://shop.example/thanks?session_id=cs_test_YJLU...&delivery=WG-CCPJCB
Do not treat that redirect as proof. A browser can be closed on the way back. The webhook is what is reliable — or read the session back with the API.

Webhooks

Give us a URL and we post you every change. Retried with backoff for about a day, so an endpoint that is down for an hour loses nothing.

EventMeans
checkout.completedThey finished, and a delivery exists
delivery.acceptedA rider took it — the payload names them
delivery.picked_upThe parcel is with the rider
delivery.deliveredIt arrived and the recipient gave the code
delivery.cancelledIt will not happen
delivery.no_driverNobody could be found

Checking the signature

Every request carries a timestamp and an HMAC-SHA256 over timestamp.body:

Waga-Signature: t=1788202921,v1=9155226fbb0aed...
const crypto = require('crypto');

function verify(rawBody, header, secret) {
  const m = /t=(\d+),v1=([a-f0-9]+)/.exec(header || '');
  if (!m) return false;

  // Reject anything old, or a captured request can be replayed later.
  if (Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;

  const expected = crypto.createHmac('sha256', secret)
    .update(`${m[1]}.${rawBody}`).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(m[2]), Buffer.from(expected));
}
Use the raw body. Parse the JSON and re-serialise it and the bytes change, so the signature will not match. Read the body as text, verify, then parse.

Reference

Base URL /api/v1. Authenticate with Authorization: Bearer <key> on every call.

EndpointWhat it does
GET /meWho this key belongs to. The first call to make.
POST /checkout/sessionsStart a checkout, get a URL
GET /checkout/sessions/:idRead one back, with its delivery
GET /checkout/sessionsRecent sessions
POST /checkout/sessions/:id/cancelThey abandoned the basket
GET /deliveries/:codeTrack one, by our code or your reference
GET /deliveriesYour deliveries, newest first
POST /quotesPrice a route with no checkout — for a "delivery from X" line
GET /vehicle-typesWhat we carry, and what it costs
GET /webhooks/deliveriesWhat we tried to send you, and what came back

Errors

One shape, always, so you write one handler:

{
  "error": {
    "type": "invalid_request_error",
    "message": "weight_kg must be a positive number",
    "param": "weight_kg"
  }
}

type is authentication_error (401), invalid_request_error (400 / 404 / 409) or api_error (5xx).

Rate limits

Counted per key, not per IP — a load balancer is not an abuser. 300 requests a minute. Over it you get a 429 and should back off.

Stuck on something?

Something unclear, or missing? Tell us — this page is meant to be enough on its own. Your webhook attempts and their replies are in the company portal, so "you never told us" always has an answer.

Call centre
9828