API docs

Instant integrator API guide

The integrator API is what an integrator's servers call to run their customers in Instant: the customers, their members and the members' tokens, the webhook Instant posts to, and the return URL and its origins. Credentials are issued by the Instant team, who also bring an integrator live.

The reference (booksbyinstant.com/docs) has every path, field and status. This guide says what a reference cannot: the conventions every route follows, how to retry, how to verify a webhook, and how each object lives.

Every id and secret below is a placeholder.

The API

  • Base. https://app.booksbyinstant.com, with every route under /api/integrator/v1.
  • Production only. Integrators integrate against production; there is no test environment. A customer you make while building is a real one. A customer cannot be deleted. A bank connected in it from a page on your own machine is a real bank too.
  • Versions. Versions are in the path: a breaking change gets v2 beside v1. A field may be added to an answer at any time; a client ignores fields it does not know.
  • The customer API. A member token works on the customer API, as that member. The customer API has its own reference and guide, for integrators with a credential: they are not on this page, and Fetching these docs says how to fetch them.

Authentication

Every request carries one secret as Authorization: Bearer <secret>, exactly: the scheme Bearer, one space, and the secret in its minted format. Anything else is 401 before anything else is read. Whether a well-formed secret is live is checked after the request's own validation: a malformed request can answer 400, 404, 413 or 415 before a dead secret's 401 (PUT /api/integrator/v1/webhook checks the credential before it looks at the URL).

Secret Format Where it works Who mints it
Integrator credential live_<100 hex> the integrator API only the people running Instant, shown once
Member token live_m_<100 hex> the customer API, as that member the integrator (POST /api/integrator/v1/customers/{id}/members/{memberId}/tokens), shown once
Webhook signing secret live_w_<64 hex> never sent to Instant; it verifies what Instant posts Instant, on every PUT /api/integrator/v1/webhook, shown once

A credential is never accepted where a member token is expected and the other way round; a session cookie or any other token is never accepted. Unknown, revoked, rotated and a disabled integrator's credentials are the same 401 "Unauthorized.", indistinguishable on purpose. A 401 carries WWW-Authenticate: Bearer realm="instant-integrator-api".

Keep every secret on the integrator's servers only, in a secret manager. Instant keeps the credential and member tokens only as a SHA-512 hash, and the webhook secret encrypted, because it signs with it. Each has a hint: the prefix, … and the last four hex characters (live_…, live_m_…, live_w_…). Instant never shows a secret again.

Requests

  • A body is Content-Type: application/json (anything else is 415 "Use a JSON request."), one JSON object (else 400 "Check your details and try again."), within the route's size limit (Limits; over it, declared or sent, is 413 "Request is too large.").
  • Bodies are strict: a field the route does not take is 400 "Invalid <what> request: unknown field.", and the field is not repeated in the answer. A refused field is named by its path with what it must be ("Invalid customer request: member.language must be one of Instant's languages: …").
  • A route that takes a query (the two lists) is strict about it: another parameter, one given twice, an empty value or a number with a leading zero is 400. A route that takes none ignores one.
  • Ids are UUIDs, matched in any case and answered in lower case. A path id that is not a UUID is the same 404 as an unknown one, never 400. Another integrator's object is 404, never 403.
  • References (reference, member.reference) are the integrator's own ids, 1 to 200 characters; a member reference is unique across all the integrator's customers, so a person in two customers is two members. Spaces at either end are trimmed; any other whitespace at an end (a tab, a newline, a no-break space) is refused, never trimmed.

Responses

Every answer a route gives, success or not, is X-Content-Type-Options: nosniff and Cache-Control: no-store, except the docs themselves: a 200 or 304 of the docs endpoints is private, no-cache, with an ETag. JSON is UTF-8; times are ISO 8601 in UTC with milliseconds (2026-11-06T10:00:00.000Z). A create answers 201; everything else that succeeds answers 200.

Errors

Every answer that is not a success is one envelope:

{ "error": "A customer with that reference already exists.", "code": "CONFLICT", "existingId": "00000000-0000-7000-8000-000000000000" }

error is Instant's sentence for people; code is stable and is what an integrator branches on; retryAfter (seconds) is there only when above 0, with a Retry-After header (as a 429 does when there is a wait). A 409 that names an object already there carries existingId too.

Status code Meaning
400 VALIDATION_ERROR the request is refused as sent; the sentence says why
401 UNAUTHENTICATED no live secret (Authentication)
403 FORBIDDEN the member may not do this (a role, an approval rule)
404 NOT_FOUND no such object for this caller
409 CONFLICT the state does not allow it now (a duplicate, a decided approval, a changed object)
413 LIMIT_EXCEEDED the body or a part is too large
415 UNSUPPORTED_MEDIA_TYPE the wrong content type, or a file of a kind Instant never reads
422 UNREADABLE_FILE a file of a kind Instant reads that it could not read
429 RATE_LIMITED a rate limit; wait retryAfter when given
any other (5xx) UNAVAILABLE always "Instant is temporarily unavailable. Please try again shortly."; retry later

A 4xx keeps the refusal's own sentence; a 5xx always answers the generic one. More codes may be added: treat one you do not know by its status. This API answers 400, 401, 404, 409, 413, 415 and 5xx; 403, 422 and 429 are the customer API's today.

Unknown paths and methods

What a request outside the routes gets:

Request Answer
A path under /api/integrator/v1/ that is not a route, or one under a version that does not exist (v2) 404 with the framework's HTML not-found page, not the envelope; Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate
A method the route does not take (POST /api/integrator/v1/me, DELETE /api/integrator/v1/customers, PUT /api/integrator/v1/customers/{id}) 405 with an empty body, no Allow header and no envelope
OPTIONS on a route 204 with Allow listing its methods and no CORS header: the API is server to server
HEAD on a GET route the GET answer without its body
A path with a trailing slash (/api/integrator/v1/) 308 to the path without it

So a client treats a 404 or 405 without a JSON body as a wrong path or method in its own code, never as a missing object.

Idempotency and retries

There is no Idempotency-Key header. What a repeat of each write does:

Route A repeat
POST /api/integrator/v1/customers 409 "A customer with that reference already exists." with existingId, also when two identical creates race: the retry after a lost answer finds what it created
POST /api/integrator/v1/customers/{id}/members 409 "A member with that reference already exists." without an id: list the customer's members and find the reference; if it is not there, another of your customers has it, because a member reference is unique across all your customers
PATCH /api/integrator/v1/customers/{id}, PATCH /api/integrator/v1/customers/{id}/members/{memberId} the same values again are a 200 that changes nothing
POST /api/integrator/v1/customers/{id}/members/{memberId}/tokens not idempotent: each call issues a new token. A token whose answer was lost is never shown again and counts toward the cap of 20 until it is revoked or expires: list the member's tokens and revoke what nobody holds
DELETE /api/integrator/v1/customers/{id}/members/{memberId}/tokens/{tokenId} 409 "That token is no longer active.": it is revoked, as asked
PUT /api/integrator/v1/webhook every call mints a new secret and the previous one stops at once. After a lost answer, PUT again and keep the new secret
DELETE /api/integrator/v1/webhook, PUT /api/integrator/v1/return-url, DELETE /api/integrator/v1/return-url, PUT /api/integrator/v1/return-origins idempotent: the same state again is a 200

Retry a 429 after retryAfter, and a 5xx with backoff. Never retry a 4xx unchanged.

Pagination

GET /api/integrator/v1/customers and GET /api/integrator/v1/webhook/deliveries answer a page at a time, newest first. A page's nextCursor is the next request's cursor, and it is null on the last page. Pages hold steady while customers are added: a customer created after the first page never shifts a later one. limit (1 to 200, 50 by default) sets the page's size; a deliveries page can be narrowed by status.

The other lists are whole: a customer's members and entities, oldest first, and a member's tokens, newest first.

Limits

Limit Value
Request body 4096 bytes by default; a member's 32 KiB, which 500 entity ids need
Fields references 1–200 characters; names 1–120; URLs at most 2000; up to 500 entity ids in a selection
Tokens at most 20 active, unexpired tokens per member (a resource, not a rate)
Rates, per integrator or per customer none of their own yet
Webhook a payload is at most 2048 bytes; a receiver has 10 seconds; 7 attempts; deliveries are kept 30 days

Webhooks

Instant posts to the integrator's webhook what it would otherwise have told a person: the kinds and the payload are in the reference. Instant adds kinds and fields over time: answer 2xx to a kind you do not know and drop it; ignore fields you do not know.

A notice is delivered only when its recipient is the integrator's member: a person who is not, such as an accountant a customer has let in, gets none. Deliveries continue while a customer is paused. A payload holds ids and the integrator's own references, never content: read what it is about through the customer API, as the member; for docs.updated, fetch the docs.

docs.updated concerns no customer. A production release changed a reference or a guide: this API's or the customer API's. It is sent to every active integrator with a webhook, at most once an hour; changes in the meantime go out together. changed names each part that changed, its new version (its ETag without the quotes) and the URL to fetch it with the integrator credential. Like every kind it is delivered at least once: deduplicate on Instant-Event-Id. A rollback that changes what Instant serves is announced like any other change, so a version can be announced again.

link.finished says that a bank connection link ended, once for each link. item is the bank_link: its id, as the link answered it, and its outcome, connected, cancelled, failed or expired. A link expires when nobody opened it within 15 minutes, or its consent did not come back within the hour. Instant tells of that end within about a quarter of an hour. Like every delivery, it can come late or after a later one. Read the link by its id through the customer API for its status, its connection and why it failed.

Headers and verification

Each delivery is a POST of the payload with:

Content-Type: application/json
User-Agent: Instant-Webhooks/1
Instant-Event-Id: <the payload's id>
Instant-Event-Kind: <the payload's kind>
Instant-Signature: t=<unix seconds when this attempt was sent; each retry is signed again>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" keyed with the whole secret>
import { createHmac, timingSafeEqual } from 'node:crypto';

// secret: the live_w_<64 hex> string from PUT /api/integrator/v1/webhook, kept in a secret manager.
// rawBody: the request body exactly as received, a Buffer, before any parsing.
export function verify(secret, header, rawBody, now = Date.now() / 1000) {
  const [, t, v1] = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? '') ?? [];
  if (!t || Math.abs(now - Number(t)) > 300) return false; // a 5-minute tolerance
  const expected = createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest();
  const given = Buffer.from(v1, 'hex');
  return given.length === expected.length && timingSafeEqual(expected, given);
}

// In the receiver: verify the raw bytes before parsing, then deduplicate on the event id.
// if (!verify(secret, request.headers['instant-signature'], rawBody)) return respond(401);
// if (await seen(request.headers['instant-event-id'])) return respond(200);
// await enqueue(JSON.parse(rawBody)); return respond(200);
  • Verify the raw body as received, before parsing it; check the timestamp.
  • Delivery is at least once: the same event can arrive more than once, always with the same Instant-Event-Id, so deduplicate on it.
  • Answer any 2xx within 10 seconds and do the work afterwards; the body is read up to 1 KiB and ignored. A redirect is a failure and is never followed.
  • Test a duplicate delivery, a slow answer and a receiver outage before going live.

Retries and retention

A delivery has 7 attempts, due at 0 s, 1 m, 5 m, 30 m, 2 h, 6 h and 24 h after its creation; a late attempt skips the offsets it missed. Delivery runs once a minute, so an attempt can follow its due time by a minute or more. After the seventh failure it is failed and never sent again. While the integrator is disabled its deliveries wait unsent. Deliveries are kept 30 days: a pending one older than that fails expired, and delivered and failed ones are then deleted. There is no replay; the deliveries list and the customer API are how a missed event is found.

Lifecycle

Object States What the integrator sees
Integrator active → disabled → active (the people running Instant) Disabled: the credential and every member token are 401; no agent work runs in its customers; deliveries wait; a member's connection links are refused. Enabled again, all of it works; tokens were not revoked
Credential rotated by the people running Instant The old credential is 401 at once; there is no overlap. Agree a cut-over time
Customer active ↔ paused (PATCH /api/integrator/v1/customers/{id}) Paused: Instant answers every chat message with a fixed sentence that the customer is paused, and runs no scheduled, background or event work; reads, decisions, uploads, connections and member changes work; customer.paused / customer.resumed are posted. Nothing is deleted. There is no delete
Member active → deactivated → active (PATCH /api/integrator/v1/customers/{id}/members/{memberId}) Deactivated: every token is revoked for good (revokedReason: "deactivated") and 401; reactivated: role and access come back, tokens do not. There is no delete. The owner and the last admin cannot be deactivated
Member token active → expired or revoked Both are 401. Revoked by the integrator (integrator), by deactivation (deactivated) or by the people running Instant (operator)
Webhook set ↔ removed (PUT /api/integrator/v1/webhook, DELETE /api/integrator/v1/webhook; the people running Instant may remove it) Removed: pending deliveries fail webhook_removed; the history stays 30 days
Return URL set ↔ cleared (the people running Instant may clear it) Cleared: a connection ends on Instant's own result page
Return origins set, [] clears (PUT /api/integrator/v1/return-origins) Up to ten origins your return URLs may be on, each exactly as a browser writes it: https, or http on a loopback address on your own machine. The return URL's own origin counts without being listed
Delivery pending → delivered or failed 7 attempts in 24 hours; kept 30 days
Connection link (a member's one-use link to connect a mailbox, a calendar or a bank) unused → used or expired 15 minutes, once

Fetching these docs

This guide and the reference are served with the integrator credential, so your servers can fetch them:

Without a query they answer the integrator API's docs. With ?api=customer they answer the customer API's guide and reference, fetched with the same integrator credential.

Each answer has an ETag: the version of that file, an opaque string that changes when the file changes, and only then. Keep it with your copy and send it back as If-None-Match; while your copy is current, the answer is 304 without a body. The answers are Cache-Control: private, no-cache: keep your copy, and ask again before you use it. A fetch is a use of the credential like any other request, a 304 included.

With a webhook, docs.updated says when to fetch: fetch once after you set the webhook, then when a notice names a part. Without one, check at most once an hour. The public docs page updates within the hour after a notice.