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.

ReconcileIQ CodeIQ IQ Books

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).

ScopeGrants
reconcileiq:readPoll reconciliation jobs.
reconcileiq:writeSubmit reconciliation jobs.
codeiq:readPoll coding jobs.
codeiq:writeSubmit coding jobs.
iqbooks:readRead IQ Books organisations, accounts, contacts, bank data, invoices, settlements, journals.
iqbooks:writeCreate 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.

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:

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_..."
Data minimisation. CodeIQ codes against a transient working session that is deleted along with your submitted transactions once the result is extracted. Only the coded result is retained, for polling. 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:

A VAT Control account is required before any VAT-bearing posting. Confirming a bank transaction that carries VAT, issuing an invoice with VAT lines, or posting a journal with VAT all need an account with 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" }
HTTPcodeMeaning
400invalid_jsonThe 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.
400validation_errorGeneric 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.
401unauthorizedMissing, 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.
401invalid_tokenThe first-party web-session JWT on a /api/v1/keys endpoint is invalid or expired. Standard envelope with request_id.
403forbiddenKey lacks the required scope.
404not_foundUnknown, or not visible to you (no existence leak).
402insufficient_creditsNot enough credits (details.required / available).
409conflictConflicts with current state (e.g. duplicate code).
422invalid_dataset_formatReconcileIQ canonical contract violation.
422invalid_request_formatCodeIQ / IQ Books strict-format violation.
422unprocessable_entitySemantically rejected (e.g. unbalanced journal).
429rate_limitedPer-key rate limit exceeded.
503busyJob queue full, retry shortly.
500internal_errorUnexpected server error.
One envelope, no exceptions.
  • Malformed JSON is rejected with the standard envelope: HTTP 400 invalid_json, with request_id and the X-Request-Id header, 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's 400/422 code as above.
  • The first-party /api/v1/keys endpoints are protected by your web session (a JWT bearer token), not an API key. On a missing token they return 401 unauthorized; on an invalid or expired token, 401 invalid_token — both in the standard envelope with a request_id, identical in shape to API-key auth failures.
  • The maximum accepted request body is 25 MB; a larger body returns 413 payload_too_large in 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.