Skip to content

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