Reference

The contract.

One page, deliberately: an integrator debugging at eleven at night wants one long, searchable page, not four short ones. Endpoints and fields live in the generated reference; this page is everything that holds across all of them.

On this page: Errors · Rate limits · Pagination · Idempotency · Concurrency · Money · Versioning · Change feed · Payroll · Coming from QuickBooks

The error envelope.

Every error, on every endpoint, is the same shape. The code is part of the contract — it exists so your client can tell "retry this" from "this will never succeed" without parsing prose.

The shape, stated once

{
  "error": {
    "code": "stale_write",
    "message": "This record changed since you loaded it.",
    "detail": { "your_version": 3, "current_version": 5 }
  }
}

The codes, with the column this table exists for:

Code Status Meaning What to do
validation_error 400 The body failed validation; detail lists the fields. Fix the request. An identical retry fails identically.
invalid_money 400 An amount was not a decimal string. Send "1234.56", never 1234.56.
unbalanced_entry 400 Debits and credits are not equal. Fix the entry. Never retry as-is.
insufficient_inventory 400 The sale needs more stock than is on hand. Receive stock or adjust quantities first; never retry as-is.
period_closed 409 The transaction date falls in a closed period. Do not retry. Post to an open period, or reopen the period in the app.
immutable_entry 409 Posted entries are never edited or deleted. Never retry. Post a reversal or a correcting entry instead.
idempotency_conflict 409 The same Idempotency-Key arrived with a different body. Never retry with that key. A different request needs a fresh key.
number_conflict 409 A hand-picked document number collided with a concurrent write. Nothing was persisted. Retry once; or omit the number and let the server allocate it.
stale_write 412 The record changed since you read it. Re-read, re-apply your change, resend with the new version.
not_part_of_public_api 403 The endpoint is internal; API keys are refused there. Never retry. The supported surface is the reference, in full.
throttled 429 Over the rate limit. Wait Retry-After seconds, then retry.

Authentication failures answer 401 with codes such as authentication_failed and not_authenticated; a key acting beyond its role answers 403 permission_denied. Both are the same envelope.

Rate limits.

Limits exist, they are per key, and a request over them answers 429 with a Retry-After header stating how many seconds to wait. Honour it — a client that backs off when told to will never notice the limits exist.

The specific numbers are deliberately not printed here: they are being tuned, and a stale figure on this page would be worse than none. Treat limits as real and subject to change. If you are building something that will sustain heavy traffic, talk to us first.

Idempotency.

POSTs that create financial records honour the Idempotency-Key header. For 24 hours, a request retried with the same key replays the stored response — same status, same body — rather than creating a second record. A timed-out create is therefore always safe to resend.

The key identifies the request, not the endpoint: reusing a key with a different body is refused with idempotency_conflict, because silently answering either body would mean lying about one of them.

Concurrency.

Every record carries an integer version that increments on each write. A well-behaved updater sends the version it read — in the write body, or as an If-Unmodified-Since header — and the server refuses the write with 412 stale_write if the record has moved on, telling you both versions in detail. Re-read, re-apply your change, resend. An updater that omits the version is declaring it wants last-writer-wins, and gets it.

Money.

Every amount is a decimal string plus a currency code, never a JSON number — a float cannot represent 0.1, and this is an accounting system. Responses carry amounts at the ledger's four-decimal storage scale ("4820.0000"); requests may send the natural two decimals ("4820.00"). A numeric amount in a request is refused with invalid_money.

If your JSON library parses amounts into floats, you have already lost — keep them as strings or decimals end to end.

Versioning and deprecation.

The version is in the URL: /api/v1/. Within v1, published paths, documented fields, types and status codes do not change incompatibly. Additive changes — new fields, new endpoints, new enum values — arrive without notice, so your client must ignore fields it does not recognise and tolerate enum values it has never seen.

Anything removed gets a minimum six-month deprecation window, announced in the changelog and signalled on the wire with Deprecation and Sunset headers on the affected endpoints. Anything absent from the generated reference is internal, carries no promise, and refuses API keys outright.

Polling for changes.

There are no webhooks. GET /changes is the supported sync pattern: one endpoint that answers "what changed since my last cursor" across the whole organisation, so you poll one URL instead of re-fetching every collection.

Call it once with no cursor to get a starting position — that first call returns no changes, only a cursor for "everything from here." Then pass the cursor you were last given. Keep following while has_more is true.

GET /api/v1/changes?cursor=… — response

{
  "changes": [
    {
      "entity_type": "ar_invoice",
      "entity_id": "6f1c…",
      "operation": "updated",
      "occurred_at": "2026-08-17T09:14:22Z",
      "entity_version": 4
    }
  ],
  "cursor": "eyJvIjoi…",
  "has_more": false
}

The feed says what changed, not what it changed to — fetch the entity when you care. entity_types narrows it to a comma-separated list, and limit sets the page size. A cursor belongs to one organisation and is rejected by any other. You only ever see types your key's role may read.

Payroll.

Summely does not run payroll and has no Employee entity — there is nothing to connect a payroll product's per-employee records to, and no connector to look for. What the books need from a pay run is smaller than that: one balanced summary entry per run, posted with POST /api/v1/journal-entries. The seeded chart of accounts already includes Payroll Liabilities (2300) and Payroll Expenses (6600).

Request — POST /api/v1/journal-entries

{
  "txn_date": "2026-08-31",
  "memo": "Payroll, August, per provider report",
  "lines": [
    { "account": "<id of 6600 Payroll Expenses>",
      "debit": "5210.00" },
    { "account": "<id of 1000 Business Checking>",
      "credit": "3890.00" },
    { "account": "<id of 2300 Payroll Liabilities>",
      "credit": "1320.00" }
  ]
}

Gross cost debits the expense; net pay credits the bank; withholdings credit the liability until they are remitted. The entry is created as a draft — POST /api/v1/journal-entries/{id}/post posts it to the ledger.

Coming from QuickBooks Online.

Summely is not wire-compatible with QuickBooks — an SDK built for QBO will not repoint here. The ideas mostly exist, under names that say what they are:

In QBO Here
Class, Department Tracking categories — /api/v1/tracking-categories and /api/v1/tracking-options, applied per line.
SyncToken The integer version field on every record; a stale write is refused with 412 stale_write rather than silently overwritten.
Sparse update PATCH — send only the fields you are changing.
TaxCode Tax groups and tax rates — /api/v1/tax-groups, /api/v1/tax-rates.
minorversion None. The version is the URL path, and within it changes are additive only.
Change Data Capture The change feed described above, when it ships.
Employee, payroll None — see Payroll.

Everything else is generated.

Every endpoint, field and enum, regenerated from the running service on every deploy — incapable of drifting from the code.