Errors and idempotency
The two things that decide whether an integration survives a bad night: what a failure tells you, and what happens when you try again.
Idempotency
Every write takes an Idempotency-Key. Send the same key twice and you get the original result back — not a duplicate, and not an error. Keys are scoped to your product and retained for 24 hours.
POST /v1/transfers
Idempotency-Key: vend_01J9F2K3M4
{ "from": "hld_01J9F2", "to": "grp_01J9F2", "amount": { "currency": "NGN", "value": "8000.00" } }Ingest has its own, stronger guarantee that needs no key from you: a credit is unique on the authority’s own reference. If the bank notifies twice, there is one credit and one event. This is not a convenience — it is the difference between a tenant being credited once and being credited twice.
Status codes
| Field | Type | Meaning |
|---|---|---|
| 200 / 201 | done | It worked. A repeat with the same idempotency key returns this too. |
| 400 | malformed | The request is wrong in shape. Fix it; retrying will not help. |
| 401 | unauthenticated | Missing, expired or invalid key. |
| 403 | not permitted | Authenticated, but this key or user cannot do this. The message says which capability was missing. |
| 404 | not found | Or not yours. Nineveh does not distinguish — telling you a record exists but belongs to another product is itself a leak. |
| 409 | conflict | A state transition applied out of order: approving an approved payout, paying an unapproved one. |
| 422 | refused | Well-formed and understood, but not allowed: overdrawing a wallet, a payout with no bank reference, a break closed by the person who claimed it. |
| 429 | rate limited | Back off. Retry-After says how long. |
| 503 | paused | Ingest is deliberately paused. Keep notifying: nothing is lost and it will be re-notified. |
The error body
{
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "only 5000.00 is free to pay out",
"detail": { "available": "26000.00", "held": "20000.00", "requested": "25000.00" }
}
}Messages are written to be shown to the person who caused the problem, and they say the actual numbers. A refusal that will not tell you what it was expecting is an outage with better manners.
Why so many 422s
Because a lot of what Nineveh does is refuse things, on purpose. A payout cannot be paid before it is approved. A break cannot be signed off by whoever claimed it. A cycle cannot settle twice. A group cannot request more than is free after money already reserved. These are not edge cases handled defensively — they are the rules, enforced where they cannot be worked around, and they answer with the reason.
When Nineveh is the problem
- Your endpoint is down: events queue and retry for ~47 hours. Nothing is lost; replay what dead-lettered.
- Nineveh is down: the bank keeps notifying and re-notifies; ingest is idempotent, so nothing double-counts on the way back up.
- The bank is silent: the console says so — last credit, last cycle, and an alert after six hours of nothing.