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.
Pagination.
Lists are cursor-paginated, 50 rows per page: the envelope is
next, previous, results, and
you follow next until it is null.
Cursors rather than page numbers because cursors stay stable while
other clients are writing — page 3 cannot shift under you between
requests.
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.