Get started
Conventions
The few rules that hold across the whole API, so that once you have learned them you can guess the rest.
Requests and responses
- The base URL is
https://api.waca.com.ng/api/v1. Everything is JSON over HTTPS; sendContent-Type: application/jsonon requests with a body. The two upload endpoints takemultipart/form-data. - A successful answer wraps what you asked for in
data. A list also carries paging fields beside it. - An unsuccessful answer has
errorwith a stablecode, a humanmessageand arequestId. See Errors. - A field with no value is
null(a few, such ascheckoutUrl, are simply absent when they do not apply). We may add fields at any time, so ignore the ones you do not recognise.
{ "data": { "reference": "WC-7KQ2M9XA", "status": "confirmed" } }
{ "data": [ { … }, { … } ], "page": 1, "totalPages": 4, "total": 73 }Money
Every amount is a whole number of kobo (₦1 = 100 kobo), in the field’s natural name: dailyRate, total, amount. Never a decimal, never a string: floating-point money is how a payout ends up one kobo out. Divide by 100 only to display.
| You see | It means |
|---|---|
4500000 | ₦45,000 |
150000 | ₦1,500 |
-450000 | a discount of ₦4,500 (a negative line) |
100 | ₦1 |
Prices you send (a car’s dailyRate, a listing’s askingPrice) must be whole naira, that is a multiple of 100 kobo. The currency is always NGN.
Dates and time zones
Dates and times are ISO 8601. Both airports are in Nigeria (WAT, UTC+1, no daylight saving), so send local times with the offset: 2026-11-20T10:00:00+01:00. We return UTC (…Z); convert for display. A rental day is a 24-hour block counted from the pick-up time, with 60 minutes’ grace: 3 days 30 minutes late is still 3 days, 3 days 90 minutes late is 4. A rental lasts at least one day and at most 90.
Pagination
Lists take page (from 1) and limit (1–100, default 20) and answer with page, totalPages and total. Lists that you read as a stream, such as GET /events, use a cursor instead: pass the id of the last event you saw as starting_after, and stop when hasMore is false.
Idempotency
Networks fail. If you send POST /rentals/bookings and never see the answer, you must be able to ask again without booking twice. Send an Idempotency-Key header (a UUID you generate) on every request that creates something: repeat the request with the same key and you get the same booking back, not a second one. Use a new key for a genuinely new booking, and the same key for every retry of one.
curl -X POST "https://api.waca.com.ng/api/v1/rentals/bookings" \
-H "Authorization: Bearer $WACA_KEY" \
-H "Idempotency-Key: 6b2f7c3e-6f0a-4a3e-9d1c-1c2b6f1a9e55" \
-H "Content-Type: application/json" -d '{ … }'Two customers, one car
409 unavailable. Offer them another car.Metadata
Bookings and orders accept a metadata object of your own: up to 10 keys (1–40 letters, digits, _ . -), each value text of up to 500 characters. Put your order id there and it comes back on the booking, in every webhook and in GET /events. To remove a key on update, set it to null.
Rate limits
Each key may make 120 requests a minute. Every response says where you stand:
| Header | Meaning |
|---|---|
RateLimit-Limit | Requests allowed per minute (120). |
RateLimit-Remaining | Requests left in this window. |
RateLimit-Reset | Seconds until the window resets. |
Retry-After | On a 429 only: how long to wait. |
Over the limit you get 429 rate_limited. Wait, then retry. Do not retry other 4xx errors: they will fail the same way. This wrapper retries what can succeed later and no more:
async function waca(path, init = {}, tries = 4) {
for (let attempt = 0; ; attempt++) {
const res = await fetch('https://api.waca.com.ng/api/v1' + path, {
...init,
headers: { Authorization: 'Bearer ' + process.env.WACA_KEY, 'Content-Type': 'application/json', ...init.headers },
})
if (res.ok) return (await res.json()).data
const { error } = await res.json().catch(() => ({ error: { code: 'server_error', message: res.statusText } }))
// Retry only what can succeed later: rate limits and server errors. Never a 4xx the request caused.
const retryable = res.status === 429 || res.status >= 500
if (!retryable || attempt >= tries) throw Object.assign(new Error(error.message), { code: error.code, status: res.status, requestId: error.requestId })
const wait = Number(res.headers.get('retry-after')) || 2 ** attempt
await new Promise((r) => setTimeout(r, wait * 1000))
}
}Polling less
Prefer webhooks to polling a booking again and again, and cache the catalogue calls (locations, options, classes) for a few minutes.
Request ids
Every response, success or error, has an X-Request-Id header, and every error body repeats it as requestId. Log it. When you write to developers@waca.com.ng, quote it and we can find the exact request.
Versioning
The version is in the address (/api/v1). Within a version we only make additive changes: new endpoints, new optional parameters, new fields in answers, new event types. Anything that could break you (removing or renaming a field, changing a meaning) would be a new version, announced in the changelog long before the old one is retired. Write your code to ignore fields and event types it does not know.