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.
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"
}
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
payer | What 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
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.
| Event | Means |
|---|---|
checkout.completed | They finished, and a delivery exists |
delivery.accepted | A rider took it — the payload names them |
delivery.picked_up | The parcel is with the rider |
delivery.delivered | It arrived and the recipient gave the code |
delivery.cancelled | It will not happen |
delivery.no_driver | Nobody 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));
}
Reference
Base URL /api/v1. Authenticate with
Authorization: Bearer <key> on every call.
| Endpoint | What it does |
|---|---|
GET /me | Who this key belongs to. The first call to make. |
POST /checkout/sessions | Start a checkout, get a URL |
GET /checkout/sessions/:id | Read one back, with its delivery |
GET /checkout/sessions | Recent sessions |
POST /checkout/sessions/:id/cancel | They abandoned the basket |
GET /deliveries/:code | Track one, by our code or your reference |
GET /deliveries | Your deliveries, newest first |
POST /quotes | Price a route with no checkout — for a "delivery from X" line |
GET /vehicle-types | What we carry, and what it costs |
GET /webhooks/deliveries | What 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.