Quick start
API base URL: https://wagadelivery.com/api/v1.
Test and live keys use this same address. Replace shop.example
in return and webhook URL examples with your company's own domain.
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://wagadelivery.com/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",
"payer": "company",
"payer_options": ["company", "recipient"],
"company_contribution": 30,
"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_YJLUFqibJp8AUQ4N1HPtABCD",
"object": "checkout.session",
"status": "open",
"url": "https://wagadelivery.com/checkout/qX9...",
"mode": "test",
"payer": "company",
"payer_options": ["company", "recipient"],
"tracking_url": null,
"expires_at": "2026-09-11T15:04:05.000Z"
}
This response is abbreviated and illustrative; always use the actual returned ID and URL.
sender and
receiver with names, phones, addresses and map coordinates.
Set customer_role to "sender" or
"receiver" to select whose phone identifies the customer of record. 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 is billed to your linked active company account, at its negotiated rate
and within its credit limit. The customer does not pay the driver. 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. |
Defaults and payer choices
To let the shopper choose, send "payer_options": ["company", "recipient"]
with your default payer. Omit it to fix the payer. customer
is an alias for recipient; only explicitly authorized choices can be confirmed.
Share the delivery fee
For a company subsidy, send "payer": "recipient", "company_contribution": 30.
The company pays up to 30 ETB and the customer pays the remainder in cash to the driver.
Contributions are capped at the discounted total: on a 20 ETB delivery the company pays 20 and
the customer pays 0. payer: "company" always charges the company the full fee.
Company administrators can set a default under Company portal → Integrations →
select an integration → Settings → Company contribution per delivery (ETB).
A positive default makes new sessions use recipient payment plus that contribution when payer
is omitted. An authenticated API request may override company_contribution, including
0. Changing the setting affects new sessions only. The shopper cannot alter the contribution.
Get the price before the customer agrees
Creating a session with both coordinates returns pricing.options immediately, without
booking or charging. Include vehicle_type_id to also receive pricing.selected.
Each option contains gross_fare, discount, payable,
company_amount and customer_amount, all in ETB. Show customer_amount
before asking the customer to accept. Without both locations pricing is null until you quote.
If pricing fails after session creation, the 201 response includes pricing_error;
reuse that session ID rather than creating another one.
curl https://wagadelivery.com/api/v1/checkout/sessions/SESSION_ID/quote \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
# After the customer accepts the returned customer_amount:
curl https://wagadelivery.com/api/v1/checkout/sessions/SESSION_ID/confirm \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"max_customer_amount":70}'
70 is illustrative: send the amount actually accepted by your customer. Confirmation recalculates the fee and returns 409 before charging if the customer share exceeds that ceiling. A quote is an estimate, not a reservation or price guarantee. Requoting edited addresses does not save those edits as booking details; submit the same pickup/dropoff and vehicle on confirmation.
After confirmation, changes to an API delivery's route, vehicle or weight require cancellation and a new checkout with customer approval. Support may still correct contact details and notes. A no-driver API delivery also requires a new checkout instead of retrying the refunded booking.
Every successful session quote, including the initial creation quote and portal repricing,
emits checkout.quoted when a webhook is configured. After booking,
checkout.completed includes the final amounts. delivery.delivered
reports those same amounts when the delivery finishes. The completed session's pricing has
final: true; quoting itself never debits the company.
Supply sender and receiver objects with name,
phone, address, lat and lng to prefill
the portal. Addresses remain editable unless locked: true or
editable: false is set. Locked addresses require coordinates.
If you already have all details, including vehicle_type_id, call
POST /api/v1/checkout/sessions/:id/confirm from your server with your API key.
An empty JSON body uses the saved details; payer, editable addresses and recipient
contact fields can be supplied to update them. Retrying a completed session returns the same delivery.
curl https://wagadelivery.com/api/v1/checkout/sessions/SESSION_ID/confirm \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"payer":"recipient"}'
curl https://wagadelivery.com/api/v1/checkout/sessions/SESSION_ID \
-H "Authorization: Bearer YOUR_API_KEY"
Replace SESSION_ID with the complete ID returned at creation.
Send {} on confirmation to keep the saved payer and details.
To reopen using a session ID, call GET /api/v1/checkout/sessions/:id
with the same merchant and mode key, then open the returned url.
Completed sessions also provide tracking_url. Keep these links private:
they grant access to that delivery. A session ID by itself is not a public access credential.
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.
The shopper can stay on tracking or return 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
Tracking refreshes every five seconds while visible. Active road routes refresh at most every thirty seconds, subject to routing-provider availability. When the connection fails, the page identifies the last known location and retries. Test keys never dispatch drivers.
Driver coordinates stop being exposed after delivery, cancellation or no-driver termination. Road geometry depends on the configured maps provider; an unavailable route may be approximated.
Webhooks
When delivery SMS is enabled by Waga, the sender and receiver also receive booking and driver/status messages at the supplied phones, without needing the app. Messages show the route, package, price and company/customer split. Driver details are added when assigned; only the receiver receives the handover code. Test-mode merchant deliveries do not send real SMS. SMS is separate from the signed webhooks sent to your company backend.
Configure a publicly reachable HTTPS webhook URL and its secret in your company integration. Waga posts the events below. Return a 2xx response within ten seconds after safely storing the event. Failed attempts are retried up to seven times in total, with delays of 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours and 24 hours.
| Event | Means |
|---|---|
checkout.quoted | Price available before booking, with company/customer amounts in data.pricing.options and selected |
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]{64})$/.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], 'hex'), Buffer.from(expected, 'hex'));
}
The JSON envelope contains id (evt_…), type,
created, livemode, merchant,
data.delivery and data.session. The session includes your
reference and metadata. Deduplicate by event id,
acknowledge duplicates, and read the session again if events arrive out of order.
Requests also carry Waga-Event. No webhook is queued without a configured URL.
data.pricing contains quote options before booking, and final payable,
company_amount and customer_amount for booking and delivery events.
On checkout.quoted, data.delivery is null. Quote events can arrive more
than once as the customer changes location, payer or vehicle; use quoted_at and read the current
session before updating an already-completed order.
Use the authenticated session response's tracking_url for the hosted portal.
Webhook delivery payloads have a separate /track/… link and do not include
every field of the merchant API delivery response.
Reference
Base URL https://wagadelivery.com/api/v1. Authenticate with
Authorization: Bearer <key> on every call; X-API-Key
is also accepted. Keep keys on your backend. Session and delivery lookups are scoped
to the owning merchant and the key's test/live mode.
| 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 |
POST /checkout/sessions/:id/confirm | Book using saved delivery details; safe to retry after completion |
POST /checkout/sessions/:id/quote | Return price and company/customer split before confirmation; emit a price webhook |
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 |
GET /webhooks/events | Supported events and signature format |
Session creation fields
| Field | Behavior |
|---|---|
sender, receiver | Optional objects: name (or contact), Ethiopian mobile phone, address, lat, lng, locked or editable. Supplied coordinates must be valid; locked locations require both. Phones are normalized to +251. |
payer, payer_options | Default company; recipient means cash on delivery. Options must include the default payer. Company billing requires a linked company account. |
company_contribution | Nonnegative fixed ETB amount, at most two decimals; defaults to the integration setting. Positive contribution defaults payer to recipient when omitted. Snapshotted for this session; editable only by creating a new session through the merchant API. |
customer_role | sender or receiver (default). Sender mode requires a sender mobile number at confirmation. Recipient phone is required in either mode. |
vehicle_type_id | Optional active vehicle ID. Supplying it fixes the vehicle; omit it to let the shopper select. Weight and distance limits still apply. |
package_desc, weight_kg | Parcel description and positive weight; weight defaults to 1 kg. |
driver_note | Prefilled, editable delivery instructions. notes remains an alias. |
reference, metadata | Your order reference and JSON metadata, returned to your backend and in session webhook data. Not exposed by the public portal API. |
success_url, cancel_url | Optional HTTP(S) return URLs without embedded credentials. Use your company's URLs. |
expires_in_minutes | Default 60; limited to 5–1440 minutes. This controls an open checkout, not a booked delivery's tracking. |
Legacy pickup/dropoff objects, flat sender_*/receiver_*
contact and address fields, recipient_name/recipient_phone, and
customer_is remain supported. Prefer sender/receiver objects for new integrations.
Confirmation and retries
Confirmation accepts payer, editable pickup/dropoff
objects (address, lat, lng), vehicle_type_id when not fixed,
recipient_name, recipient_phone, notes, and optional max_customer_amount.
Omitted fields use saved values. Locked locations and a fixed vehicle cannot be overridden.
Save the session ID before confirmation. A repeated completed confirmation returns the same
delivery; a concurrent confirmation may return 409. Read the session before retrying after
a timeout. Creating another session creates another checkout: reference does not
deduplicate requests, and there is no creation Idempotency-Key support.
Status, pricing and lists
Session statuses: open, completed, expired,
cancelled. Completed means booked, not delivered. Delivery statuses include
searching, accepted, arrived_pickup, picked_up,
arrived_dropoff, delivered, cancelled, no_driver.
Session cancellation does not cancel a booked delivery; no merchant delivery-cancel endpoint is exposed.
Standalone quotes accept pickup/dropoff coordinates and optional
weight_kg, payer and company_contribution. They return ETB prices by vehicle, distance and duration, with
too_heavy/too_far eligibility flags. Company-funded and split quotes apply the
company's negotiated discount to the total before splitting it. Delivery responses expose
gross fare, discount, total payable, company_amount
and customer_amount. Company invoices and balance debits include only the company share;
cancellation refunds that share. The driver collects customer_amount and the wallet reconciles the rest.
Session and delivery lists return {object: "list", data: [...]}, newest first.
Use integer limit (default 20, maximum 100). Deliveries also accept status.
No cursor pagination is exposed. Reference lookup returns the newest match. Webhook attempts
return the latest 50 for the merchant and are not filtered by the key's mode.
Session responses include mode, payer choices, contacts, addresses, vehicle-independent parcel
details, your reference/metadata, return URLs, fare/distance, timestamps and delivery (null until booked).
Driver location freshness is driver_location_at. ISO timestamps ending in Z are UTC;
display times in Africa/Addis_Ababa (UTC+03:00).
Errors
Merchant route validation and authentication errors use this shape:
{
"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 / 402 / 404 / 409) or
api_error (5xx).
402 indicates a billing or credit problem. Middleware errors can instead contain a string
error. Handle both forms and HTTP status codes; infrastructure failures may not be JSON.
Rate limits
Default 300 requests per minute, shared by keys belonging to the same merchant and configurable
by the server operator. Other platform limits can also apply. A 429 includes
Retry-After and typically {error: "…", retry_in_sec: N}.
Observe RateLimit-Limit, RateLimit-Remaining and
RateLimit-Reset (seconds). Back off rather than retrying immediately.
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.