Agent Reference
This page is optimized for coding agents. Use the linked human pages for explanation, but treat this page as the searchable contract summary.
Public API
Section titled “Public API”Base path: /api/v1.
Authentication for every current endpoint:
Authorization: Bearer ptk_<lookup>.<secret>Bearer auth only. No cookie fallback. API key identifies the organization, not a user.
Current endpoints only:
| Method | Path | Contract |
|---|---|---|
GET | /api/v1/me | Returns { authenticated_at, organization:{ id,name,slug }, api_key:{ id, public_prefix } }. |
GET | /api/v1/users | Returns { users:[{ id,email,name,first_name,last_name,status }] } for the key organization. |
POST | /api/v1/documents | Reserve PDF upload for user_email; returns 202 with a signed storage PUT URL for new content, or an upload-less duplicate resolution (see Handling Duplicate Uploads). |
GET | /api/v1/documents/{documentId} | Return document metadata, confirmation, download, and polling by Plainterms document UUID. |
GET | /api/v1/documents/custId-{customerDocumentId} | Same metadata by customer-supplied document ID. |
GET | /api/v1/documents/{documentId}.pdf | API-key-authenticated rendered PDF resource; 200 application/pdf when ready, 202 with Retry-After while in flight/retrying, 409 JSON when confirmation-gated/not-ready, 410 JSON when terminal. |
POST | /api/v1/documents/confirm | Confirm by exactly one of document_id or customer_document_id plus confirmed_by_email. |
Do not invent: API-key management API, webhook configuration API, document
listing/search API, multipart upload API, bearerless rendered-PDF URL,
uploaded_by request field, OpenAPI document, or root /api/v1 endpoint.
POST /api/v1/documents body:
export interface CreateDocumentRequest { user_email: string; filename: string; file_size_bytes: number; // positive integer, max 20 MB mime_type: 'application/pdf'; content_hash: string; // 64 SHA-256 hex chars; Plainterms normalizes to lowercase idempotency_key?: string; // defaults to `${content_hash}-${filename}` customer_document_id?: string; // <= 512 chars, org-unique when present}Customer document ID rule:
customer_document_idis optional at the API level, but production integrations that need CRM/Retool/policy-system correlation should send one stable value per logical customer document or quote.- Reuse the same
customer_document_idwhen the user retries or re-uploads the same logical PDF. Do not generate a fresh value per button click, browser attempt, or upload retry. idempotency_keyis the reservation retry key.customer_document_idis the customer-side document identity used later for metadata, confirmation, and webhook correlation. They are not interchangeable.- A duplicate/retry upload that sends a new customer ID for an existing logical
document can return
409; look up the existing logical document and resend its originalcustomer_document_id.
Response:
export interface CreateDocumentResponse { document: { id: string; customer_document_id: string | null; // Echoes the caller-supplied request metadata (production-observed). metadata: Record<string, unknown>; storage_path: string; // 'extracting' for new content; the existing document's real status // (for example 'approved') when the reservation resolves a duplicate. status: string; reservation_outcome?: string; // open enum -- observed: idempotent_reuse, duplicate_reuse integration_state?: string; }; user: { id: string; email: string; name: string }; // Present only when next_action is 'upload'. Omitted when next_action is // 'none' -- dispatch on reservation.next_action, never on outcome strings. // See /public-api/#handling-duplicate-uploads. upload?: { method: 'PUT'; url: string; token: string; headers: { 'content-type': 'application/pdf' }; }; // Expected on every 202 per the platform reservation contract; // production-confirmed on duplicate/reuse responses, pending confirmation // on fresh 'created' responses (plainterms#2). Parse tolerantly: if it is // ever absent and an upload object is present, treat as next_action 'upload'. reservation?: { // Treat as open: created | idempotent_reuse | duplicate_reuse | // reopened_for_reupload | duplicate_content (and future values). outcome: string; document_state: string; // Open like outcome: 'upload' | 'none' today. Treat any other value as // an error to surface — never a parse failure, never a silent success. next_action: string; reopened_for_reupload: boolean; }; extraction?: Record<string, unknown>;}After the 202, dispatch on reservation.next_action: 'upload' (the
upload object is present) means PUT the raw PDF bytes to upload.url;
'none' means the content is already satisfied server-side — skip the PUT
and poll/fetch the returned document.id instead. An absent upload object
with any other next_action is an error, not a success.
Document metadata response:
export type ApiDocumentPollingState = | 'in_progress' | 'retrying' | 'ready' | 'confirmation_required' | 'not_ready' | 'terminal';
export interface ApiDocumentResponse { document: { id: string; customer_document_id: string | null; status: string; filename: string; created_at: string; uploaded_at: string; updated_at: string | null; uploaded_by: { id: string | null; email: string | null }; confirmation: { state: 'not_required' | 'required' | 'received'; acknowledged_at: string | null; acknowledged_by: { id: string; email: string | null } | null; }; reason: string | null; extracted_data: Record<string, unknown> | null; value: SavvyPersonalAutoWebhookValue | null; // see Webhooks contract download: { available: boolean; method: 'GET'; url: string; // /api/v1/documents/{documentId}.pdf content_type: 'application/pdf'; authentication: 'api_key'; }; polling: { state: ApiDocumentPollingState; retry_after_seconds?: number; }; };}Metadata GETs return 200 JSON when the document is found. Use
document.polling.retry_after_seconds as the polling hint when
polling.state is in_progress or retrying; metadata responses do not need
a Retry-After header. download.url is not signed or bearerless; fetch it
with the same API key.
GET /api/v1/documents/{documentId}.pdf:
- Requires
Authorization: Bearer <API key>. ?disposition=inlineswitches from attachment to inline.- Ready:
200,Content-Type: application/pdf, PDF bytes. - In flight or retrying:
202,Retry-After, JSON{ error:{ code,message }, document:{ id,customer_document_id,status }, polling:{ state,retry_after_seconds? } }. - Confirmation-gated or not-ready:
409with the same JSON shape. - Terminal no-download state:
410with the same JSON shape. Error codes mirror polling states. - Missing document in the key organization:
404.
POST /api/v1/documents/confirm body:
export type ConfirmDocumentRequest = | { document_id: string; confirmed_by_email: string; customer_document_id?: never } | { customer_document_id: string; confirmed_by_email: string; document_id?: never };Response includes:
export interface ConfirmDocumentResponse { document: { id: string; customer_document_id: string | null; confirmation: { state: 'received'; acknowledged_at: string; acknowledged_by: { id: string; email: string | null } | null; already_acknowledged: boolean; }; }; webhook: | { status: 'started'; eventType: 'confirmation.received'; runId: string } | { status: 'failed_to_start'; eventType: 'confirmation.received'; errorMessage: string } | null;}Common errors: 400 validation, 401 invalid API key, 403 suspended user,
404 same-org user/document not found, 409 conflict/state mismatch, 410
terminal PDF unavailable, 500 server persistence/load/render failure.
API Keys
Section titled “API Keys”Raw format: ptk_<22 base64url lookup id>.<43 base64url secret>.
Storage/auth model:
- Metadata table:
organization_api_keys. - Verifier table:
organization_api_key_verifiers, service-role-only. - Verifier:
base64url(HMAC-SHA-256(API_KEY_PEPPER for verifier_version, raw key)). - Current verifier version:
1. - Auth checks lookup id, revoked/expired metadata, recomputed verifier, and constant-time equality.
- Usage RPC updates
last_used_atanduse_count; usage write failure does not fail otherwise valid auth. - Raw key is displayed once on creation. It is not recoverable.
- Rotation today means create a new key and revoke the old key. There is no
separate rotate operation or
/api/v1key-management endpoint.
Webhooks
Section titled “Webhooks”Configuration is in Settings -> Webhooks only. No /api/v1 webhook
configuration endpoints exist.
Event names:
export type PlaintermsWebhookEventName = | 'document.captured' | 'document.approved' | 'confirmation.required' | 'confirmation.received' | 'document.rejected' | 'landing_page.opened' | 'landing_page.clicked';Signed headers:
export interface PlaintermsWebhookHeaders { 'content-type': 'application/json'; 'webhook-id': string; 'webhook-timestamp': `${number}`; 'webhook-signature'?: `t=${number},v1=${string}`;}Signature algorithm:
hmac = HMAC-SHA-256(webhook_signing_secret, `${t}.${rawJsonBody}`)Webhook-Signature = `t=${t},v1=${hex(hmac)}`Use t from Webhook-Signature, not a parsed JSON timestamp. Verify against
the exact raw body. Deduplicate by Webhook-Id.
Delivery behavior:
- Enabled endpoints delivered independently.
deliverOneWebhook.maxRetries = 5.- Fetch timeout is about 10 seconds.
- HTTP
2xxis success. - Non-
2xx, network errors, and timeouts are failures. - Delivery attempts are logged to
webhook_deliveries. - Stored webhook payloads may include
/api/v1download_url; it has no bearer token and still requires API-key auth.
Download URL enrichment:
- Possible on
document.captured,document.approved,confirmation.received, and document-backedlanding_page.clicked. - Not added for
confirmation.required,document.rejected, orlanding_page.opened. - Not added when the payload’s
confirmation.stateisrequired. - Value is an absolute URL to
/api/v1/documents/{documentId}.pdf. - Receiver must fetch it with the same Plainterms API key.
- No
download_url_expires_atfield.
Payload contract: see Webhooks. Important
document fields: document_id, customer_document_id, required
uploaded_by, required confirmation, optional reason, extracted_data,
typed SavvyPersonalAutoWebhookValue | null, optional download_url.
Retool Pattern
Section titled “Retool Pattern”Use one Retool REST API resource for Plainterms:
- Base URL: Plainterms app origin.
- Auth: Bearer token using the organization API key.
- Global header:
Authorization: Bearer {{ retoolContext.config.plainterms_api_key }}or equivalent secret storage.
Use Retool REST queries:
GET /api/v1/usersto select/validateuser_email.POST /api/v1/documentswith JSON metadata and optionalcustomer_document_id; use a stable value from the Retool row, CRM quote, or policy document record when you need correlation.- When
reservation.next_actionis"upload", binary or rawPUTto the returned signed upload URL for the PDF bytes; when it is"none", skip thePUTand poll the returned document instead (see Handling Duplicate Uploads). GET /api/v1/documents/{documentId}or/api/v1/documents/custId-{customerDocumentId}for polling.GETthe webhookdownload_urlor metadatadownload.urlwith the same API key to retrieve the PDF.
Use a Retool Workflow webhook trigger for Plainterms return routing:
- Plainterms sends JSON payloads.
- Retool exposes incoming data as
startTrigger.headers,startTrigger.pathParams, andstartTrigger.data. - If using Retool’s recommended
X-Workflow-Api-Key, that protects the Retool workflow URL. It is separate from PlaintermsWebhook-Signature. - Native Retool webhook triggers do not by themselves verify the Plainterms
HMAC. Verify
t.<rawBody>in a code/function step if HMAC verification is required by the integration.
Correlation rule: API key -> organization; user_email -> Plainterms user and
document created_by; webhook uploaded_by -> that uploader;
customer_document_id -> caller’s stable document key reused across retries
for the same logical document; webhook document_id -> Plainterms document
UUID.