Rate Limits and Errors¶
Applies to: All subscriptions
Purpose¶
State the limits, define the error envelope, and give an unambiguous retry policy. Getting retry wrong is the most expensive integration mistake available: it turns one failure into a sustained load that harms everyone on the system, including the retrying integration.
Audience¶
Integration developers.
Prerequisites¶
Reference¶
Limits¶
| Surface | Limit | Window |
|---|---|---|
Authentication (/api/auth) |
20 requests | 15 minutes |
Ingest and integration (/api/v1/*) |
200 requests | 1 minute |
| Batch ingest | 50 documents | Per request |
Standard rate-limit headers accompany responses. Read them and adapt, rather than discovering the limit by hitting it.
Verify: Whether these limits vary by commercial plan is not established here. Confirm before designing for a volume near the ceiling. Recorded as assumption F4 in the Assumptions Register.
At 200 requests per minute, batching matters: 50 documents per request gives an effective ceiling far above what single-document calls allow. An integration sending each document individually will hit the limit at a fraction of the throughput available to it.
The retry policy¶
This table is the whole page. Everything else supports it.
| Response | Retry? | Why |
|---|---|---|
200, 201 |
— | Succeeded |
400 |
Never | Malformed. It will be malformed next time |
401 |
Once, after refreshing the credential | The credential may have expired mid-flight |
403 |
Never | Authorisation will not change on retry |
404 |
Never | Nothing to find |
409 |
Never | Duplicates are permanent. The number is consumed at the government, forever |
422 |
Never | The data is wrong. Fix it |
429 |
Yes, after backoff | Transient by definition |
500, 502, 503, 504 |
Yes, with exponential backoff, bounded | May be transient |
| Timeout, connection error | Yes, but reconcile first | You do not know the outcome |
The single most damaging mistake is retrying a 422 or a 409. Neither can succeed. Both consume rate-limit headroom that genuine traffic needs, and a retry loop against them can exhaust the limit for the whole integration.
Backoff¶
For the retryable cases:
| Attempt | Delay |
|---|---|
| 1 | 1 second |
| 2 | 2 seconds |
| 3 | 4 seconds |
| 4 | 8 seconds |
| 5 | 16 seconds |
| Then | Stop, and raise it to a person |
Add jitter — a random fraction of the delay — so that a fleet of retrying clients does not synchronise into repeating waves.
Bound the attempts. An integration that retries indefinitely converts a brief outage into a sustained one.
Handling a timeout correctly¶
A timeout is not a failure. It is an unknown outcome, and treating it as a failure is how duplicates are created.
request times out
|
v
GET /api/v1/ingest/status/{docNo}
|
+-- found --> it was accepted. Record and continue
|
+-- 404 --> it was not accepted. Safe to resend
Build this reconciliation before go-live, not after the first network incident.
The error envelope¶
{
"status": "REJECTED",
"docNo": "INV-2026-0001",
"docType": "INV",
"errors": [ { "Level": "HEADER", "InvoiceNo": "INV-2026-0001",
"Field": "BillTo_Pos", "Message": "…", "Severity": "ERROR" } ],
"requestId": "…"
}
| Field | Use |
|---|---|
status |
Programmatic branch |
docNo, docType |
Correlate the failure to the document, even when it was refused at the door |
errors |
The specific faults. Field is the stable identifier |
message |
Human-readable, on non-validation failures |
requestId |
Store it. It is what makes a support conversation efficient |
Circuit breaking on your side¶
Beyond retry, implement a circuit breaker: after a threshold of consecutive failures, stop calling for a cooldown period. Complifly does this for outbound write-back delivery, and an ERP integration benefits from the same discipline in the inbound direction.
Without it, an integration facing a sustained failure spends its entire rate-limit allowance on calls that cannot succeed, and recovers slowly even after the underlying problem is fixed.
Validation¶
Test each in the sandbox. Reading the table is not the same as having handled it.
| Scenario | Expected behaviour |
|---|---|
422 returned |
Not retried. Fields surfaced to a person |
409 returned |
Not retried. Recognised as terminal |
403 returned |
Not retried. Reported as a scope problem |
401 returned |
Credential refreshed, retried once |
429 returned |
Backed off, then retried successfully |
500 returned |
Retried with growing delays, bounded |
| Timeout | Status queried before any resend |
| Sustained failure | Circuit opens; calls stop until cooldown |
| Rate limit approached | Headers read and the rate adapted |
| Batch used | 50 per request rather than 50 single calls |
| Request identifiers stored | Present in your logs against every call |
Force a 429 deliberately once, in the sandbox. It is the only way to know your backoff works, and it is cheap to do.
Troubleshooting¶
| Symptom | Cause | Action |
|---|---|---|
Persistent 429 at modest volume |
Retrying non-retryable failures, or authenticating per call | Fix the retry policy; cache tokens |
| The integration gets slower over time | Retry queue growing with unretryable failures | Stop retrying 4xx other than 429 |
| Duplicates after a network incident | Blind resend on timeout | Reconcile by status first |
| A brief outage becomes a long one | Unbounded retries saturating the recovery | Bound attempts and add a circuit breaker |
| Retry waves every few seconds | No jitter; clients synchronised | Add randomisation to the delay |
| Support cannot trace a failure | Request identifier not stored | Store it against every call |
| A business user cannot see why a document failed | The ERP swallows the error body | Surface Field and Message to someone who can act |
| Throughput far below the limit | Sending one document per call | Batch up to 50 |
429 only at month end |
Real load concentration | Spread submission, or batch more aggressively |
Related Articles¶
- API Conventions — status codes and envelopes
- Ingest API — where these responses come from
- Error Code Reference — every code, classified by who fixes it
- Write-back API — the outbound direction's own retry model