Requests, errors and limits

Base URL https://hasslefreebooks.com/api/v1. JSON in, JSON out, one envelope everywhere.

The envelope

{"ok": true,  "request_id": "f4d4ad9f…", "data": { … }, "meta": {"timestamp": "2026-10-30T12:00:00+00:00"}}
{"ok": false, "request_id": "f4d4ad9f…", "error": {"code": "VALIDATION_ERROR", "message": "…", "details": {}}, "meta": { … }}

Branch on ok and on error.code (stable, SCREAMING_SNAKE_CASE). Show error.message to people. error.details is always an object. Keep request_id (also the X-Request-Id header) for support. Ignore fields you do not know: new ones are added without notice (versioning).

Conventions

Content typeContent-Type: application/json on requests with a body; uploads are multipart/form-data.
MoneyTwo decimals. Send 1500, "1500.00" or "1,500.00"; "1,5", "1e3" and "12abc" are refused (422). Responses use strings on invoices, payments and journal entries, numbers on bills and reports — accept both.
DatesYYYY-MM-DD, real calendar dates only. Timestamps are UTC.
IdsPositive integers. Unknown, or belonging to another business: 404.
Free textBounded by its column (names 255, memos 500). Too long is a 400/422 naming the field.

Status codes

StatusMeaningDo
200, 201Success (creates answer 200 or 201 depending on the endpoint)Read ok and data
400, 422The request is wrongFix it; do not retry unchanged
401No, expired or revoked tokenRefresh, or reconnect if the refresh fails
403Not allowed: INSUFFICIENT_SCOPE, role, ENTITY_NOT_GRANTED, NOT_AVAILABLE_TO_APPSDo not retry
404No such resource in this business
409The record's state refuses it (already finalized or paid, period locked, duplicate number)Usually an earlier attempt already succeeded — read it
429Rate limitedWait Retry-After seconds
5xxOur fault, or an upstream outageRetry with backoff; quote request_id if it persists

Rate limits

Per client address and route: normally 60 requests a minute, with lower limits on AI, bulk, import-commit and login routes. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix time); a 429 adds Retry-After. Back off — do not retry in a loop, and do not spread calls over extra credentials to get around a limit (that is against the terms and the abuse monitor notices).

Pagination

Paged lists clamp the page size (usually 100) and report their position in data.pagination:

EndpointRequestResponse
GET /transactionspage+per_page, or limit+offsettotal, page, per_page, has_more, …
GET /invoices, GET /billspage, per_pagepage, per_page, total, total_pages
GET /dimensions, /approvals/queue, webhook deliverieslimit, offsetlimit, offset, total

Contacts, the chart of accounts and a few other short lists are returned whole with a count.

Retrying safely

  • POST /journal-entry takes an Idempotency-Key header (1–255 characters, 24 hours). A repeat of the same request returns the first response with X-Idempotent-Replay: true; the same key with a different request is 422 IDEMPOTENCY_KEY_REUSED. Always send one.
  • Invoices with your own invoice_number, and contacts by email, are naturally retry-safe (409 on the duplicate).
  • finalize, approve, pay and record-payment claim the record atomically; a retry after success is 409, never a second posting.
  • After a timeout on a create with no natural key, read before you retry.

Common jobs

Bill a customerPOST /contacts → POST /invoices → POST /invoices/{id}/finalize → POST /invoices/record-payment
Pay a vendorPOST /bills → POST /bills/{id}/approve → POST /bills/{id}/pay
Post to the ledgerPOST /journal-entry (balanced lines, with an Idempotency-Key)
Read the booksGET /reports/pl, /reports/bs, /reports/cashflow, /reports/ar-aging, /reports/ap-aging, GET /transactions, GET /coa

Every request and response body is in the API reference.