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

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.

Test keys never dispatch drivers. They create real session and delivery records and exercise billing, including company balances. Use a dedicated test company. Build the whole integration before a single motorbike moves.

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.

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

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

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

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.

EventMeans
checkout.quotedPrice available before booking, with company/customer amounts in data.pricing.options and selected
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]{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'));
}
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.

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.

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
POST /checkout/sessions/:id/confirmBook using saved delivery details; safe to retry after completion
POST /checkout/sessions/:id/quoteReturn price and company/customer split before confirmation; emit a price webhook
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
GET /webhooks/eventsSupported events and signature format

Session creation fields

FieldBehavior
sender, receiverOptional 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_optionsDefault company; recipient means cash on delivery. Options must include the default payer. Company billing requires a linked company account.
company_contributionNonnegative 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_rolesender or receiver (default). Sender mode requires a sender mobile number at confirmation. Recipient phone is required in either mode.
vehicle_type_idOptional active vehicle ID. Supplying it fixes the vehicle; omit it to let the shopper select. Weight and distance limits still apply.
package_desc, weight_kgParcel description and positive weight; weight defaults to 1 kg.
driver_notePrefilled, editable delivery instructions. notes remains an alias.
reference, metadataYour order reference and JSON metadata, returned to your backend and in session webhook data. Not exposed by the public portal API.
success_url, cancel_urlOptional HTTP(S) return URLs without embedded credentials. Use your company's URLs.
expires_in_minutesDefault 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.

Call centre
9828