Documents API reference
Updated
13 operations of the Fax for Agents API, from the OpenAPI document version 1.20.0, snapshot of 2026-11-13. The base URL is https://api.faxforagents.com/v1. This is one group of the full reference.
- A document is immutable once created, and its sha256 identifies exactly what will be sent.
- Completing an upload checks the size and, when one is sent, the sha256 of the bytes.
- The bytes of an upload go to a signed upload URL with no API key.
Operations
GET /v1/documentsList documents, newest firstPOST /v1/documentsCreate an immutable document from an upload or from textDELETE /v1/documents/{document_id}Delete a document (its bytes and previews)GET /v1/documents/{document_id}Read a document's hash, pages and validation stateGET /v1/documents/{document_id}/contentThe PDF bytes behind a signed URL (no API key)GET /v1/documents/{document_id}/downloadA short-lived signed download URLGET /v1/documents/{document_id}/pagesRendered pages with signed preview URLsGET /v1/documents/{document_id}/pages/{page_number}/contentA page PNG behind a signed URL (no API key)GET /v1/documents/{document_id}/validationValidation warningsPOST /v1/uploadsCreate a bounded upload sessionDELETE /v1/uploads/{upload_id}Cancel an incomplete uploadPOST /v1/uploads/{upload_id}/completeFinalize an upload (size and hash checked)PUT /v1/uploads/{upload_id}/contentSend the bytes to a signed upload URL (no API key)
What is a document in this API?
The exact thing a fax will carry. POST /v1/documents creates it from an upload or from text, and the summary of the operation calls it immutable. The answer has a sha256, a page_count, the source and a validation object. Nothing changes those afterwards. A quote and an approval both name the hash, so a person who approves a send approves these bytes.
How does an upload session work?
Four calls, and only three of them take your key.
POST /v1/uploads opens a bounded session. The body declares the filename, the size_bytes and the content_type, which is application/pdf, image/png, image/jpeg or image/tiff. The answer has the id and a signed upload_url with its expiry.
PUT /v1/uploads/{upload_id}/content is that signed URL. It takes the bytes and no API key. The signature in the address is the credential.
POST /v1/uploads/{upload_id}/complete closes the session. The size is checked against what was declared. The body can carry a sha256, and the call fails when the bytes differ. Send an empty JSON object when you have no hash.
POST /v1/documents with the upload_id then makes the document. DELETE /v1/uploads/{upload_id} cancels a session that was not completed.
Can a document be made without a file?
Yes. The same create call takes text, markdown or html in place of upload_id, and exactly one of the four. Text is rendered to a letter PDF. Markdown renders headings, lists, emphasis and code. HTML is rendered by a headless browser with scripts off and no network, so inline CSS and data: images print and an external resource does not. A filename is optional.
The send guide has the fonts, the page size and what happens to an image.
How do I check what was made?
GET /v1/documents/{document_id} returns the hash, the pages and the validation state. GET /v1/documents/{document_id}/validation returns the warnings alone, such as a blocked resource in HTML or an image that was rotated. GET /v1/documents/{document_id}/pages returns the rendered pages with signed preview URLs, so a person can look at each page before a send.
How are the bytes downloaded?
Never through a public address. GET /v1/documents/{document_id}/download answers with a short-lived signed URL. The content routes behind such URLs, for the PDF and for one page as PNG, take exp and sig in the query and no key. When the link has expired, ask for a new one.
What does deleting do?
DELETE /v1/documents/{document_id} removes the bytes and the previews. A fax that already used the document keeps its record of the hash and the page count. GET /v1/documents lists documents newest first with cursor and limit.
Which scopes and limits apply?
Reads need document:read. Creating, deleting and the upload session need document:write. The signed routes need neither.
A file or a body over the size limit answers 413. A rendering service that is down answers 503 on the create call, and that call can be repeated. The page and size limits with their values are on the limits page.
Each operation in detail
GET /v1/documents
List documents, newest first
Credential: an API key or a signed-in session. Scope: document:read.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
cursor | query | string or null | no | |
limit | query | integer | no | Default 50. |
Responses
| Status | Meaning |
|---|---|
200 | Successful Response |
401 | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
403 | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
422 | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
429 | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |
Response fields
| Field | Type | Required | Description |
|---|---|---|---|
items | array of object Document | yes | |
next_cursor | string or null | no |
Example
curl https://api.faxforagents.com/v1/documents \
-H "Authorization: Bearer ffa_test_..."
POST /v1/documents
Create an immutable document from an upload or from text
Credential: an API key or a signed-in session. Scope: document:write.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
Idempotency-Key | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |
Request body application/json, required
Send exactly one of text, markdown, html or upload_id.
| Field | Type | Required | Description |
|---|---|---|---|
filename | string or null | no | |
html | string or null | no | HTML rendered to PDF by a headless browser with scripts off and no network: inline CSS and data: images print, external resources are counted as blocked. US letter unless @page says otherwise. At most 2 MiB. |
markdown | string or null | no | Markdown (headings, lists, emphasis, code) rendered to PDF. |
text | string or null | no | Plain text rendered to a letter PDF. |
upload_id | string or null | no |
Responses
| Status | Meaning |
|---|---|
201 | Successful Response |
401 | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
403 | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
404 | not_found: no such resource for this credential. A resource of another organization answers the same. |
409 | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
413 | The body is larger than the limit of this call: payload_too_large. |
422 | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
429 | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |
503 | Something this call depends on is not available or not set up here, for example billing_unavailable or provider_unavailable. error.retryable says whether to send the same call again. |
Response fields
| Field | Type | Required | Description |
|---|---|---|---|
content_type | string | yes | |
created_at | string | yes | |
deleted_at | string or null | yes | |
environment | string (live, test) | yes | |
expires_at | string or null | yes | |
filename | string | yes | |
id | string | yes | |
page_count | integer | yes | |
render_reason | string or null | yes | |
render_status | string | yes | |
sha256 | string | yes | |
size_bytes | integer | yes | |
source | string | yes | |
status | string | yes | |
upload_id | string or null | yes | |
validation | object | yes |
Example
curl https://api.faxforagents.com/v1/documents \
-X POST \
-H "Authorization: Bearer ffa_test_..." \
-H "Content-Type: application/json" \
-d '{"text":"Hello from Fax for Agents"}'
DELETE /v1/documents/{document_id}
Delete a document (its bytes and previews)
Credential: an API key or a signed-in session. Scope: document:write.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
document_id | path | string | yes | |
Idempotency-Key | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |
Responses
| Status | Meaning |
|---|---|
200 | Successful Response |
401 | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
403 | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
404 | not_found: no such resource for this credential. A resource of another organization answers the same. |
409 | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
422 | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
429 | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |
Response fields
| Field | Type | Required | Description |
|---|---|---|---|
deleted | boolean | no | |
id | string | yes |
Example
curl https://api.faxforagents.com/v1/documents/DOCUMENT_ID \
-X DELETE \
-H "Authorization: Bearer ffa_test_..."
GET /v1/documents/{document_id}
Read a document's hash, pages and validation state
Credential: an API key or a signed-in session. Scope: document:read.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
document_id | path | string | yes |
Responses
| Status | Meaning |
|---|---|
200 | Successful Response |
401 | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
403 | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
404 | not_found: no such resource for this credential. A resource of another organization answers the same. |
422 | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
429 | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |
Response fields
| Field | Type | Required | Description |
|---|---|---|---|
content_type | string | yes | |
created_at | string | yes | |
deleted_at | string or null | yes | |
environment | string (live, test) | yes | |
expires_at | string or null | yes | |
filename | string | yes | |
id | string | yes | |
page_count | integer | yes | |
render_reason | string or null | yes | |
render_status | string | yes | |
sha256 | string | yes | |
size_bytes | integer | yes | |
source | string | yes | |
status | string | yes | |
upload_id | string or null | yes | |
validation | object | yes |
Example
curl https://api.faxforagents.com/v1/documents/DOCUMENT_ID \
-H "Authorization: Bearer ffa_test_..."
GET /v1/documents/{document_id}/content
The PDF bytes behind a signed URL (no API key)
Credential: the signed URL as it was given.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
document_id | path | string | yes | |
exp | query | string or null | no | |
sig | query | string or null | no |
Responses
| Status | Meaning |
|---|---|
200 | Successful Response |
404 | not_found: no such resource for this credential. A resource of another organization answers the same. |
422 | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
Example
curl https://api.faxforagents.com/v1/documents/DOCUMENT_ID/content
GET /v1/documents/{document_id}/download
A short-lived signed download URL
Credential: an API key or a signed-in session. Scope: document:read.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
document_id | path | string | yes |
Responses
| Status | Meaning |
|---|---|
200 | Successful Response |
401 | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
403 | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
404 | not_found: no such resource for this credential. A resource of another organization answers the same. |
422 | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
429 | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |
Response fields
| Field | Type | Required | Description |
|---|---|---|---|
content_type | string | yes | |
expires_at | string | yes | |
url | string | yes |
Example
curl https://api.faxforagents.com/v1/documents/DOCUMENT_ID/download \
-H "Authorization: Bearer ffa_test_..."
GET /v1/documents/{document_id}/pages
Rendered pages with signed preview URLs
Credential: an API key or a signed-in session. Scope: document:read.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
document_id | path | string | yes |
Responses
| Status | Meaning |
|---|---|
200 | Successful Response |
401 | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
403 | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
404 | not_found: no such resource for this credential. A resource of another organization answers the same. |
422 | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
429 | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |
Response fields
| Field | Type | Required | Description |
|---|---|---|---|
document_id | string | yes | |
items | array of object PagePreview | yes | |
page_count | integer | yes | |
render_reason | string or null | yes | |
render_status | string | yes |
Example
curl https://api.faxforagents.com/v1/documents/DOCUMENT_ID/pages \
-H "Authorization: Bearer ffa_test_..."
GET /v1/documents/{document_id}/pages/{page_number}/content
A page PNG behind a signed URL (no API key)
Credential: the signed URL as it was given.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
document_id | path | string | yes | |
page_number | path | integer | yes | |
exp | query | string or null | no | |
sig | query | string or null | no |
Responses
| Status | Meaning |
|---|---|
200 | Successful Response |
404 | not_found: no such resource for this credential. A resource of another organization answers the same. |
422 | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
Example
curl https://api.faxforagents.com/v1/documents/DOCUMENT_ID/pages/PAGE_NUMBER/content
GET /v1/documents/{document_id}/validation
Validation warnings
Credential: an API key or a signed-in session. Scope: document:read.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
document_id | path | string | yes |
Responses
| Status | Meaning |
|---|---|
200 | Successful Response |
401 | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
403 | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
404 | not_found: no such resource for this credential. A resource of another organization answers the same. |
422 | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
429 | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |
Response fields
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | |
page_count | integer | yes | |
render_reason | string or null | yes | |
render_status | string | yes | |
status | string | yes | |
warnings | array of object | yes |
Example
curl https://api.faxforagents.com/v1/documents/DOCUMENT_ID/validation \
-H "Authorization: Bearer ffa_test_..."
POST /v1/uploads
Create a bounded upload session
Credential: an API key or a signed-in session. Scope: document:write.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
Idempotency-Key | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |
Request body application/json, required
| Field | Type | Required | Description |
|---|---|---|---|
content_type | string | no | application/pdf, image/png, image/jpeg or image/tiff. An image becomes 8-bit gray letter pages, since fax is monochrome. A frame wider than it is tall is rotated. A frame above 300 dpi letter is downsampled. Every TIFF frame is a page. PNG and JPEG take the first frame. The bytes must be the declared type. |
filename | string | yes | |
size_bytes | integer | yes |
Responses
| Status | Meaning |
|---|---|
201 | Successful Response |
401 | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
403 | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
409 | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
413 | The body is larger than the limit of this call: payload_too_large. |
422 | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
429 | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |
Response fields
| Field | Type | Required | Description |
|---|---|---|---|
completed_at | string or null | yes | |
content_type | string | yes | |
created_at | string | yes | |
declared_size | integer | yes | |
expires_at | string | yes | |
filename | string | yes | |
id | string | yes | |
received_size | integer | yes | |
sha256 | string or null | yes | |
status | string | yes | |
upload_method | string or null | no | |
upload_url | string or null | no | |
upload_url_expires_at | string or null | no |
Example
curl https://api.faxforagents.com/v1/uploads \
-X POST \
-H "Authorization: Bearer ffa_test_..." \
-H "Content-Type: application/json" \
-d '{"filename":"contract.pdf","size_bytes":1}'
DELETE /v1/uploads/{upload_id}
Cancel an incomplete upload
Credential: an API key or a signed-in session. Scope: document:write.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
upload_id | path | string | yes | |
Idempotency-Key | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |
Responses
| Status | Meaning |
|---|---|
200 | Successful Response |
401 | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
403 | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
404 | not_found: no such resource for this credential. A resource of another organization answers the same. |
409 | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
422 | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
429 | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |
Response fields
| Field | Type | Required | Description |
|---|---|---|---|
deleted | boolean | no | |
id | string | yes |
Example
curl https://api.faxforagents.com/v1/uploads/UPLOAD_ID \
-X DELETE \
-H "Authorization: Bearer ffa_test_..."
POST /v1/uploads/{upload_id}/complete
Finalize an upload (size and hash checked)
Credential: an API key or a signed-in session. Scope: document:write.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
upload_id | path | string | yes | |
Idempotency-Key | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |
Request body application/json
| Field | Type | Required | Description |
|---|---|---|---|
sha256 | string or null | no | Optional: fail if the bytes differ. |
Responses
| Status | Meaning |
|---|---|
200 | Successful Response |
401 | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
403 | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
404 | not_found: no such resource for this credential. A resource of another organization answers the same. |
409 | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
422 | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
429 | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |
Response fields
| Field | Type | Required | Description |
|---|---|---|---|
completed_at | string or null | yes | |
content_type | string | yes | |
created_at | string | yes | |
declared_size | integer | yes | |
expires_at | string | yes | |
filename | string | yes | |
id | string | yes | |
received_size | integer | yes | |
sha256 | string or null | yes | |
status | string | yes | |
upload_method | string or null | no | |
upload_url | string or null | no | |
upload_url_expires_at | string or null | no |
Example
curl https://api.faxforagents.com/v1/uploads/UPLOAD_ID/complete \
-X POST \
-H "Authorization: Bearer ffa_test_..."
PUT /v1/uploads/{upload_id}/content
Send the bytes to a signed upload URL (no API key)
Credential: the signed URL as it was given.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
upload_id | path | string | yes | |
exp | query | string or null | no | |
sig | query | string or null | no | |
Idempotency-Key | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |
Responses
| Status | Meaning |
|---|---|
200 | Successful Response |
404 | not_found: no such resource for this credential. A resource of another organization answers the same. |
409 | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
413 | The body is larger than the limit of this call: payload_too_large. |
422 | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
429 | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |
Response fields
| Field | Type | Required | Description |
|---|---|---|---|
completed_at | string or null | yes | |
content_type | string | yes | |
created_at | string | yes | |
declared_size | integer | yes | |
expires_at | string | yes | |
filename | string | yes | |
id | string | yes | |
received_size | integer | yes | |
sha256 | string or null | yes | |
status | string | yes | |
upload_method | string or null | no | |
upload_url | string or null | no | |
upload_url_expires_at | string or null | no |
Example
curl https://api.faxforagents.com/v1/uploads/UPLOAD_ID/content \
-X PUT