Errors & idempotency
What Billy returns when something goes wrong, and how to retry without paying a bill twice.
Error shape
Every error is JSON with a message you can log:
{
"message": "Wallet balance is insufficient for this payment.",
"code": "insufficient_balance"
}
Validation errors additionally carry per-field detail:
{
"message": "The 10 Digit Account Number field is required.",
"errors": {
"fields.field_1": ["The 10 Digit Account Number field is required."]
}
}
Status codes
| Status | Meaning | Retry? |
|---|---|---|
401 | Missing, invalid, or revoked API key. | No — fix the key. |
402 | Insufficient float. The request was valid; you're out of money. | Yes, after topping up. |
404 | Not found, or not available to your account. | No. |
422 | Validation failed — a bad amount, a missing or malformed field. | No — fix the request. |
429 | Rate limit exceeded. | Yes, after backing off. |
5xx | Something broke on Billy's side. | Yes, with the same idempotency key. |
402 and 422 differently. They look similar and mean opposite things. A 422 will never succeed until you change the request. A 402 is a perfectly good request that will go through the moment your float is funded — surface it to whoever tops up, don't log it as a bug.Rate limits
| Requests | Limit |
|---|---|
| Authenticated (per partner) | 120 per minute |
| Unauthenticated (per IP) | 30 per minute |
The limit is per partner, not per key — issuing more keys won't raise it. Exceeding it returns 429; back off and retry.
Idempotency
Networks fail in the worst possible way: your request arrives, Billy pays the bill, and the response is lost. You have no idea whether it worked, and retrying blind risks paying twice.
An idempotency key fixes this. Send one on every payment:
Idempotency-Key: 8f14e45f-ea28-4e1a-9a2b-1c2d3e4f5a6b
If Billy has already seen that key from your account, it returns the original payment instead of creating a new one. Retry as many times as you like — you get the same payment back every time.
Choosing keys
- Unique per payment attempt — a UUID, or your own order id. Never reuse a key for a different payment.
- Stable across retries — generate it before the first attempt and reuse the same value. A key generated per attempt provides no protection at all.
- Scoped to your account — no need to worry about collisions with other partners.
Timeouts
If a payment request times out, don't assume it failed. Retry with the same idempotency key: you'll get the original payment if it landed, or a fresh one if it didn't. Either way you end up with exactly one payment.