Partner API Reference
Introduction
Document Gateway is the clearinghouse for an organization’s documents of record — it ingests, certifies, vaults, classifies, abstracts, and tracks compliance for every document across its lifecycle. The Partner API is the programmatic surface over that whole platform: an external system — your DMS, data warehouse, ERP/CLM, analytics platform, or workflow engine — can push documents in, read any part of the record (documents & storage, vault & certificates, compliance, connectors & hierarchy, audit trail, plus AI abstracts), search, and react to events in real time.
The API spans the platform’s domains, not a single feature:
| Domain | What a partner does | Endpoints |
|---|---|---|
| Documents & storage | Push documents in; mirror the document register, storage tiers & dedup | POST /v1/documents, GET /v1/documents, /v1/storage-objects, /v1/duplicates, /v1/assets |
| Vault & certification | Read certified documents of record, certificates & Sentry fingerprints | /v1/vault, /v1/certificates |
| Compliance | Pull the requirement matrix & current compliance state | /v1/compliance-requirements, /v1/compliance-status |
| Connectors & hierarchy | Read connector health & the org taxonomy/nodes | /v1/connectors, /v1/hierarchy-nodes, /v1/counterparties |
| Access & audit | Stream the access log & lifecycle audit trail | /v1/access-logs, /v1/audit-logs |
| Reports | Pull submitted rent rolls/arrears/TB reports, their line items, QC, and correlation to the document set | /v1/reports, /v1/report-line-items, /v1/report-qc-results, /v1/report-correlation-results, /v1/report-exceptions |
| Document intelligence (Abstract.DI) | Mirror AI abstracts & families, retrieve sources, search | /v1/abstracts, /v1/families, POST /v1/search |
| Events | React when documents are ingested or abstracted | document.ingested, abstract.completed webhooks |
Document intelligence (abstracts/families) is one domain among several — see Platform resources for the full catalog and Ingest to push documents in.
Quickstart
1 · Get an API key
In Gateway, open API Marketplace → API Keys and issue a partner key. The key is
shown once — store it securely. Keys are prefixed aidi_
and carry a set of scopes.
2 · Make your first call
# Confirm your key and see its scopes
curl https://nwbjvdemkdvlzdbntgnd.supabase.co/functions/v1/partner-api/v1/me \
-H "Authorization: Bearer aidi_YOUR_KEY"
// 200 OK
{ "key_id": "8853f747-…", "scopes": ["abstracts:read","families:read","documents:read","search","webhooks:manage"], "tenant_id": null }
3 · Pull your abstracts
curl ".../partner-api/v1/abstracts?limit=10" \
-H "Authorization: Bearer aidi_YOUR_KEY"
Data-science quickstart
The thing that sets AI.DI apart: you don’t just get documents and metadata — you get the real intelligence. AI.DI abstracts any document, in any industry — contracts, agreements, invoices, policies, regulatory filings, certificates, HR records, financial statements — into structured fields, key clauses, and risk flags. In about a minute you can pull that intelligence into a pandas DataFrame and build on it.
From zero to a DataFrame of document intelligence
# pip install requests pandas
import requests, pandas as pd
API = "https://nwbjvdemkdvlzdbntgnd.supabase.co/functions/v1/partner-api"
H = {"Authorization": "Bearer aidi_YOUR_KEY"}
# 1 · Pull abstracts — each includes the FULL extracted intelligence
# (field_groups, key_clauses, risk_flags), not just metadata.
abstracts = requests.get(f"{API}/v1/abstracts?limit=100", headers=H).json()["data"]
# 2 · Flatten every extracted field into a tidy DataFrame
rows = [
{"document": a["file_name"], "doc_type": a.get("document_type"),
"group": g.get("group"), "field": f.get("label"), "value": f.get("value")}
for a in abstracts
for g in a.get("field_groups", [])
for f in g.get("fields", [])
]
df = pd.DataFrame(rows)
# 3 · Now query the intelligence like any dataset — pivot, filter, join, model
expirations = df[df.field.str.contains("Expiration", case=False, na=False)]
print(expirations.head())
# → any extracted field, across every document type, in one frame.
Ask in natural language (semantic search)
# Meaning-based search across the abstract intelligence — any document type
hits = requests.post(f"{API}/v1/search", headers=H, json={
"query": "agreements with an auto-renewal clause expiring in 2026",
"limit": 20,
}).json()["data"]
Connect an AI agent (MCP)
AI.DI ships a built-in MCP server — the same intelligence exposed as 24 tools any AI agent can call. Connect Claude (claude.ai one-click OAuth 2.1 connector, Claude Code, or Claude Desktop), ChatGPT (GPT Actions), or your own Python / LangChain agent — all from one workspace-scoped key. Read-only, RLS-enforced, scoped to your data.
The toolset spans the whole platform — get_abstract (a document’s full
field groups, clauses & risk flags), query_abstract_fields (query extracted
fields across every document at once), get_family_abstract (rolled-up terms for a
grouped document set, e.g. a contract and its amendments), plus search, compliance, obligations,
anomalies, expirations, and more. Mint an agent key in Warehouse → API Keys → Connect Claude,
then point any MCP client at:
https://nwbjvdemkdvlzdbntgnd.supabase.co/functions/v1/mcp-server/mcp # Bearer dg_agent_YOUR_KEY
# Claude Code — one command:
claude mcp add aidi --transport http .../functions/v1/mcp-server/mcp \
--header "Authorization: Bearer dg_agent_YOUR_KEY"
What you can build on top of AI.DI
Because the structured intelligence is an API, your data scientists and product teams can build directly on it — no document parsing, no model to train:
- Analytics & dashboards — turn any document set into a queryable dataset: registries, obligation calendars, expirations, exposure, KPIs — straight from the abstract fields.
- Models & pipelines — feed clean document features into your own models in Snowflake, Databricks, or a notebook — no parsing, no model to train.
- Agents & copilots — attach Claude or ChatGPT and let it answer in natural language over your real documents, grounded and cited.
- Apps & extensions — embed AI.DI intelligence into the systems your clients already run (ERP, CLM, ICM, BI), or ship a new product on top of it.
- Event-driven workflows — subscribe to
abstract.completedwebhooks and trigger downstream automation the moment a document is understood.
Authentication & scopes
Every request (except the public OpenAPI spec) must include a bearer token:
Authorization: Bearer aidi_YOUR_KEY
Keys are stored only as SHA-256 hashes — Gateway cannot recover a lost key; rotate it
(revoke + reissue) instead. Each key is authorized for a set of scopes; a call
to an endpoint outside the key's scopes returns 403 forbidden.
| Scope | Grants |
|---|---|
abstracts:read | List and read abstracts |
families:read | List and read document families |
documents:read | Documents, storage objects, dedup candidates, assets + signed source-file URLs |
search | Run semantic / hybrid search |
vault:read | Vault records & document certificates |
compliance:read | Compliance requirements & status |
connectors:read | Integration connectors (no credentials), hierarchy & counterparties |
audit:read | Document access & audit logs |
reports:read | Submitted reports (rent rolls, arrears, trial balances) & their line items, QC results, correlation matches, and exceptions |
documents:write | Write — ingest documents into Gateway. Grant deliberately. |
webhooks:manage | Register, list, and delete webhooks |
Key & secret lifecycle
There are two secrets in an integration: your API key (you send it on every request) and a webhook signing secret (we use it to sign callbacks to you). Both are shown exactly once and stored only as hashes/secrets on our side — treat them like passwords.
| Stage | API key (aidi_…) | Webhook secret (whsec_…) |
|---|---|---|
| Issued by | Your imkore administrator, in Gateway → API Marketplace → API Keys. There is no self-service signup — request a key from your contact. | Automatically, when you register an endpoint via POST /v1/webhooks. |
| Shown | Once, at creation. ~192 bits of entropy. | Once, in the POST /v1/webhooks response (secret field). |
| Stored by imkore | Only a SHA-256 hash + a short display prefix. The full key is never recoverable. | The secret, used only to compute delivery signatures. |
| Carries | A fixed set of scopes and an optional tenant binding, both chosen at issue time. | The set of events you subscribed to. |
| Used for | Authorization: Bearer aidi_… on every request. | Verifying the X-Imkore-Signature on each webhook — see Verifying signatures. |
| Rotated | Revoke + reissue (there is no “reset” — the old key can’t be recovered). Old key returns 401 immediately on revoke. | Delete the webhook and register a new one to get a fresh secret. |
| Scope change | Issue a new key with the new scopes/tenant and retire the old one. Scopes are fixed per key. | Re-register with the new event list. |
documents:write. If a key is exposed, ask your administrator to revoke it;
revocation is instant.Base URL & format
https://nwbjvdemkdvlzdbntgnd.supabase.co/functions/v1/partner-api
All responses are JSON. Successful resource responses wrap the payload in
data; list responses add a pagination
object. All timestamps are ISO-8601 UTC.
Pagination
List endpoints accept limit (1–100, default 25) and
offset (default 0), and return a pagination
block with the exact total count.
{
"data": [ … ],
"pagination": { "limit": 25, "offset": 0, "total": 74, "returned": 25 }
}
For incremental sync, page by created_at using the
since filter on /v1/abstracts rather than
deep offsets.
Errors
Errors use standard HTTP status codes and a consistent envelope:
{ "error": { "code": "forbidden", "message": "Key lacks scope abstracts:read" } }
| Status | code | Meaning |
|---|---|---|
| 400 | bad_request | Missing/invalid parameter |
| 401 | unauthorized | Missing, malformed, or revoked key |
| 403 | forbidden | Key lacks the required scope |
| 404 | not_found | Resource does not exist (or not in your tenant) |
| 500 | internal | Server error — safe to retry with backoff |
Rate limits & versioning
Be a good citizen: keep sustained traffic under ~120 requests/minute per key and
use the since cursor instead of polling full lists. The API is
versioned in the path (/v1/…); breaking changes ship under a new
version and the previous version remains available during a deprecation window announced
in the changelog.
Identity
Returns the calling key's id, scopes, and tenant binding. Use it to validate a key and discover what it can do.
Abstracts
An abstract is the structured extraction of a single document: typed fields grouped by section, risk flags, key clauses, a summary, and provenance. See the Abstract object reference.
abstracts:readList abstracts, newest first.
| Query param | Type | Notes |
|---|---|---|
limit | integer | 1–100, default 25 |
offset | integer | default 0 |
document_type | string | Exact match, e.g. Absolute Net Lease Agreement |
document_category | string | Exact match |
family_key | string | Only abstracts in this family |
since | ISO-8601 | Only abstracts created at/after this time |
curl ".../v1/abstracts?document_type=Absolute%20Net%20Lease%20Agreement&limit=2" \
-H "Authorization: Bearer aidi_YOUR_KEY"
{
"data": [
{ "id": "…", "file_name": "Lease Agreement Fetna - 599.pdf",
"document_type": "Absolute Net Lease Agreement",
"document_family_key": "grange conglomerate inc|100 hayden avenue…",
"total_fields": 142, "created_at": "2026-06-22T11:25:40Z" }
],
"pagination": { "limit": 2, "offset": 0, "total": 74, "returned": 2 }
}
abstracts:readFetch a single abstract in full, including all field_groups,
risk_flags, key_clauses, and
summary.
documents:readReturns a short-lived (15-minute) signed URL to the original source document. The file never moves and the URL expires automatically.
{ "id": "…", "file_name": "Lease Agreement Fetna - 599.pdf",
"file_type": "application/pdf",
"download_url": "https://…signed…", "expires_in": 900 }
Families
A family groups a primary document with its amendments, SNDAs, and related instruments (e.g. a lease and everything that modifies it). Members are deduplicated to the most recent abstract per source document and ordered by effective date.
families:readList families (paginated), most recently updated first.
families:readFetch one family by its family_key (URL-encode it), including
the rolled-up master_abstract and the ordered member list.
curl ".../v1/families/grange%20conglomerate%20inc%7C100%20hayden%20avenue…" \
-H "Authorization: Bearer aidi_YOUR_KEY"
{ "data": {
"family_name": "Grange conglomerate inc | 100 Hayden Avenue…",
"family_type": "lease", "member_count": 2,
"members": [
{ "file_name": "Lease Agreement Fetna - 599.pdf", "sequence_role": "original" },
{ "file_name": "SNDA and Amendment… - 600.pdf", "sequence_role": "amendment" }
] } }
Rendered documents
Pull the actual rendered files behind an abstract or family — not just the extracted data — as short-lived signed download URLs. Generated on demand (or reused if already rendered): the source Lease Abstract, an Excel workbook of the same, an Audit Exceptions workbook, and the Hawkeye Audit Report.
abstracts:readRender (or reuse) one artifact for this abstract and return a signed download URL.
| Query param | Type | Notes |
|---|---|---|
kind | string | one of abstract (Lease Abstract PDF, default), audit (Hawkeye Audit Report PDF), excel (Lease Abstract Excel), exceptions (Audit Exceptions Excel) |
curl ".../v1/abstracts/{id}/pdf?kind=audit" \
-H "Authorization: Bearer aidi_YOUR_KEY"
{ "abstract_id": "…", "family_key": "…", "kind": "audit",
"content_type": "application/pdf", "download_url": "https://…signed…", "expires_in": 900 }
422 if the abstract isn't part of a document family — the audit/exceptions artifacts are family-level.
families:readSame four kind options, rendered for the family's
master abstract instead of a single member document.
abstracts:readAll four artifacts in one call — Lease Abstract (PDF), Lease Abstract (Excel), Audit
Exceptions (Excel), and the Hawkeye Audit Report (PDF) — each a signed
download_url, or an error string for
whichever one couldn't be produced (e.g. exceptions requires a saved ERP audit).
{ "abstract_id": "…", "family_key": "…",
"documents": [
{ "kind": "abstract", "label": "Lease Abstract", "content_type": "application/pdf", "download_url": "…", "expires_in": 900 },
{ "kind": "exceptions", "error": "No saved ERP audit for this family" }
] }
families:readThe same all-four-artifacts call, keyed by family instead of a single abstract.
Search
searchSemantic + keyword (hybrid) search across the abstracted corpus.
| Body field | Type | Notes |
|---|---|---|
query required | string | Natural-language query |
limit | integer | 1–100, default 25 |
document_type | string | Restrict to a type |
document_category | string | Restrict to a category |
kinds | string[] | clause, risk_flag, field, summary |
curl -X POST ".../v1/search" \
-H "Authorization: Bearer aidi_YOUR_KEY" -H "Content-Type: application/json" \
-d '{"query":"rent escalation in net leases","limit":5}'
Platform resources
Beyond abstracts, the API exposes read access to the rest of the
document-of-record platform. Every resource below follows the same shape:
GET /v1/{resource} (paginated list with the standard
limit/offset + the filters noted)
and GET /v1/{resource}/{id} (single record). All return the
{ data, pagination } envelope and honor your key's scopes and
tenant binding. Sample/demo records are never returned.
Documents & storage — scope documents:read
| Resource | Returns | Filters |
|---|---|---|
/v1/documents | Logical document records (type, status, version, dates, content hash, asset) | status, workflow_status, document_type_id, asset_id, content_hash, since |
/v1/storage-objects | Physical file registry (hash, size, MIME, tier, reference count) | storage_tier, content_hash |
/v1/duplicates | Near-duplicate candidates awaiting review (method, similarity) | status, source_document_id |
/v1/assets | Assets/properties documents are filed against | asset_type, status, state |
Vault & certification — scope vault:read
| Resource | Returns | Filters |
|---|---|---|
/v1/vault | Immutable vault records (serial number, content hash, Sentry fingerprint, vaulted-at) | document_id, serial_number, client_id |
/v1/certificates | Document certificates (fingerprint, status, version, page count) | document_id, status |
Compliance — scope compliance:read
| Resource | Returns | Filters |
|---|---|---|
/v1/compliance-requirements | Required document types per entity/period | client_id, property_id, document_type |
/v1/compliance-status | Current compliance state per requirement | requirement_id, document_id, is_compliant |
Connectors & hierarchy — scope connectors:read
| Resource | Returns | Filters |
|---|---|---|
/v1/connectors | Integration connectors & health (credentials never exposed) | connector_type, is_active, health_status |
/v1/hierarchy-configurations | Hierarchy/taxonomy configurations | is_active, is_default |
/v1/hierarchy-levels | Levels within a configuration | configuration_id, level_key |
/v1/hierarchy-nodes | Nodes with document counts (tenant-scoped) | configuration_id, level_key, parent_id, status |
/v1/counterparties | Counterparty organizations | role, tier, room_id |
Access & audit — scope audit:read
| Resource | Returns | Filters |
|---|---|---|
/v1/access-logs | Every document access event (forever-retained) | document_id, access_type, since |
/v1/audit-logs | Document lifecycle audit trail (events, actors) | document_id, event_type, node_id, since |
Reports — scope reports:read
Reports (rent rolls, arrears, trial balances, P&L) ride the same submission rails as documents — validated, QC'd, and cross-referenced against the document set, not just stored files.
| Resource | Returns | Filters |
|---|---|---|
/v1/reports | Submitted reports (type, period, ingest status, source format) | node_id, report_type, ingest_status, since |
/v1/report-line-items | Parsed row-level data for a report | report_id |
/v1/report-qc-results | Quality-control checks run against a submitted report | report_id, packet_document_id, status |
/v1/report-correlation-results | Cross-references between report line items and the document set (e.g. a rent-roll tenant matched to its lease) | report_id, rule_id, match_status |
/v1/report-exceptions | Open discrepancies surfaced from QC/correlation, with first/last-seen tracking | node_id, report_id, status, exception_type |
Ingest a document (write)
Push a file into Gateway. The bytes run the full ingest pipeline — SHA-256 hash,
exact-duplicate check, text extraction, fuzzy fingerprinting, tiered storage, and record
creation — and fire the document.ingested webhook. This is the
only write endpoint; it needs the dedicated documents:write scope,
which you grant deliberately (it is not in the default read-only key).
Ingest is asynchronous: the call returns 202 Accepted right
away and the pipeline runs in the background. You get the final
document_id, content_hash, and
result from the document.ingested webhook
(or by polling GET /v1/documents) — not in the immediate response.
Large files — upload directly to storage (recommended)
Do not send multi-MB files as base64 inside the JSON body — large request bodies do
not pass reliably through the API gateway (you may see a 504).
Instead, upload the raw bytes straight to storage with a short-lived signed URL, then
start ingest with just the path. Three steps:
1. Ask for a signed upload URL (tiny request):
documents:write| Body field | Type | Notes |
|---|---|---|
file_name required | string | Original file name |
mime_type required | string | e.g. application/pdf |
curl -X POST ".../v1/documents/upload-url" \
-H "Authorization: Bearer aidi_YOUR_KEY" -H "Content-Type: application/json" \
-d '{"file_name":"lease.pdf","mime_type":"application/pdf"}'
// 200 OK
{ "data": { "upload_url": "https://…/storage/v1/object/upload/sign/…?token=…",
"storage_path": "partner-ingest/…/lease.pdf",
"method": "PUT", "headers": { "Content-Type": "application/pdf", "x-upsert": "true" } } }
2. PUT the raw file bytes to upload_url
(this goes directly to storage and bypasses the API gateway — no size problem):
curl -X PUT "<upload_url>" \
-H "Content-Type: application/pdf" -H "x-upsert: true" \
--data-binary "@lease.pdf"
3. Start ingest with the returned storage_path:
documents:write| Body field | Type | Notes |
|---|---|---|
storage_path required* | string | Path returned by /v1/documents/upload-url after you PUT the bytes. Preferred for all but tiny files. |
file_base64 * | string | Alternative to storage_path — inline base64 for small files only (≤ 6 MB). Larger inline bodies are rejected (413). |
file_name required | string | Original file name |
mime_type required | string | e.g. application/pdf |
file_size | integer | Optional byte-size hint when using storage_path |
document_type | string | Optional type hint |
asset_id | uuid | File against an asset |
metadata | object | Arbitrary key/values stored with the document |
* Provide either storage_path (preferred) or file_base64 (small files only).
curl -X POST ".../v1/documents" \
-H "Authorization: Bearer aidi_YOUR_KEY" -H "Content-Type: application/json" \
-d '{"storage_path":"partner-ingest/…/lease.pdf","file_name":"lease.pdf","mime_type":"application/pdf"}'
// 202 Accepted — ingest runs asynchronously
{ "data": { "status": "processing", "storage_path": "partner-ingest/…/lease.pdf",
"file_name": "lease.pdf", "file_size": 123456 } }
When the pipeline finishes, the document.ingested webhook delivers
the document_id, content_hash, and
result — one of STORED_NEW,
EXACT_DUPLICATE (byte-identical file already stored — a record is
still created, the file is not re-stored), or NEAR_DUPLICATE.
Small files — inline base64 (≤ 6 MB)
For small files you can skip the signed-URL step and inline the bytes directly. Same async
202 response as above.
curl -X POST ".../v1/documents" \
-H "Authorization: Bearer aidi_YOUR_KEY" -H "Content-Type: application/json" \
-d '{"file_name":"lease.pdf","mime_type":"application/pdf","file_base64":"JVBERi0x..."}'
Webhooks — events & payloads
Subscribe to events instead of polling. Gateway POSTs a signed JSON payload to your URL when an event fires.
| Event | Status | Fires when |
|---|---|---|
abstract.completed | live | One or more abstracts finished processing (includes their ids — fetch with /v1/abstracts/{id}) |
family.reconciled | available | A document family's membership/rollup was (re)computed |
document.ingested | live | A document entered the pipeline (fires on every POST /v1/documents ingest) |
Delivery payload:
{
"event": "abstract.completed",
"delivery_id": "f1e2…",
"created_at": "2026-06-22T12:30:00Z",
"data": { "job_id": "…", "abstract_ids": ["…","…"], "count": 2 }
}
Headers on every delivery:
| Header | Value |
|---|---|
X-Imkore-Event | The event name |
X-Imkore-Delivery | Unique delivery id (for idempotency) |
X-Imkore-Timestamp | Unix seconds when sent |
X-Imkore-Signature | sha256=<hmac> — see verification |
Managing webhooks
webhooks:manageRegister an endpoint. The response includes a secret
(prefixed whsec_) returned only once — store it to
verify signatures.
curl -X POST ".../v1/webhooks" \
-H "Authorization: Bearer aidi_YOUR_KEY" -H "Content-Type: application/json" \
-d '{"url":"https://your-system.example.com/imkore","events":["abstract.completed"]}'
webhooks:manageList your registered webhooks (secrets are never returned again).
webhooks:manageRemove a webhook.
Verifying signatures
Recompute the HMAC-SHA256 of "{X-Imkore-Timestamp}.{rawRequestBody}"
using your webhook secret, then constant-time compare it to the hex in
X-Imkore-Signature. Reject if it doesn't match or the timestamp is
stale (> 5 min).
// Node.js (Express)
import crypto from 'crypto';
function verify(req, secret) {
const ts = req.headers['x-imkore-timestamp'];
const sig = req.headers['x-imkore-signature'].replace('sha256=', '');
const mac = crypto.createHmac('sha256', secret)
.update(ts + '.' + req.rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(sig));
}
# Python (Flask)
import hmac, hashlib
def verify(headers, raw_body, secret):
ts = headers['X-Imkore-Timestamp']
sig = headers['X-Imkore-Signature'].replace('sha256=', '')
mac = hmac.new(secret.encode(), f"{ts}.{raw_body}".encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, sig)
Abstract object
| Field | Type | Description |
|---|---|---|
id | uuid | Stable abstract identifier |
file_name | string | Source file name |
file_size / file_type | int / string | Bytes and MIME type |
document_type | string | Classified type (e.g. lease, amendment) |
document_category | string | Higher-level grouping |
summary | string | Natural-language summary |
field_groups | array | Extracted fields grouped by section: [{group_name, fields:[{label,value}]}] |
risk_flags | array | Detected risks / unusual terms |
key_clauses | array | Notable clauses |
missing_or_unusual | array | Expected-but-absent or atypical items |
total_fields | int | Count of extracted fields |
classification_confidence | number | 0–100 confidence of the type classification |
document_family_key | string | Family this document belongs to (null if standalone) |
sequence_role | string | original, amendment, … |
effective_date_parsed | date | Parsed effective date |
source_connector_type / source_external_id | string | Where the document originated (e.g. filestar + its id) |
created_at | datetime | When the abstract was produced |
OpenAPI spec
A machine-readable OpenAPI 3.1 document is served (no auth) for code generation and import into Postman / Insomnia / Swagger UI:
GET https://nwbjvdemkdvlzdbntgnd.supabase.co/functions/v1/partner-api/openapi.json
Support & changelog
Questions, key provisioning, or tenant scoping: contact your imkore representative or developers@imkore.com.
| Version | Date | Notes |
|---|---|---|
| v1.1 | 2026-07-31 | Added the Reports domain (reports:read) and rendered-document endpoints (/pdf, /documents) for pulling the actual Lease Abstract, Audit Report, and Excel/exceptions artifacts, not just extracted data. |
| v1.0 | 2026-06-22 | Initial release: abstracts, families, documents, search, webhooks. |