---
title: "The rules every request follows · Builder's guide · Chappell AI"
description: "The API is large (over 400 routes) but uniform. Learn these rules once and every domain behaves the same way."
url: https://chappellai.us/guide/07-the-rules-every-request-follows/
---

Builder's guide · Section 7

# The rules every request follows

The API is large (over 400 routes) but uniform. Learn these rules once and every domain behaves the same way.

## Headers

| Header | When | Notes |
| --- | --- | --- |
| `X-Source-System` | Every request | Your writer's code. It must match the key. |
| `Authorization: Bearer` | Every request | Your writer's key. Never log it. |
| `Idempotency-Key` | Every POST, PUT, PATCH, DELETE | 1–255 printable characters; a UUID v4 per user action. |
| `X-Request-Id` (reply) | Every reply | Record it with the person who asked (section 8). |
| `Idempotent-Replayed` (reply) | Replayed writes | `true` when the reply is the stored result of an earlier attempt. |

## Money is a string

Every amount is a JSON **string** with exactly two decimals: `"1234.50"`. Never `1234.5` (a number), never `"1,234.50"`, never negative; direction comes from the field (`debit_amount` / `credit_amount`). Quantities and unit prices are strings too, at their own precision. In your code, keep amounts as strings or decimals (for example with `decimal.js`), and format them only for display. Never convert an amount to a floating-point number, because a double cannot hold every cent.

## One error shape

```
HTTP/1.1 422 Unprocessable Entity
{ "error": {
    "code": "PERIOD_CLOSED",
    "message": "Posting date 2026-08-31 falls in fiscal period 2026-08, which is closed.",
    "details": [ { "code": "PERIOD_CLOSED", "field": "posting_date",
                   "message": "Period 2026-08 is closed." } ],
    "request_id": "req_01J9Z6Q4N8" } }
```

-   **Branch on `code`**, which never changes meaning. Show `message` to people, but never parse it.
-   A validation failure lists **every** problem in `details`, each with its field path (`lines[1].debit_amount`). Mark each field on the form.
-   A **4xx** is an answer about the books. Show it and do not retry. Only a **5xx** or a network failure is retried, with the same idempotency key.
-   Make your own server's errors use the same envelope, so the screens handle one shape.

## Idempotency: one key per user action

Create the key when the person clicks “Post”, not for each HTTP attempt. If the network drops and you retry, GAAPcore returns the first result instead of posting twice. Two traps:

-   **After a refusal, use a new key.** GAAPcore stores refusals too. If the user fixes the cause and you resend with the old key, you get the old refusal back. Keep a key only while the outcome is unknown.
-   **A changed request is a new action.** The same key with a different body is refused (`409 IDEMPOTENCY_KEY_REUSED`).

For scripts and imports, derive the key from the content (for example, the source file name and line number), so re-running the script is safe.

## Companies, lists and numbers

-   **`company_id` is always explicit:** in the body of every create, and as a query parameter on company reads. GAAPcore never infers it. Put the company in your URLs (`/c/<company>/…`) and in every cache key, so switching company never shows another company's data.
-   **Lists are paged:** `{ data, next_cursor }`, at most 200 rows a page. Follow `next_cursor` until it is `null`.
-   **Document numbers come from GAAPcore.** When a number series exists for a document type, don't send a number: GAAPcore assigns the next one and refuses yours. Show the expected next number as information only.
-   **Warnings.** A successful write can carry `warnings` (for example, deactivating an account that still has a balance). Show them after the write.

## Pin the version; generate the client

Keep a copy of the `openapi.yaml` you built against, with its commit and schema version (the reference console keeps them in `vendor/accountingkit/PIN.json`). Generate types and a typed client from it, for example with `openapi-typescript` and `openapi-fetch`, so a renamed field fails the compiler, not a customer demo. At run time, compare your pin with `GET /v1/version` and show a banner when they differ.
