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. |
iqbooks:write | Create and modify IQ Books data, post journals. |
The key that created a reconciliation or coding job can always poll that specific job even without the matching :read scope.
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.
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, and manual journals. 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.
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. 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.
9. 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.
10. 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.
Legacy / first-party API
An earlier first-party JWT API (account registration and login, WebSocket progress, the internal reconciliation flow) is documented separately at /api-documentation. That surface is for the IQ Suite web app itself. For third-party integrations, the API-key /api/v1 API documented here is the 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.