Error Handling
A practical guide to handling errors from the SpeyBooks API.
All error responses follow a consistent envelope. Error codes are machine-readable (snake_case strings), messages are human-readable, and an optional field property identifies the offending input for validation errors.
Error response envelope
Every error response has the same structure:
{
"success": false,
"error": {
"code": "error_code",
"message": "Human-readable explanation",
"field": "fieldName"
}
}
The code field is stable and safe to match against in your code. The message field may change between releases and is intended for logging or display. The field property is only present on validation errors.
HTTP status codes
| Status | Meaning | When it happens |
|---|---|---|
| 400 | Bad Request | Malformed JSON, missing required fields, invalid field values |
| 401 | Unauthorised | Missing or invalid API key, expired JWT session |
| 403 | Forbidden | API key lacks the required scope for this operation |
| 404 | Not Found | Resource does not exist, or belongs to a different organisation |
| 409 | Conflict | Resource is in a state that prevents the operation |
| 413 | Payload Too Large | Request body exceeds the size limit (e.g. CSV import) |
| 422 | Unprocessable Entity | Request is valid JSON but fails business logic validation |
| 429 | Too Many Requests | Rate limit exceeded - check Retry-After header |
| 500 | Internal Server Error | Unexpected failure on our side - safe to retry |
Common validation errors
Validation errors (400) include the field property so you can programmatically highlight the problem input.
| Error code | Field | Cause |
|---|---|---|
validation_error | name | Required field is missing or empty |
validation_error | contactType | Value must be customer or supplier |
validation_error | email | Invalid email format |
validation_error | total | Amount must be a positive integer (minor units) |
invalid_type | paymentTerms | Expected integer, received string |
invalid_date | dueDate | Date must be in YYYY-MM-DD format |
All monetary amounts are integers in minor units. A validation error on total with value 5000.00 means you should send 500000 instead (GBP 5,000.00 = 500000 pence).
Conflict errors (409)
Conflict errors occur when a resource is in a state that prevents the requested operation. These are not retryable - the underlying condition must be resolved first.
| Error code | Cause |
|---|---|
already_reconciled | Transaction has been reconciled and cannot be modified or deleted |
already_paid | Invoice is fully paid and cannot be edited |
already_voided | Resource has been voided and cannot be modified |
duplicate_entry | A resource with the same unique key already exists |
Business logic errors (422)
The 422 status indicates a request that is syntactically valid but violates a business rule.
| Error code | Context | Cause |
|---|---|---|
balance_mismatch | Bank imports | Statement rows do not reconcile to the expected balance |
unresolved_conflicts | Bank imports | Import has unresolved duplicate or mapping conflicts |
incomplete_ledger | VAT returns | Transactions are missing categorisation for the VAT period |
insufficient_profit | Dividends | Available profit is insufficient for the declared dividend amount |
Rate limiting
When you exceed the request limit, the API returns 429 Too Many Requests with a Retry-After header. The header value is the number of seconds to wait before retrying.
Do not retry immediately. Read the Retry-After value and wait at least that long. Clients that ignore the header and retry aggressively may be temporarily blocked.
Idempotency keys and failures
Idempotency keys let you safely retry POST requests without creating duplicates. Include an Idempotency-Key header with a unique string (up to 255 characters). Within 24 hours, repeating the same key returns the original response.
How the key behaves depends on the outcome of the first request:
| First result | Key behaviour | Your action |
|---|---|---|
| 2xx success | Stored - replays the original response | Safe to retry with the same key |
| 4xx client error | Stored - replays the error response | Fix the request body, then use a new key |
| 5xx server error | Released - key is available for retry | Retry with the same key |
If a request fails with 5xx, the key is released so you can retry the exact same request. If it fails with 4xx, the error is stored against the key - retrying with the same key and a corrected body will still return the original error. Generate a new key after fixing a 4xx.
Retry strategy
Not all failures should be retried. Use this as a guide:
| Status | Retry? | Strategy |
|---|---|---|
| 400, 401, 403, 404 | No | Fix the request - these are deterministic |
| 409 | No | Resolve the conflict, then try the operation again |
| 413 | No | Reduce the payload size |
| 422 | No | Fix the business logic issue (e.g. categorise transactions before filing VAT) |
| 429 | Yes | Wait for Retry-After seconds, then retry |
| 500 | Yes | Exponential backoff: 1s, 2s, 4s, 8s, 16s with jitter |
For automated workflows, use exponential backoff with jitter on 5xx errors. Start with a 1-second delay and double it on each attempt, adding a random offset of 0-500ms to avoid thundering herd problems. Cap retries at 5 attempts.
For rate limit errors (429), always respect the Retry-After header rather than using your own backoff schedule.
Which operations are idempotent?
GET, PUT, and DELETE requests are inherently idempotent - repeating them produces the same result. POST and PATCH requests are not idempotent by default. Use the Idempotency-Key header for any POST request in an automated workflow.
Webhook delivery failures
When SpeyBooks cannot deliver a webhook event, it retries with increasing delays. The retry schedule uses exponential backoff: roughly 1 minute, 5 minutes, 30 minutes, 2 hours, and 24 hours. After 5 failed delivery attempts, the event is marked as failed. The endpoint stays active and continues to receive new events.
Monitor delivery health via the Webhook Endpoints API. The GET /webhook-endpoints/{id}/deliveries endpoint returns delivery logs including status codes, response times, and retry counts.
Best practices
Write your error handling to match on error.code, not error.message. Codes are stable and machine-readable; messages may be reworded in future releases.
For validation errors, use the field property to highlight the specific input. When building a form that submits to the API, map error.field directly to the form field name.
Log the full error response body on failures. The code and message together provide enough context for debugging without needing to reproduce the request.