The IQ Suite Public API
One API key, three products. Reconcile bank data, run the real CodeIQ coding pipeline, and read or write an IQ Books ledger over a clean, versioned REST API.
The public API is served from https://api.bankreconciler.app with every endpoint under the /api/v1 prefix. The full machine-readable contract is published at /openapi.json (OpenAPI 3.1) and rendered in the API Reference below.
1. Get an API key
API keys are created from your logged-in IQ Suite web account, not through the API itself. Key management lives at /api/v1/keys and is authenticated by your first-party web session, so you mint and revoke keys from the web app while signed in.
When a key is created the full secret is shown once and is never recoverable afterwards. Store it securely. A key looks like:
iqk_Ab3kZ9mQ2xWp_s3cr3tValueBase64UrlEncodedNeverShownAgain
You choose the key's scopes at creation time and, optionally, whether it is allowed to incur overage charges.
2. Authenticate every request
Send the key on every call, either as a bearer token or in the X-API-Key header. Both are equivalent.
curl https://api.bankreconciler.app/api/v1/me \
-H "Authorization: Bearer iqk_Ab3kZ9mQ2xWp_s3cr3t..."
# or
curl https://api.bankreconciler.app/api/v1/me \
-H "X-API-Key: iqk_Ab3kZ9mQ2xWp_s3cr3t..."
GET /api/v1/me returns your user id, the key's scopes, and your current credit balance. GET /api/v1/health is public and needs no key.
Scopes
Each key carries a fixed set of scopes. Every operation requires a specific scope; a key without it gets 403 forbidden (the scope is checked before the request body is even validated).
| Scope | Grants |
|---|---|
reconcileiq:read | Poll reconciliation jobs. |
reconcileiq:write | Submit reconciliation jobs. |
codeiq:read | Poll coding jobs. |
codeiq:write | Submit coding jobs. |
iqbooks:read | Read IQ Books organisations, accounts, contacts, bank data, invoices, settlements, journals and reports. |
iqbooks:write | Create and modify IQ Books data, post journals. |
iqbooks:send | Email a document to a customer or supplier. Deliberately separate from iqbooks:write: a draft can be deleted, a sent invoice cannot be recalled, so a key can be allowed to change the books without being allowed to mail anyone. |
The key that created a reconciliation or coding job can always poll that specific job even without the matching :read scope.
Restricting a key to particular books
A key covers every IQ Books organisation you are a member of unless you say otherwise. Pass organisation_ids when creating one to pin it to named books, which is how a practice hands an agent a single client rather than the whole portfolio. A restricted key cannot see the others at all: they are absent from organisation listings, and any attempt to read or change them is refused. Naming an organisation you are not a member of is refused at creation with 422.
curl -X POST https://api.bankreconciler.app/api/v1/keys \
-H "Authorization: Bearer $WEB_SESSION_JWT" \
-H "Content-Type: application/json" \
-d '{"name":"Bookkeeping agent, Harbourside only","scopes":["iqbooks:read","iqbooks:write"],"organisation_ids":[7]}'
3. The credit model
ReconcileIQ and CodeIQ consume account credits. IQ Books does not: reading and writing your own ledger is never credit-metered.
- ReconcileIQ: a reconciliation costs credits equal to the number of bank rows submitted, and is charged only if the run completes and is balance-verified. An unverifiable run still completes with
status: completed— it is intentional, documented behaviour, not an error — butresult.credits_chargedis0and nothing is billed. The completed result always carries an explicit, machine-readable verification contract (see §5):result.summary.verified, top-level andresult.credits_charged, andresult.verificationwithis_verified,expected_net_discrepancy,actual_net_effectand a human-readablereason. Branch onverification.is_verified/credits_charged, not onstatusalone. - CodeIQ: a coding job costs credits equal to the number of transactions submitted, charged only on a successful completed run.
Submit a whole period, not one account at a time
The coding pipeline reads a batch as a whole, so what you leave out changes what it can work out. Send the period's outstanding invoices and bills in invoices as well: without them a payment that settles an invoice is coded as ordinary income or expense, and the result gives no sign that matching was never attempted. Send every account with activity in the period in a single job and tag each line with bank_account: a transfer between two of your own accounts is identified by pairing an outgoing line against an incoming one, so if the two sides arrive in separate jobs, or share one account name, the pair cannot be found and both sides are coded as ordinary income and expense. Splitting a month across several jobs will give you a worse result than submitting it once.
Credits draw from your monthly allocation first, then bonus credits. If a job is obviously unaffordable at submit time you get an immediate 402 insufficient_credits with details.required and details.available. By default there are no surprise charges: overage is only ever billed when the key, the request (allowOverage: true) and your subscription all permit it. If credits run short at charge time the job ends with status: failed and an insufficient_credits error, and nothing is charged.
4. The async and poll pattern
Reconciliation and coding are asynchronous. A successful submit returns 202 Accepted with a job id and a poll_url:
{ "reconciliation_id": "rec_8Hq2Lm9Xk4PwZ0aBcD1eF",
"status": "queued",
"poll_url": "/api/v1/reconciliations/rec_8Hq2Lm9Xk4PwZ0aBcD1eF" }
Poll the GET endpoint until status is completed or failed. Intermediate states are queued and processing. The result object appears only on completed; the error object appears only on failed. Jobs run one at a time per product on a bounded queue: if the queue is full, submit returns 503 busy and you should retry shortly. There are no webhooks in v1.
5. ReconcileIQ: strict canonical format
ReconcileIQ performs no column mapping and no auto-detection. You must submit datasets already in the canonical shape. Every row of both bank and book must be exactly { date, amount, description } with no extra keys:
date: string, exact ISO 8601YYYY-MM-DD, a real calendar date.amount: a finite JSON number, signed (negatives allowed).description: string, 1 to 500 characters.
Only bank, book and options are allowed at the top level. Any deviation is rejected with 422 invalid_dataset_format, listing the offending dataset, row index and field. Options: allowedDateDifferenceInDays (0–30, default 3), includeMatched (default false), allowOverage (default false).
curl -X POST https://api.bankreconciler.app/api/v1/reconciliations \
-H "Authorization: Bearer iqk_..." \
-H "Content-Type: application/json" \
-d '{
"bank": [
{ "date": "2026-01-03", "amount": -42.50, "description": "TESCO STORES 3294" },
{ "date": "2026-01-05", "amount": 1200.00, "description": "CLIENT PAYMENT INV-1001" }
],
"book": [
{ "date": "2026-01-03", "amount": -42.50, "description": "Tesco groceries" },
{ "date": "2026-01-06", "amount": 1200.00, "description": "Invoice 1001 receipt" }
],
"options": { "allowedDateDifferenceInDays": 3, "includeMatched": true }
}'
# then poll:
curl https://api.bankreconciler.app/api/v1/reconciliations/rec_8Hq2Lm9Xk4PwZ0aBcD1eF \
-H "Authorization: Bearer iqk_..."
The completed result contains a summary, missing_from_books (in the bank, not the books), remove_from_books (in the books, not the bank), a verification block, and matched pairs when includeMatched was set.
The verification contract
Every completed reconciliation is balance-checked: the net discrepancy implied by the two datasets (expected_net_discrepancy = total bank − total book) is compared against the net effect of the items the engine could not reconcile (actual_net_effect = sum of missing_from_books − sum of remove_from_books). They must agree within a small tolerance for the run to be verified. A run that completes but cannot be balance-verified is intended, documented behaviour (not a failure): the result is still returned, and the run is simply not charged. The contract is always present and machine-readable:
// completed + verified -> charged
{ "reconciliation_id": "rec_...", "status": "completed",
"credits_charged": 3,
"result": {
"summary": { "verified": true, "bank_rows": 3, "book_rows": 2,
"matched_count": 2, "missing_from_books_count": 1,
"remove_from_books_count": 0 },
"credits_charged": 3,
"verification": { "is_verified": true,
"expected_net_discrepancy": 20, "actual_net_effect": 20,
"reason": "Verified: the unreconciled items fully account for the net balance discrepancy between the two datasets." },
"missing_from_books": [ { "date": "2024-04-03", "amount": 20, "description": "PAY C MISSING" } ],
"remove_from_books": [] } }
// completed but NOT verifiable -> NOT charged (intended behaviour)
{ "reconciliation_id": "rec_...", "status": "completed",
"credits_charged": 0,
"result": {
"summary": { "verified": false, "bank_rows": 20, "book_rows": 20,
"matched_count": 20, "missing_from_books_count": 0,
"remove_from_books_count": 0 },
"credits_charged": 0,
"verification": { "is_verified": false,
"expected_net_discrepancy": 0.018, "actual_net_effect": 0,
"reason": "Not verified: an unexplained net discrepancy of 0.02 remains. The expected net discrepancy (0.018) does not match the net effect of the unreconciled items (0), so the result could not be balance-verified and this run was not charged." },
"missing_from_books": [], "remove_from_books": [] } }
Always branch on result.verification.is_verified (or the equivalent top-level credits_charged), never on status alone: an unverified run is status: completed, not failed. verification.reason is a stable human-readable explanation suitable for logs and support.
6. CodeIQ: coding a batch
CodeIQ runs the real coding pipeline over your transactions and your chart of accounts. Strict format too: any deviation is 422 invalid_request_format with section, row and field. Account type is one of INCOME, EXPENSE, ASSET, LIABILITY, EQUITY (case-insensitive; REVENUE is accepted as an alias of INCOME). Returned VAT codes are universal: NV, ST, RR, EX, ZR.
curl -X POST https://api.bankreconciler.app/api/v1/codeiq/coding-jobs \
-H "Authorization: Bearer iqk_..." \
-H "Content-Type: application/json" \
-d '{
"transactions": [
{ "date": "2026-01-03", "description": "TESCO STORES 3294", "amount": -42.50 },
{ "date": "2026-01-04", "description": "SHELL FUEL LONDON", "amount": -68.00, "merchant": "Shell" }
],
"chart_of_accounts": [
{ "code": "5000", "name": "Cost of Goods Sold", "type": "EXPENSE" },
{ "code": "7300", "name": "Motor Vehicle Expenses", "type": "EXPENSE" },
{ "code": "4000", "name": "Sales", "type": "INCOME" }
]
}'
# then poll:
curl https://api.bankreconciler.app/api/v1/codeiq/coding-jobs/cod_4Tn1Ks8Wm2Qx5aZbCd9eF \
-H "Authorization: Bearer iqk_..."
suggested_account_type may be null even when an account is suggested.
7. IQ Books: ledger access
IQ Books exposes a member organisation's ledger: organisations and tax codes, chart of accounts, customers and suppliers, bank accounts and transactions, sales and purchase invoices, customer receipts and supplier payments, manual journals, and reports. You must be an accepted member of the target organisation; if you are not (or it does not exist) you get a 404 with no existence leak.
Reports
The same statements the app draws, under /organisations/{id}/reports/: profit-loss and profit-loss-monthly, balance-sheet, trial-balance, cash-flow, vat-return, period-comparison, balance-comparison, aged-debtors, aged-creditors, sales-by-customer, purchases-by-supplier, and customer-statement/{contactId} / supplier-statement/{contactId}. All are GET, all need iqbooks:read, and none is metered.
Dates are query parameters in YYYY-MM-DD: period_start and period_end for period reports, as_of for point-in-time ones. Every figure comes from the ledger itself, so a report agrees with the app to the penny.
curl "https://api.bankreconciler.app/api/v1/iqbooks/organisations/7/reports/profit-loss?period_start=2026-01-01&period_end=2026-03-31" \
-H "Authorization: Bearer $IQ_API_KEY"
vat-return returns the figures only. Submitting a return to HMRC is deliberately not in this API: an MTD submission is a declaration by the taxpayer, and it is made in the app.
Money and dates
Money is exchanged as decimal major units (e.g. 1234.56, 2 decimal places) with an explicit currency at the boundary; internally it is stored as integer pence. Dates are ISO YYYY-MM-DD.
Response envelope
Every IQ Books response wraps its payload in a data key. A single resource (a GET .../accounts/{id}, and every successful POST/PATCH/void/confirm/journal write) returns the object under data:
{ "data": { "id": 70, "code": "7600", "name": "Depreciation", ... } }
Read response.data, never the top level, for the resource itself. List endpoints return an array under data (see Pagination). The tax-codes list additionally carries top-level vat_registered and vat_scheme alongside data.
Pagination
List endpoints accept ?limit (1–200, default 50) and ?offset (default 0). Out-of-range paging returns 422 invalid_request_format. Paginated responses use:
{ "data": [ ... ],
"pagination": { "limit": 50, "offset": 0, "count": 50, "total": 214 } }
total is included when the underlying service reports it. A few small collections (organisations, accounts, bank accounts) return { "data": [ ... ] } without a pagination block.
# list this month's confirmed bank transactions for org 7
curl "https://api.bankreconciler.app/api/v1/iqbooks/organisations/7/bank-transactions?status=confirmed&date_from=2026-01-01&limit=100" \
-H "Authorization: Bearer iqk_..."
# post a balanced manual journal
curl -X POST https://api.bankreconciler.app/api/v1/iqbooks/organisations/7/journals \
-H "Authorization: Bearer iqk_..." \
-H "Content-Type: application/json" \
-d '{
"entry_date": "2026-01-31",
"description": "Depreciation - January",
"lines": [
{ "account_id": 70, "debit": 250.00, "memo": "Depreciation expense" },
{ "account_id": 18, "credit": 250.00, "memo": "Accumulated depreciation" }
]
}'
Each journal line has exactly one positive value (debit OR credit), at least two lines, and total debits must equal total credits. An unbalanced journal is rejected with 422 unprocessable_entity. Creating an invoice issues it by default (posting the AR/AP and VAT-control journal); pass issue: false for a draft.
Special accounts: the role field
When you create an account you may set an optional role string. A role marks an account as a system-significant account that postings route to. The role that matters for integrations is vat_control:
role: "vat_control" to exist in the organisation first. A brand-new organisation has no such account. Until you create one, those operations are rejected with 422 unprocessable_entity and the message "VAT Control account is required before confirming VAT bank transactions" (or the invoice/journal equivalent). Create it once per organisation:
curl -X POST https://api.bankreconciler.app/api/v1/iqbooks/organisations/7/accounts \
-H "Authorization: Bearer iqk_..." -H "Content-Type: application/json" \
-d '{ "code": "2202", "name": "VAT Control", "account_type": "LIABILITY", "role": "vat_control" }'
Non-VAT postings (a transaction with vat_code: "NV" and no VAT amount, an invoice with no VAT, a journal with no VAT lines) do not require it.
8. MCP: driving the suite from an agent
Everything above is REST. There is also an MCP server at https://api.reconcileiq.com/mcp, which is the same capabilities shaped for an agent rather than a script: seventeen tools instead of sixty-one endpoints, because a tool definition costs context on every turn and an agent sequencing five REST calls has five chances to drop a figure.
Two ways in. OAuth 2.1 with PKCE and dynamic client registration, which is what Claude and other hosted clients use when you add a connector by URL: the person signs in on our own consent screen, chooses which organisations the connection may touch, and the client receives a token. Or an API key in the Authorization header, which suits Claude Code and scripted clients. Both resolve to the same thing internally, so an OAuth token also works against the REST endpoints documented here.
| Tool group | What it does |
|---|---|
| Reading | find, get_document, get_report across the fourteen reports. A paged result says so, rather than letting an agent total a slice and present it as a whole. |
| Changing | plan_bookkeeping compiles plain English into validated operations and writes nothing; commit_plan carries out exactly what was previewed, once, against a single-use token. |
| CodeIQ | A real coding session: initialise from the connected platform so the client's ledger history is learned, add each bank account, pull outstanding invoices, add a marketplace export, then code the lot in one pass. |
Scopes work as they do above, with one addition: iqbooks:send covers emailing a document to a customer or supplier and is separate from iqbooks:write, because a draft can be deleted and a sent invoice cannot be recalled.
9. Error handling
Every error uses one envelope, and every response carries an X-Request-Id header that also appears in the body as request_id. Quote it in support requests.
{ "error": { "code": "not_found", "message": "Reconciliation not found" },
"request_id": "e62473baa9ee" }
| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_json | The request body is not syntactically valid JSON. The body is { "error": { "code": "invalid_json", "message": "Request body is not valid JSON", "details": { "reason": "<parser message>" } }, "request_id": "…" } — the standard envelope, with X-Request-Id. Returned before authentication, so an unauthenticated malformed body still gets this clean shape. |
| 400 | validation_error | Generic structural rejection with details.issues. Note: a well-formed body that fails a product's payload schema is not a 400 — it returns the product's 422 code below (invalid_dataset_format for ReconcileIQ, invalid_request_format for CodeIQ / IQ Books). Handle those 422s, not 400, for bad request bodies. |
| 401 | unauthorized | Missing, invalid or revoked API key. Also returned by the first-party /api/v1/keys endpoints when the web-session token is missing. Standard envelope with request_id. |
| 401 | invalid_token | The first-party web-session JWT on a /api/v1/keys endpoint is invalid or expired. Standard envelope with request_id. |
| 403 | forbidden | Key lacks the required scope. |
| 404 | not_found | Unknown, or not visible to you (no existence leak). |
| 402 | insufficient_credits | Not enough credits (details.required / available). |
| 409 | conflict | Conflicts with current state (e.g. duplicate code). |
| 422 | invalid_dataset_format | ReconcileIQ canonical contract violation. |
| 422 | invalid_request_format | CodeIQ / IQ Books strict-format violation. |
| 422 | unprocessable_entity | Semantically rejected (e.g. unbalanced journal). |
| 429 | rate_limited | Per-key rate limit exceeded. |
| 503 | busy | Job queue full, retry shortly. |
| 500 | internal_error | Unexpected server error. |
- Malformed JSON is rejected with the standard envelope: HTTP
400invalid_json, withrequest_idand theX-Request-Idheader, e.g.{ "error": { "code": "invalid_json", "message": "Request body is not valid JSON", "details": { "reason": "Unexpected token …" } }, "request_id": "…" }. This check runs before authentication, so even an unauthenticated request with a broken body gets the clean shape. A well-formed body that fails schema validation still returns the product's400/422code as above. - The first-party
/api/v1/keysendpoints are protected by your web session (a JWT bearer token), not an API key. On a missing token they return401unauthorized; on an invalid or expired token,401invalid_token— both in the standard envelope with arequest_id, identical in shape to API-key auth failures. - The maximum accepted request body is 25 MB; a larger body returns
413payload_too_largein the same envelope.
10. Rate limits
Requests are limited per API key: 120 requests per 60-second window. Every response includes RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds). Exceeding the limit returns 429 rate_limited.
11. Versioning
This is API v1: all endpoints are under /api/v1 and GET /api/v1/health reports "version": "v1". Breaking changes will ship under a new version prefix; build against the published openapi.json.
First-party app API
The IQ Suite web app uses a separate internal first-party JWT API (account registration and login, WebSocket progress, the internal reconciliation flow). That surface is for the app itself and is not part of the public API. For third-party integrations, the API-key /api/v1 API documented here is the single, supported public developer API.
API Reference
The full OpenAPI 3.1 contract, rendered live from /openapi.json. Try-it requests run against the production server.