Skip to content

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.

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:

MethodPathContract
GET/api/v1/meReturns { authenticated_at, organization:{ id,name,slug }, api_key:{ id, public_prefix } }.
GET/api/v1/usersReturns { users:[{ id,email,name,first_name,last_name,status }] } for the key organization.
POST/api/v1/documentsReserve 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}.pdfAPI-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/confirmConfirm 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_id is 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_id when 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_key is the reservation retry key. customer_document_id is 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 original customer_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=inline switches 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: 409 with the same JSON shape.
  • Terminal no-download state: 410 with 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.

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_at and use_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/v1 key-management endpoint.

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 2xx is success.
  • Non-2xx, network errors, and timeouts are failures.
  • Delivery attempts are logged to webhook_deliveries.
  • Stored webhook payloads may include /api/v1 download_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-backed landing_page.clicked.
  • Not added for confirmation.required, document.rejected, or landing_page.opened.
  • Not added when the payload’s confirmation.state is required.
  • 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_at field.

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.

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/users to select/validate user_email.
  • POST /api/v1/documents with JSON metadata and optional customer_document_id; use a stable value from the Retool row, CRM quote, or policy document record when you need correlation.
  • When reservation.next_action is "upload", binary or raw PUT to the returned signed upload URL for the PDF bytes; when it is "none", skip the PUT and poll the returned document instead (see Handling Duplicate Uploads).
  • GET /api/v1/documents/{documentId} or /api/v1/documents/custId-{customerDocumentId} for polling.
  • GET the webhook download_url or metadata download.url with 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, and startTrigger.data.
  • If using Retool’s recommended X-Workflow-Api-Key, that protects the Retool workflow URL. It is separate from Plainterms Webhook-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.