Conventions¶
URL shape¶
- Base:
https://app.saebooks.com.au/api/v1 - Resource collection:
/api/v1/<resources>(plural) - Resource item:
/api/v1/<resources>/{id}—idis always a UUIDv4 - Sub-resource:
/api/v1/<resources>/{id}/<children>
Verbs¶
| Verb | Purpose |
|---|---|
GET |
Read; pagination via ?page= / ?page_size= or ?limit= / ?offset= |
POST |
Create; body is JSON, returns the created resource |
PATCH |
Partial update; body is the diff |
PUT |
Full replace (rarely used) |
DELETE |
Soft-delete by default; ?hard=true for hard-delete (admin) |
Pagination¶
Offset-based. Most list endpoints accept ?page= (1-based, default 1) and
?page_size= (default 50, max 500). Some endpoints use ?limit= /
?offset= directly. All list responses share the same envelope:
GET /api/v1/invoices?page=1&page_size=50
→
{
"items": [...],
"total": 312,
"limit": 50,
"offset": 0
}
Errors¶
All non-2xx JSON responses conform to
RFC 7807 Problem Details
(Content-Type: application/problem+json):
{
"type": "https://saebooks.io/problems/invoice_not_found",
"title": "Invoice Not Found",
"status": 404,
"detail": "No invoice with that ID exists in this tenant.",
"code": "invoice_not_found"
}
code is a stable machine-readable identifier — key on it rather than
parsing detail prose. For Pydantic validation failures (422), the
response adds an errors array with per-field detail:
{
"type": "https://saebooks.io/problems/validation_failed",
"title": "Validation Failed",
"status": 422,
"code": "validation_failed",
"detail": "Request body or query parameters failed validation.",
"errors": [{"loc": ["body", "amount"], "msg": "must be >= 0", ...}]
}
HTTP status follows the usual conventions: 400 bad request, 401
missing/bad bearer, 403 role gate failed, 404 not found, 409
write conflict (version mismatch or idempotency key conflict), 422
schema validation failure, 428 precondition required (missing
If-Match), 5xx server fault.
Idempotency¶
Most write endpoints accept X-Idempotency-Key: <uuid>. On the first
request the response is stored; retries with the same key and identical
body return the stored response verbatim. Submitting the same key with
a different request body returns 409 with code idempotency_key_conflict.
Optimistic locking¶
PATCH and DELETE on versioned resources (invoices, bills, payments, journal
entries, etc.) require If-Match: <version>, where <version> is the integer
version field from the most-recent GET response. A mismatch returns 409
with code version_mismatch. Omitting the header on a route that requires it
returns 428 with code precondition_required.
Time¶
All timestamps are ISO-8601 with timezone (...+10:00 for AEST). The
server timezone is Australia/Brisbane.