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.