Developer Guide
How a bank’s developers and IT staff integrate with Bookend: the REST API and its authentication, submitting closing packages from a loan origination system, tracking them, reading findings and evidence, the watch folder, boarding and wire files, and the MCP server. Everything here runs inside the bank’s network against the bank’s own install.
1. Integration options at a glance
Section titled “1. Integration options at a glance”| You want to… | Use | Section |
|---|---|---|
| Push closing packages from an LOS or document system | REST (POST /v1/loans + document upload) with an intake:write API key, or the watch folder |
4, 8 |
| Show status, findings or queue figures in another system | REST reads with a status:read API key |
5, 6 |
| Archive or verify evidence | REST evidence export with an evidence:read API key |
7 |
| Load boarding records into a core without write access | File export folder (JSON, XML, CSV) | 9 |
| Let an assistant or automation query Bookend | MCP server at /mcp |
10 |
Decisions stay with people. Approving or rejecting a loan, accepting, overriding or escalating findings, staging, approving or committing a boarding, staging, approving or exporting a wire, funding and sealing all require a signed-in user; API-key tokens are refused on those routes.
2. Conventions
Section titled “2. Conventions”- Base URL. Through the published
app-uiport, the API is athttps://<host>/api/v1/…(the proxy strips/api). Paths below are written from/v1. - Reference. The full OpenAPI 3.1 document is served by the install at
/api/v1/openapi.json, ships in each release bundle, and is rendered on this site as the API reference. Generate a client from it rather than hand-writing request types. - JSON. Request and response bodies are camelCase JSON. Money and rates are decimal strings (
"1250000.00","0.08750"); parse them into a decimal type, never a float. Timestamps are ISO-8601 UTC; dates areYYYY-MM-DD. - Loans are addressed by the bank’s reference (
externalRef, for example your LOS loan number): letters, digits,.,_and-. A route with{ref}always addresses the latest package version of that loan. - Paging. List endpoints take
page(1-based) andsize(up to 200) and return{ items, page, size, total }. - Errors are RFC 9457
application/problem+jsonwithtitle,detailand, for validation failures,errors: [{ field, message }]:
| Status | Meaning |
|---|---|
| 400 | Validation failed; see errors[] |
| 401 | Missing, expired or invalid token or credentials (code: "totp_required" when a password sign-in needs an authenticator code) |
| 403 | Authenticated, but the role or scope does not allow the route |
| 404 | Unknown loan, document, finding or key |
| 409 | Conflict with the current state (reference already exists, loan not in a state that allows the action, concurrent edit) |
| 422 | Refused by policy (license expired, package over the size cap, adapter refused, format not enabled) |
| 429 | Rate limit reached; retry after a short wait |
3. Authentication
Section titled “3. Authentication”API keys (systems)
Section titled “API keys (systems)”Integrations authenticate with API keys. An administrator issues one on Users & access → API keys, or with POST /v1/api-keys while signed in:
{ "name": "LOS intake", "scopes": ["intake:write", "status:read"] }| Scope | Allows |
|---|---|
status:read |
Every read of loans, documents, fields, pipeline, findings, reconciliation runs, review state, boarding preview and status, catalogs, rules, the dashboard and the license; required to reach /mcp |
intake:write |
Creating loans and package versions, uploading documents and the LAR, updating borrower, principal and assignee, reprocessing |
evidence:read |
Listing evidence events, exporting the evidence packet (JSON or PDF) and verifying the chain |
The response contains the plaintext key (it starts with bk_) exactly once; Bookend stores only its SHA-256 hash. Give each integration its own key with the fewest scopes it needs.
Exchange the key for a short-lived bearer token:
curl -s -X POST https://<host>/api/v1/auth/token \ -H 'content-type: application/json' -d '{"apiKey":"bk_…"}'# → { "accessToken": "…", "expiresAt": "2026-09-29T14:15:00Z", "scopes": ["intake:write","status:read"] }Send it as Authorization: Bearer <accessToken>. Tokens last auth.access_token_minutes (15 minutes by default). There is no refresh token for API keys: when a call returns 401, or shortly before expiresAt, exchange the key again. Revoking a key (DELETE /v1/api-keys/{id}) stops new exchanges immediately; tokens already issued expire on their own.
User tokens (tools that act as a person)
Section titled “User tokens (tools that act as a person)”An internal tool that acts on behalf of a signed-in person uses the same endpoints as the Bookend web app:
POST /v1/auth/loginwith{ email, password, totpCode? }returns{ accessToken, accessExpiresAt, refreshToken, refreshExpiresAt, user }. When the user has confirmed an authenticator app and the bank enforces it, the first attempt withouttotpCodereturns 401 withcode: "totp_required".POST /v1/auth/otp/requestwith{ email }emails a single-use sign-in link;POST /v1/auth/otp/redeemwith{ token }returns the same token pair.POST /v1/auth/refreshwith{ refreshToken }returns a new pair; refresh tokens rotate, so always store the newest one.POST /v1/auth/revokesigns out.- Banks that use single sign-on configure OpenID Connect; the web app handles that flow.
Keep tokens in memory. The web app never writes them to cookies or browser storage, and neither should a tool built on it.
Validating Bookend tokens
Section titled “Validating Bookend tokens”Access tokens are RS256 JWTs. The public key is published at /v1/.well-known/jwks.json if another internal service needs to validate a Bookend token.
Rate limits
Section titled “Rate limits”Each principal (user or API key) has a token bucket of 300 requests per minute by default. The credential endpoints (/v1/auth/login, /otp/*, /token, /oidc/*) share a much tighter bucket of 10 per minute per client address. Cache your API-key token for its lifetime instead of exchanging on every call. Both limits are set by the bank’s administrators in the api’s configuration.
4. Submitting a package
Section titled “4. Submitting a package”A package is one loan’s closing documents (PDFs) plus, optionally, its loan approval record (LAR).
1. Register the loan:
curl -s -X POST https://<host>/api/v1/loans -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \ -d '{ "externalRef": "LN-2026-0412", "borrowerName": "Riverbend Holdings, LLC", "expectedPrincipal": "1250000.00" }'The loan is created as package version 1 in state intake (201). A reference that already exists is refused with 409 unless you send "newVersion": true, which adds the next package version. Packages are never overwritten.
2. Upload the documents as multipart/form-data with one or more files (any field name):
curl -s -X POST https://<host>/api/v1/loans/LN-2026-0412/documents -H "Authorization: Bearer $TOKEN" \ -F files=@01_Promissory_Note.pdf -F files=@02_Business_Loan_Agreement.pdf -F files=@lar.json- Package documents must be PDFs. A file named
lar.json,lar.csv,lar.xml,lar.pdforlar.docxis ingested as the LAR; send the form fieldasLar=trueto treat every file in the request as the LAR. - Each file is stored by content hash, so re-uploading identical bytes is idempotent.
- New documents start the processing pipeline and move the loan to
processing. - Documents can be added while the loan is in
intake,processingorreview(409 otherwise). A corrected package after approval or rejection is a new version. - Limits:
documents.max_package_mbper package (200 MB by default, 422 when exceeded) and 256 MB per request through the proxy. Split very large packages across several requests.
3. Or send the LAR separately: POST /v1/loans/{ref}/lar, either as multipart/form-data (any supported LAR format) or as a raw JSON body in the canonical LAR format. It is parsed with the bank’s active LAR profile (see LAR profiles). Replacing the LAR while the loan is in review re-runs reconciliation.
Other intake calls: PATCH /v1/loans/{ref} changes the borrower name, expected principal or assignee; POST /v1/loans/{ref}/reprocess (optionally { "documentId": "…" }) runs documents through the pipeline again and answers 202 with the pipeline URL in Location.
5. Tracking a package
Section titled “5. Tracking a package”Bookend does not push events to other systems; integrations poll. A loan moves through these states:
intake → processing → review → approved → boarding_staged → boarded → funded → sealed, or review → rejected (terminal).
GET /v1/loans/{ref}returns the loan header (state,hasExceptions,packageVersion, …), its documents and the list of package versions.GET /v1/loans/{ref}/pipelinereturns, per document, the stage grid of its latest run (split,classify,ocr,extract,locate, eachpending,running,retrying,completed,skippedorfailed, with attempts, timings, detail andnextRetryAt), plusisCompleteandhasFailuresfor the package. Stages retry with backoff while the inference service is unavailable, soretryingis not an error.GET /v1/loans?state=review&exceptions=truelists loans by state, assignee, exception flag or borrower, sorted byupdated(default),created,age,borrower,principalorprincipal_asc.
A polling interval of 15 to 60 seconds per active loan is plenty; scanned packages take longer than born-digital ones.
6. Reading results
Section titled “6. Reading results”| Call | Returns |
|---|---|
GET /v1/loans/{ref}/documents |
Each document’s classified type, original name, SHA-256, size, page count, whether it is scanned, and whether it is the LAR |
GET /v1/loans/{ref}/fields |
Every extracted value with provenance: document, page, page-relative bounding box, raw text, normalized value, kind, confidence and method |
GET /v1/loans/{ref}/findings |
The latest reconciliation run’s findings (or ?run=), filterable by severity (exception, warning, info) and status (open, accepted, overridden, resolved); each cites its source documents, pages and boxes |
GET /v1/loans/{ref}/reconciliation |
Every reconciliation run with its ruleset version, the thresholds in force and summary counts |
GET /v1/loans/{ref}/review |
The latest findings with their action history (who accepted, overrode or escalated each one, with reason code and justification), whether the loan can be approved and the blockers if not |
GET /v1/rules, GET /v1/rules/{id}/doc |
The rule catalog with bank policy state, and each rule’s documentation page |
GET /v1/loans/{ref}/boarding/preview |
What boarding would send to the core, per field, with provenance and validation; nothing is staged |
GET /v1/loans/{ref}/boarding/status |
The boarding record, a live inquiry against the core once a core reference exists, and the loan state |
GET /v1/loans/{ref}/wire |
The staged or approved wire request |
GET /v1/dashboard?days=30 |
Queue counts, throughput, average review time, exception rate by rule and aging |
GET /v1/catalogs/fields, GET /v1/catalogs/document-types |
The canonical field names and document types used in every response |
All of these need status:read (or a staff role).
7. Evidence
Section titled “7. Evidence”Every evidential action (intake, extraction, findings, decisions, boarding, wires, funding, sealing) is appended to the loan’s hash chain.
GET /v1/loans/{ref}/evidence/exportreturns the machine-readable packet: loan, documents with SHA-256 hashes, the latest findings, approvals, every chain event with payload and hashes, and a fresh verification result.?format=pdfreturns the human-readable packet as a download.GET /v1/loans/{ref}/evidence/verifyrecomputes the chain and reportsintact, its length and, if broken, the first failing sequence number.GET /v1/loans/{ref}/evidencelists the events in sequence.
All three need evidence:read. The JSON packet is self-contained, so an archive can re-verify it offline:
payloadSha256is the lowercase hex SHA-256 of the event payload in canonical form: UTF-8 JSON with no whitespace, object keys sorted by ordinal comparison at every level, array order kept, and numbers written as their plain decimal value (1.50becomes1.5).chainSha256is the lowercase hex SHA-256 of the stringprevSha256 + payloadSha256(the two hex strings concatenated).- The first event (
seq1) has aprevSha256of 64 zeros; each later event’sprevSha256is the previous event’schainSha256.
8. The watch folder
Section titled “8. The watch folder”For systems that can drop files but not call an API, the api scans documents.watch_folder (/data/watch, the watch volume) every documents.watch_interval_seconds (60 by default):
/data/watch/ LN-2026-0412/ ← folder name = loan reference 01_Promissory_Note.pdf 02_Business_Loan_Agreement.pdf lar.json ← optional; lar.csv, .xml, .pdf, .docx also work- A folder is ingested once every file in it has been unchanged for
documents.watch_stable_seconds(5 by default), so write the files and then leave them alone. Writing into a temporary name and renaming the folder into place is the safest pattern. - The borrower name is read from
lar.json(borrower.legalName) when present; otherwise the loan is created as “Unknown borrower (watch folder)” and can be corrected in the app or withPATCH /v1/loans/{ref}. - A handled folder moves to
processed/, or torejected/with abookend-rejected.txtnote when the reference is invalid, already exists (the watch folder never adds a version or overwrites) or intake fails. - The folder must be writable by the api container’s
appuser so handled folders can be moved.
9. Boarding and wire files
Section titled “9. Boarding and wire files”Banks that board through files set core.provider = file.export. After a boarding checker commits a loan, Bookend writes boarding_{ref}_v{n}.json, .xml and .csv to core.file_export_path (/data/exports), with deterministic names so a retried commit overwrites rather than duplicates. Exported wire requests land under /data/exports/wires. Formats and field layout are on File export; the column codes come from the bank’s core field map. For the Jack Henry path, see Jack Henry jXchange.
A core import job should read the three files for a loan together, treat the JSON as the canonical record (it carries each value’s source document and page), and ignore files it has already imported for the same loanRef and packageVersion.
10. MCP
Section titled “10. MCP”The same operations are available to MCP clients at https://<host>/mcp, with the same API-key tokens and scopes: eight tools covering loan listing, loan detail, findings, boarding preview, package submission, evidence summary and export, and queue statistics. Every call is audited. See Connecting Claude and other agents and the MCP server reference.
11. Operations for integrators
Section titled “11. Operations for integrators”- Health.
/api/healthz(liveness) and/api/readyz(database reachable and schema at the level this build requires) return JSON with the running version. - Version.
GET /v1/releasesreturns the running version and its release notes; any authenticated principal, including API keys, may call it. Check it after an upgrade if your integration depends on a newer endpoint. - Audit. Administrators and managers can see every API-key exchange, sign-in and MCP call with
GET /v1/audit?source=auth, and row-level data changes withsource=data, or on System → Audit. - Test environment. Point integration development at a non-production install loaded with the golden packages from the release bundle’s validation pack. Their expected fields and findings are in its
manifest.json, which makes a good fixture for automated tests of your integration.
12. Troubleshooting
Section titled “12. Troubleshooting”| Symptom | Check |
|---|---|
| 401 on every call | The token expired (15 minutes by default); exchange the API key again. A revoked key gets 401 at /v1/auth/token. |
| 403 on a call | The key lacks the scope (see 3), or the route requires a signed-in person and refuses API keys. |
| 409 when creating a loan | The reference exists; send "newVersion": true to add a package version. |
| 409 when uploading | The loan is past review; submit a new package version. |
| 400 “only PDF packages and a LAR…” | A non-PDF file whose name is not lar.*; convert it or send it as the LAR. |
| 422 on intake | The license has expired (intake is read-only) or the package would exceed documents.max_package_mb. |
| 413 from the proxy | One request over 256 MB; split the upload. |
Loan stays in processing |
GET /v1/loans/{ref}/pipeline: a retrying stage with nextRetryAt means the inference service is busy or down; a failed stage carries the reason. |
| A value was not extracted | GET /v1/loans/{ref}/fields shows what was read and its confidence; an administrator can map the bank’s form in the template studio. |
| Watch-folder package never appears | The files are still changing, the folder is not writable by the api user, or it moved to rejected/; read bookend-rejected.txt. |
| 429 | Too many requests from one principal, or too many token exchanges from one address; cache the token and back off. |