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 type | Content-Type: application/json on requests with a body; uploads are multipart/form-data. |
|---|---|
| Money | Two 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. |
| Dates | YYYY-MM-DD, real calendar dates only. Timestamps are UTC. |
| Ids | Positive integers. Unknown, or belonging to another business: 404. |
| Free text | Bounded by its column (names 255, memos 500). Too long is a 400/422 naming the field. |
Status codes
| Status | Meaning | Do |
|---|---|---|
| 200, 201 | Success (creates answer 200 or 201 depending on the endpoint) | Read ok and data |
| 400, 422 | The request is wrong | Fix it; do not retry unchanged |
| 401 | No, expired or revoked token | Refresh, or reconnect if the refresh fails |
| 403 | Not allowed: INSUFFICIENT_SCOPE, role, ENTITY_NOT_GRANTED, NOT_AVAILABLE_TO_APPS | Do not retry |
| 404 | No such resource in this business | |
| 409 | The record's state refuses it (already finalized or paid, period locked, duplicate number) | Usually an earlier attempt already succeeded — read it |
| 429 | Rate limited | Wait Retry-After seconds |
| 5xx | Our fault, or an upstream outage | Retry 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:
| Endpoint | Request | Response |
|---|---|---|
GET /transactions | page+per_page, or limit+offset | total, page, per_page, has_more, … |
GET /invoices, GET /bills | page, per_page | page, per_page, total, total_pages |
GET /dimensions, /approvals/queue, webhook deliveries | limit, offset | limit, offset, total |
Contacts, the chart of accounts and a few other short lists are returned whole with a count.
Retrying safely
POST /journal-entrytakes anIdempotency-Keyheader (1–255 characters, 24 hours). A repeat of the same request returns the first response withX-Idempotent-Replay: true; the same key with a different request is 422IDEMPOTENCY_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,payandrecord-paymentclaim 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 customer | POST /contacts → POST /invoices → POST /invoices/{id}/finalize → POST /invoices/record-payment |
|---|---|
| Pay a vendor | POST /bills → POST /bills/{id}/approve → POST /bills/{id}/pay |
| Post to the ledger | POST /journal-entry (balanced lines, with an Idempotency-Key) |
| Read the books | GET /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.