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.

http
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

FieldTypeMeaning
200 / 201doneIt worked. A repeat with the same idempotency key returns this too.
400malformedThe request is wrong in shape. Fix it; retrying will not help.
401unauthenticatedMissing, expired or invalid key.
403not permittedAuthenticated, but this key or user cannot do this. The message says which capability was missing.
404not foundOr not yours. Nineveh does not distinguish — telling you a record exists but belongs to another product is itself a leak.
409conflictA state transition applied out of order: approving an approved payout, paying an unapproved one.
422refusedWell-formed and understood, but not allowed: overdrawing a wallet, a payout with no bank reference, a break closed by the person who claimed it.
429rate limitedBack off. Retry-After says how long.
503pausedIngest is deliberately paused. Keep notifying: nothing is lost and it will be re-notified.

The error body

json
{
  "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.

Every one of those refusals happens in the pipeline, not in the interface. Hiding a button is not a rule.

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.