Partner API Reference

Read across the whole document-of-record platform — abstracts, families, documents & storage, vault & certificates, compliance, connectors & hierarchy, and audit logs — run semantic searches, and subscribe to HMAC-signed webhooks. This page is self-contained — forward it to your integration team as-is.
VERSION 1.1 · STABLE

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:

DomainWhat a partner doesEndpoints
Documents & storagePush documents in; mirror the document register, storage tiers & dedupPOST /v1/documents, GET /v1/documents, /v1/storage-objects, /v1/duplicates, /v1/assets
Vault & certificationRead certified documents of record, certificates & Sentry fingerprints/v1/vault, /v1/certificates
CompliancePull the requirement matrix & current compliance state/v1/compliance-requirements, /v1/compliance-status
Connectors & hierarchyRead connector health & the org taxonomy/nodes/v1/connectors, /v1/hierarchy-nodes, /v1/counterparties
Access & auditStream the access log & lifecycle audit trail/v1/access-logs, /v1/audit-logs
ReportsPull 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
EventsReact when documents are ingested or abstracteddocument.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:

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.

ScopeGrants
abstracts:readList and read abstracts
families:readList and read document families
documents:readDocuments, storage objects, dedup candidates, assets + signed source-file URLs
searchRun semantic / hybrid search
vault:readVault records & document certificates
compliance:readCompliance requirements & status
connectors:readIntegration connectors (no credentials), hierarchy & counterparties
audit:readDocument access & audit logs
reports:readSubmitted reports (rent rolls, arrears, trial balances) & their line items, QC results, correlation matches, and exceptions
documents:writeWrite — ingest documents into Gateway. Grant deliberately.
webhooks:manageRegister, list, and delete webhooks
Tenant scoping. A key may be bound to a single tenant. When it is, every response is automatically filtered to that tenant's data. Sample/demo data is never returned through the Partner API.

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.

StageAPI key (aidi_…)Webhook secret (whsec_…)
Issued byYour 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.
ShownOnce, at creation. ~192 bits of entropy.Once, in the POST /v1/webhooks response (secret field).
Stored by imkoreOnly a SHA-256 hash + a short display prefix. The full key is never recoverable.The secret, used only to compute delivery signatures.
CarriesA fixed set of scopes and an optional tenant binding, both chosen at issue time.The set of events you subscribed to.
Used forAuthorization: Bearer aidi_… on every request.Verifying the X-Imkore-Signature on each webhook — see Verifying signatures.
RotatedRevoke + 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 changeIssue 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.
Storage guidance. Keep the API key in a server-side secret store (your vault, environment variable, or secrets manager) — never in client-side code, a repo, or a URL. Use a read-only, tenant-scoped key unless you specifically need 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" } }
StatuscodeMeaning
400bad_requestMissing/invalid parameter
401unauthorizedMissing, malformed, or revoked key
403forbiddenKey lacks the required scope
404not_foundResource does not exist (or not in your tenant)
500internalServer 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

GET/v1/me no scope required

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.

GET/v1/abstracts scope abstracts:read

List abstracts, newest first.

Query paramTypeNotes
limitinteger1–100, default 25
offsetintegerdefault 0
document_typestringExact match, e.g. Absolute Net Lease Agreement
document_categorystringExact match
family_keystringOnly abstracts in this family
sinceISO-8601Only 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 }
}
GET/v1/abstracts/{id} scope abstracts:read

Fetch a single abstract in full, including all field_groups, risk_flags, key_clauses, and summary.

GET/v1/abstracts/{id}/source scope documents:read

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

GET/v1/families scope families:read

List families (paginated), most recently updated first.

GET/v1/families/{key} scope families:read

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

GET/v1/abstracts/{id}/pdf scope abstracts:read

Render (or reuse) one artifact for this abstract and return a signed download URL.

Query paramTypeNotes
kindstringone 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.

GET/v1/families/{key}/pdf scope families:read

Same four kind options, rendered for the family's master abstract instead of a single member document.

GET/v1/abstracts/{id}/documents scope abstracts:read

All 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" }
  ] }
GET/v1/families/{key}/documents scope families:read

The same all-four-artifacts call, keyed by family instead of a single abstract.

POST/v1/search scope search

Semantic + keyword (hybrid) search across the abstracted corpus.

Body fieldTypeNotes
query requiredstringNatural-language query
limitinteger1–100, default 25
document_typestringRestrict to a type
document_categorystringRestrict to a category
kindsstring[]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

ResourceReturnsFilters
/v1/documentsLogical document records (type, status, version, dates, content hash, asset)status, workflow_status, document_type_id, asset_id, content_hash, since
/v1/storage-objectsPhysical file registry (hash, size, MIME, tier, reference count)storage_tier, content_hash
/v1/duplicatesNear-duplicate candidates awaiting review (method, similarity)status, source_document_id
/v1/assetsAssets/properties documents are filed againstasset_type, status, state

Vault & certification — scope vault:read

ResourceReturnsFilters
/v1/vaultImmutable vault records (serial number, content hash, Sentry fingerprint, vaulted-at)document_id, serial_number, client_id
/v1/certificatesDocument certificates (fingerprint, status, version, page count)document_id, status

Compliance — scope compliance:read

ResourceReturnsFilters
/v1/compliance-requirementsRequired document types per entity/periodclient_id, property_id, document_type
/v1/compliance-statusCurrent compliance state per requirementrequirement_id, document_id, is_compliant

Connectors & hierarchy — scope connectors:read

ResourceReturnsFilters
/v1/connectorsIntegration connectors & health (credentials never exposed)connector_type, is_active, health_status
/v1/hierarchy-configurationsHierarchy/taxonomy configurationsis_active, is_default
/v1/hierarchy-levelsLevels within a configurationconfiguration_id, level_key
/v1/hierarchy-nodesNodes with document counts (tenant-scoped)configuration_id, level_key, parent_id, status
/v1/counterpartiesCounterparty organizationsrole, tier, room_id

Access & audit — scope audit:read

ResourceReturnsFilters
/v1/access-logsEvery document access event (forever-retained)document_id, access_type, since
/v1/audit-logsDocument 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.

ResourceReturnsFilters
/v1/reportsSubmitted reports (type, period, ingest status, source format)node_id, report_type, ingest_status, since
/v1/report-line-itemsParsed row-level data for a reportreport_id
/v1/report-qc-resultsQuality-control checks run against a submitted reportreport_id, packet_document_id, status
/v1/report-correlation-resultsCross-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-exceptionsOpen discrepancies surfaced from QC/correlation, with first/last-seen trackingnode_id, report_id, status, exception_type
Not yet available. Approvals/workflow, distribution packages, and classification resources are on the roadmap; their tables aren’t in the live schema yet, so the API does not expose them. They’ll appear here when released — the OpenAPI spec is always the authoritative, current list.

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

POST/v1/documents/upload-url scope documents:write
Body fieldTypeNotes
file_name requiredstringOriginal file name
mime_type requiredstringe.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:

POST/v1/documents scope documents:write
Body fieldTypeNotes
storage_path required*stringPath returned by /v1/documents/upload-url after you PUT the bytes. Preferred for all but tiny files.
file_base64 *stringAlternative to storage_path — inline base64 for small files only (≤ 6 MB). Larger inline bodies are rejected (413).
file_name requiredstringOriginal file name
mime_type requiredstringe.g. application/pdf
file_sizeintegerOptional byte-size hint when using storage_path
document_typestringOptional type hint
asset_iduuidFile against an asset
metadataobjectArbitrary 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.

EventStatusFires when
abstract.completedliveOne or more abstracts finished processing (includes their ids — fetch with /v1/abstracts/{id})
family.reconciledavailableA document family's membership/rollup was (re)computed
document.ingestedliveA 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:

HeaderValue
X-Imkore-EventThe event name
X-Imkore-DeliveryUnique delivery id (for idempotency)
X-Imkore-TimestampUnix seconds when sent
X-Imkore-Signaturesha256=<hmac> — see verification

Managing webhooks

POST/v1/webhooks scope webhooks:manage

Register 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"]}'
GET/v1/webhooks scope webhooks:manage

List your registered webhooks (secrets are never returned again).

DELETE/v1/webhooks/{id} scope webhooks:manage

Remove 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

FieldTypeDescription
iduuidStable abstract identifier
file_namestringSource file name
file_size / file_typeint / stringBytes and MIME type
document_typestringClassified type (e.g. lease, amendment)
document_categorystringHigher-level grouping
summarystringNatural-language summary
field_groupsarrayExtracted fields grouped by section: [{group_name, fields:[{label,value}]}]
risk_flagsarrayDetected risks / unusual terms
key_clausesarrayNotable clauses
missing_or_unusualarrayExpected-but-absent or atypical items
total_fieldsintCount of extracted fields
classification_confidencenumber0–100 confidence of the type classification
document_family_keystringFamily this document belongs to (null if standalone)
sequence_rolestringoriginal, amendment, …
effective_date_parseddateParsed effective date
source_connector_type / source_external_idstringWhere the document originated (e.g. filestar + its id)
created_atdatetimeWhen 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.

VersionDateNotes
v1.12026-07-31Added 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.02026-06-22Initial release: abstracts, families, documents, search, webhooks.