How it works

One boundary, a handful of rules, and a database that enforces them. Everything here comes from the builder's guide, condensed.

GAAPcore is a ledger, not an app. Your software sends it business documents, and it turns them into journal entries, inventory layers and statements. Or it refuses them and explains why. The rules below are the whole contract. Learn them once and every one of the 400 routes behaves the same way.

The writer boundary

Every program that changes the books is a registered writer. Your console is one writer. A bank-feed importer, an online-store connector or a nightly script would each be another. Every request carries three things:

Header When What it is
X-Source-System every request the writer’s code; it must match the key
Authorization: Bearer ak_… every request the writer’s API key
Idempotency-Key every POST, PUT, PATCH, DELETE one key per user action

GAAPcore stamps the writer on every row the request creates. Your console adds the other half of the story: which person, signed in to the console, asked for it. GAAPcore authenticates applications, never people.

The key lives on your server, never in a browser. The server passes the browser’s requests on to /v1/… and adds the headers itself. That also makes it the one place every person’s write passes through, which is where your approvals belong.

Scoped keys

A writer has one role, and a scope of companies.

Role May Use it for
reader read the books of the companies in its scope dashboards, reporting, an outside accountant’s export
poster read and write the books of the companies in its scope your console, an importer, a store connector
administrator register, scope and retire writers; issue and revoke keys; no ledger access the operator’s tooling only

A key is shown once. GAAPcore keeps only its SHA-256 hash. Keys start with ak_, so secret scanners can find one that leaked. Revoking a key takes effect on the very next request, because credentials are checked against the database on every call. An administrator can’t change its own role or scope, and a role change needs a different administrator.

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 with the same key, GAAPcore returns the first result instead of posting twice. The reply says so with Idempotent-Replayed: true.

POST /v1/ledger/manual-journals
X-Source-System: console
Authorization: Bearer ak_…
Idempotency-Key: 6f1c2e0a-4b7d-4e59-9a51-2d8c0f3b7e11
Content-Type: application/json

{
  "company_id": "…",
  "entry_type": "adjusting",
  "posting_date": "2026-10-31",
  "description": "Accrue October electricity",
  "post": true,
  "lines": [
    { "gl_account_id": "<6100 Utilities>", "debit_amount": "412.80" },
    { "gl_account_id": "<2100 Accrued liabilities>", "credit_amount": "412.80" }
  ]
}

There are 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.
  • A changed request is a new action. The same key with a different body is refused with 409 IDEMPOTENCY_KEY_REUSED.

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

Money is a string

Every amount is a JSON string with exactly two decimals: "1234.50". Never 1234.5, never "1,234.50", and never negative. The direction comes from the field (debit_amount or credit_amount). Quantities and unit prices are strings too, at their own precision. Keep amounts as strings or decimals in your code, and format them only for display. A double can’t hold every cent.

One error shape

Every non-2xx reply has the same body:

{
  "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, such as lines[1].debit_amount. A 4xx is an answer about the books: show it and don’t retry. Only a 5xx or a network failure is retried, with the same idempotency key.

FIFO, computed for you

Stock is valued in cost layers, per product and warehouse. Receipts create layers. Shipments, scrap and production consume them in order. Your app never sets a cost, and the database checks that what a goods issue took is what the FIFO rule gives:

goods issue GI-0311 take costs 5.00 but the FIFO rule gives 4.50 (SG-056)

The layers always equal what’s in the warehouse. That’s checked at commit, not reported after the fact.

Closed periods stay closed

Every posting needs an open period for its date. The close is a two-step handshake:

  1. Preview (GET /v1/ledger/fiscal-periods/{id}/close-preview) returns the figures, the closing entry, and every blocker in GAAPcore’s words. For example: “Draft MJ-0088 is dated in the period; post, re-date or delete it first”.
  2. Close (POST …/close) sends back the figures the person saw. If anything moved since the preview, GAAPcore refuses instead of closing on different numbers.

After that, the period is final. Nothing posts into it, and it can’t be reopened:

invoice INV-1093 date 2026-09-28 falls in a closed fiscal period

Reverse, don’t edit

A posted document is never edited. A mistake is reversed: the reversal posts the mirror-image entry, points back to the original, and carries its reason. Both stay in the books, so the history is always there to read.

POST /v1/ledger/manual-journals/{id}/reverse
Idempotency-Key: 0c9d…

{ "posting_date": "2026-11-03", "description": "Wrong account: electricity is 6110" }

Documents that depend on each other reverse in order. You can’t reverse an invoice’s shipment while the invoice still stands, for example. The database also refuses an attempt to change a posted row directly.

Read more

  • The builder’s guide covers all of this in more depth, plus sign-in, approvals, going live and testing.
  • Coverage lists every transaction type and the known limits.
  • Request access to read the code and the specs.