Skip to content

Public API

The Plainterms public API is rooted at /api/v1. Every current endpoint requires an organization API key in the Authorization header:

Authorization: Bearer ptk_<lookup>.<secret>

API keys identify the organization. They do not identify a Plainterms user. Endpoints that act on behalf of a user take a user email and resolve that email inside the API key’s organization.

MethodPathPurpose
GET/api/v1/meValidate the key and return the organization/key identity.
GET/api/v1/usersList Plainterms users in the key’s organization.
POST/api/v1/documentsReserve a PDF upload for a same-organization user. Every 202 carries a reservation whose next_action says whether to PUT to a signed URL ("upload") or skip the upload entirely ("none") — see Handling Duplicate Uploads.
GET/api/v1/documents/{documentId}Read document metadata, polling state, confirmation state, and the API-key-authenticated PDF download resource.
GET/api/v1/documents/custId-{customerDocumentId}Read the same metadata by customer-supplied document ID.
GET/api/v1/documents/{documentId}.pdfDownload or poll the rendered branded PDF with the same API key.
POST/api/v1/documents/confirmRecord threshold confirmation for a document by Plainterms ID or customer document ID.

There is no bare /api/v1 route and no public document listing/search, multipart upload, API-key management, webhook configuration, OpenAPI discovery, or bearerless rendered-PDF URL endpoint.

Plainterms uses standard HTTP status codes with text or JSON error messages from the server route. Common codes:

StatusMeaning
400Request body shape, path identifier, or field validation failed.
401Missing, malformed, unknown, revoked, expired, or wrong API key.
403The referenced user is suspended, or a user-level action is not allowed.
404A user or document was not found inside the API key’s organization.
409The operation conflicts with current document state, confirmation state, or the requested customer_document_id.
410The rendered PDF is permanently unavailable because the document reached a terminal no-download state.
500Plainterms could not load, render, or persist required server state.

Returns the organization and API-key identity for the supplied Bearer key.

{
"authenticated_at": "2026-06-24T12:00:00.000Z",
"organization": {
"id": "00000000-0000-4000-8000-000000000001",
"name": "Example Insurance",
"slug": "example-insurance"
},
"api_key": {
"id": "00000000-0000-4000-8000-000000000002",
"public_prefix": "ptk_abc123lookupvalue"
}
}

Use this endpoint for startup checks and support diagnostics. Plainterms does not add permissive browser CORS headers for this endpoint.

Returns users in the API key’s organization. The list is ordered by last name, then first name.

{
"users": [
{
"id": "00000000-0000-4000-8000-000000000010",
"email": "agent@example.com",
"name": "Ava Agent",
"first_name": "Ava",
"last_name": "Agent",
"status": "active"
}
]
}

Use email as the user_email value when reserving a document upload. A suspended user can appear in this list, but document upload and confirmation actions reject suspended users.

Reserves a document upload for a same-organization Plainterms user. For new content this is a two-step upload flow: first reserve the upload through Plainterms, then PUT the PDF bytes to the signed storage URL in the response. When the content already exists in the organization, the response can have no upload object (reservation.next_action: "none") and resolve to an existing document or a server-side-cloned sibling — see Handling Duplicate Uploads.

{
"user_email": "agent@example.com",
"filename": "quote.pdf",
"file_size_bytes": 1048576,
"mime_type": "application/pdf",
"content_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"idempotency_key": "optional-retry-key",
"customer_document_id": "crm-quote-123"
}
FieldRequiredRules
user_emailYesSame-organization Plainterms user email. Case-insensitive after trimming.
filenameYesNon-empty string.
file_size_bytesYesPositive integer, maximum 20 MB.
mime_typeYesMust be application/pdf.
content_hashYesSHA-256 hex digest, 64 characters. Plainterms normalizes the value to lowercase before validation.
idempotency_keyNoString. Defaults to <content_hash>-<filename>.
customer_document_idNoString up to 512 characters. Unique within the organization when present. Recommended for production integrations that need customer-side document correlation.

Do not send uploaded_by; the API uses user_email to set Plainterms attribution.

customer_document_id is your stable identifier for the logical customer document or quote. It is optional at the API level, but production integrations should send it when they need CRM, Retool, policy-system, metadata, confirmation, webhook, or duplicate-safe re-upload correlation.

Choose the value before upload automation goes live and store it with the customer record that owns the document. Good sources include:

  • A CRM quote ID, such as crm-quote-123.
  • A policy-system document ID.
  • A deterministic content-derived document key, such as a PDF hash paired with a customer record ID your system owns.

Do not generate a new random value for every button click, browser attempt, or upload retry. When a user retries or re-uploads the same logical PDF, resend the original customer_document_id; rotating it for duplicate content can return 409.

idempotency_key and customer_document_id are related but not interchangeable. idempotency_key is the reservation retry key and defaults to <content_hash>-<filename> when omitted. customer_document_id is the customer-side document identity that later metadata, confirmation, and webhook flows can use for correlation. Send both when you have both.

Status: 202 Accepted

{
"document": {
"id": "00000000-0000-4000-8000-000000000020",
"customer_document_id": "crm-quote-123",
"storage_path": "org-id/document-id.pdf",
"status": "extracting"
},
"user": {
"id": "00000000-0000-4000-8000-000000000010",
"email": "agent@example.com",
"name": "Ava Agent"
},
"reservation": {
"outcome": "created",
"document_state": "waiting_for_upload",
"next_action": "upload",
"reopened_for_reupload": false
},
"upload": {
"method": "PUT",
"url": "https://storage.example/signed-upload-url",
"token": "upload-token",
"headers": {
"content-type": "application/pdf"
}
},
"extraction": {
"status": "started",
"workflowRunId": "run-1",
"clientRequestId": "document:doc-1:now:text",
"uploadReadyHookToken": "document-upload:doc-1:now"
}
}

When upload is present, the caller must upload the PDF bytes with PUT to upload.url and the listed content-type. Plainterms starts extraction and waits for the upload to become available. The extraction object is included when extraction has been started for the reservation.

The reservation object is expected on every 202 response per the platform’s reservation contract. Production captures confirm it on duplicate/reuse responses; its presence on fresh created responses is pending production confirmation in pagerguild/plainterms#2, so parse it tolerantly and fall back to upload presence if it is ever absent. Its next_action field is the dispatch discriminant, with two current values: "upload" (the client must PUT to upload.url) and "none" (the document’s content is already satisfied server-side — the upload object is omitted entirely and document.status reflects the document’s real status, for example "approved"). Dispatch on reservation.next_action, never on the presence of specific outcome strings — see Handling Duplicate Uploads.

  • user_email must match a user profile in the API key’s organization.
  • The matching user becomes the document owner (created_by) for Plainterms dashboard visibility and webhook uploaded_by attribution.
  • Duplicate content resolves to the existing document instead of creating a new one: in-flight duplicates can reuse the existing reservation, and settled duplicates return the existing document with no upload step (see Handling Duplicate Uploads). When the existing document already has a customer_document_id, duplicate or retry uploads should send the same value.
  • customer_document_id is separate from idempotency_key. It is useful for CRM, Retool, or policy-system correlation, and can later be used with /api/v1/documents/custId-{customerDocumentId} and /api/v1/documents/confirm.
  • A requested customer_document_id that already belongs to a different document in the organization returns 409. If this happens during a duplicate or retry upload, look up the existing logical document and resend its original customer_document_id instead of issuing a new one.

When POST /api/v1/documents receives content that already exists in the organization (matched by content_hash), the response is normally still 202 Accepted, with the reservation object describing what happened and what the client should do. The one exception: a duplicate arriving under a fresh customer_document_id in an environment without sibling uploads (the default) returns the 409 identity conflict instead — handle both outcomes on that branch (see the Sibling Documents subsection and Troubleshooting Customer ID Conflicts below). For same-document reuse (the existing document is returned), nothing is re-extracted and no state on the existing document changes, and the response contains no upload object:

{
"document": {
"id": "e03397ce-41de-4247-89a0-000087ebd3ed",
"customer_document_id": null,
"metadata": { "external_document_id": "sample-quote-e2fec121215f9ef272f67dd8ca4d0b45" },
"storage_path": "org-id/e03397ce-41de-4247-89a0-000087ebd3ed.pdf",
"status": "approved",
"reservation_outcome": "duplicate_reuse",
"integration_state": "ready"
},
"reservation": {
"outcome": "duplicate_reuse",
"document_state": "ready",
"next_action": "none",
"reopened_for_reupload": false
},
"user": { "id": "", "email": "agent@example.com", "name": "" }
}

Current reservation.outcome values:

OutcomeMeaning
createdNew content; a new document was created and the bytes must be uploaded.
idempotent_reuseSame idempotency_key as an existing document (a straight retry); resolves to that document.
duplicate_reuseSame content_hash as an existing document originally reserved under a different idempotency_key; resolves to that document.
reopened_for_reuploadAn existing document was reopened for recovery; a fresh upload reservation is issued (next_action: "upload").
duplicate_contentSame organization content arriving under a fresh, unclaimed customer_document_id created a new sibling document (feature-gated; see below).

Treat the outcome field as an open enum. The set has grown twice in one week; clients must never gate behavior on specific outcome strings. Log the value for observability and dispatch on reservation.next_action — a two-value union:

  • next_action: "upload" (the upload object is present) — PUT the bytes as usual. This covers new content (created), recovery (reopened_for_reupload: true), and sibling creations whose content could not be cloned server-side.
  • next_action: "none" (the upload object is omitted) — the document’s content is already satisfied server-side: same-document reuse, or a sibling whose content was cloned. Do not attempt a PUT. Fetch the document’s current state with GET /api/v1/documents/{documentId} (the document.id in the response) and treat the upload as succeeded.
  • No upload object with any other next_action — treat as an error and surface the response for diagnosis rather than fabricating success.

When sibling uploads are enabled for the environment, re-uploading content that already exists in the organization under a new customer_document_id no longer returns the customer-ID 409 — it creates a new sibling document with its own document.id, bound to the new customer ID. The sibling’s content is normally cloned server-side (next_action: "none", no upload step), but when server-side cloning is not possible the response falls back to a normal upload reservation (next_action: "upload" with an upload object) — always honor next_action rather than assuming duplicates never need bytes. A sibling is a real new document: it runs extraction and emits the normal document lifecycle webhooks. Where sibling uploads are not enabled, the fresh-customer-ID case keeps returning the 409 identity conflict described below.

Duplicate-content reuse is separate from customer_document_id identity conflicts, which still return 409 — see Troubleshooting Customer ID Conflicts.

If POST /api/v1/documents returns 409 and the request includes customer_document_id, check whether the same logical document already exists inside the organization with a different customer ID. For same-document retries or duplicate re-uploads, resend the existing document’s original customer_document_id.

This is separate from PDF download or confirmation-state 409 responses. Those responses include polling or confirmation details for the current document state; upload-reservation customer ID conflicts mean the integration is asking Plainterms to attach a new customer identity to an existing logical document.

Use either Plainterms’ document UUID or your own customer_document_id.

GET /api/v1/documents/00000000-0000-4000-8000-000000000020
GET /api/v1/documents/custId-crm-quote-123

For the customer-ID route, the value is the part after custId-. Use URL-safe identifiers when possible, or URL-encode the identifier in that path segment.

Metadata responses are JSON and return 200 OK when the document is found. When the document is still in flight or retrying, use document.polling.retry_after_seconds from the body as the polling hint.

{
"document": {
"id": "00000000-0000-4000-8000-000000000020",
"customer_document_id": "crm-quote-123",
"status": "approved",
"filename": "quote.pdf",
"created_at": "2026-06-27T11:30:00.000Z",
"uploaded_at": "2026-06-27T11:30:00.000Z",
"updated_at": "2026-06-27T11:35:00.000Z",
"uploaded_by": {
"id": "00000000-0000-4000-8000-000000000010",
"email": "agent@example.com"
},
"confirmation": {
"state": "not_required",
"acknowledged_at": null,
"acknowledged_by": null
},
"reason": null,
"extracted_data": {
"carrier": {
"name": "Progressive"
}
},
"value": null,
"download": {
"available": true,
"method": "GET",
"url": "https://plainterms.example/api/v1/documents/00000000-0000-4000-8000-000000000020.pdf",
"content_type": "application/pdf",
"authentication": "api_key"
},
"polling": {
"state": "ready"
}
}
}

The download.url value is a Plainterms API resource. It is not a bearerless signed URL. Fetch it with the same Authorization: Bearer <API key> header.

StateMeaningClient action
in_progressUpload, extraction, or persistence is still running.Poll metadata again after retry_after_seconds.
retryingA retryable processing failure is being retried.Poll again after the longer retry hint.
readyThe rendered PDF can be fetched from download.url.GET the PDF endpoint with the same API key.
confirmation_requiredA threshold acknowledgment is required before the rendered PDF is available.Collect confirmation and call POST /api/v1/documents/confirm.
not_readyThe current document state is not downloadable yet.Inspect reason and continue only if the upstream state changes.
terminalProcessing ended in a terminal no-download state.Treat reason as the terminal explanation.

Downloads the rendered branded PDF for a ready document. This endpoint requires the same API key authentication as the JSON endpoints.

GET /api/v1/documents/00000000-0000-4000-8000-000000000020.pdf
Authorization: Bearer ptk_<lookup>.<secret>

By default the response is an attachment. Add ?disposition=inline when the client wants an inline PDF response.

Status: 200 OK

Content-Type: application/pdf

The response body is the PDF bytes.

When the PDF is still running or retrying, Plainterms returns 202 Accepted with Retry-After and JSON:

{
"error": {
"code": "in_progress",
"message": "Document PDF is not ready yet."
},
"document": {
"id": "00000000-0000-4000-8000-000000000020",
"customer_document_id": "crm-quote-123",
"status": "persisting"
},
"polling": {
"state": "in_progress",
"retry_after_seconds": 10
}
}

For confirmation-gated or otherwise not-ready states, Plainterms returns 409 with the same error, document, and polling shape. For terminal no-download states such as unsupported, rejected, or failed-terminal documents, Plainterms returns 410 Gone. The error.code matches the polling state, such as confirmation_required, not_ready, or terminal. If the document is not found inside the API key’s organization, Plainterms returns 404.

The PDF endpoint accepts the Plainterms document UUID. If the integration only has customer_document_id, call the customer-ID metadata endpoint first and then fetch document.download.url.

Records threshold confirmation for a document. Use this when a downstream system has collected the required human confirmation and needs to notify Plainterms.

Send exactly one document identifier plus confirmed_by_email.

{
"customer_document_id": "crm-quote-123",
"confirmed_by_email": "manager@example.com"
}

or:

{
"document_id": "00000000-0000-4000-8000-000000000020",
"confirmed_by_email": "manager@example.com"
}
FieldRequiredRules
document_idConditionalPlainterms document UUID. Mutually exclusive with customer_document_id.
customer_document_idConditionalCustomer identifier. Mutually exclusive with document_id.
confirmed_by_emailYesSame-organization active Plainterms user email.
{
"document": {
"id": "00000000-0000-4000-8000-000000000020",
"customer_document_id": "crm-quote-123",
"confirmation": {
"state": "received",
"acknowledged_at": "2026-06-25T16:30:00.000Z",
"acknowledged_by": {
"id": "00000000-0000-4000-8000-000000000011",
"email": "manager@example.com"
},
"already_acknowledged": false
}
},
"webhook": {
"status": "started",
"eventType": "confirmation.received",
"runId": "run-confirmation-received"
}
}

If the document was already confirmed, already_acknowledged is true and webhook is null. If the document does not currently require threshold acknowledgment, Plainterms returns 409.