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; send Content-Type: application/json on requests with a body. The two upload endpoints take multipart/form-data.
  • A successful answer wraps what you asked for in data. A list also carries paging fields beside it.
  • An unsuccessful answer has error with a stable code, a human message and a requestId. See Errors.
  • A field with no value is null (a few, such as checkoutUrl, are simply absent when they do not apply). We may add fields at any time, so ignore the ones you do not recognise.
One object, and a page of objects
{ "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 seeIt means
4500000₦45,000
150000₦1,500
-450000a 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
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

Idempotency is about your retries. Two different customers racing for the same car are settled by the database: one booking is created, the other gets 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:

HeaderMeaning
RateLimit-LimitRequests allowed per minute (120).
RateLimit-RemainingRequests left in this window.
RateLimit-ResetSeconds until the window resets.
Retry-AfterOn 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:

Retry rate limits and server errors, with backoff
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.