What is a deployment?
A deployment is a permanent URL namespace for interacting with a document or collection. Upload once, deploy once, share the URL.
https://api.okrapdf.com/7km2x9p4ab/completion
The deployment ID is the first path segment — like a Cloudinary cloud name. Everything for that deployment lives under /{id}/....
The object model
Three Durable Objects work together. Each runs on Cloudflare’s edge with its own SQLite database.
| Object | What it owns | Storage |
|---|
| DocumentAgent | Extracted content (pages, tables, entities), chat history, vendor audit trail | SQLite + R2 |
| CollectionAgent | Document membership, cross-document queries | SQLite + D1 |
| DeploymentAgent | Access tokens, guest sessions, redaction policy, frozen manifest | SQLite |
DocumentAgent and CollectionAgent are internal. They are never exposed to end users directly. All user-facing requests go through a DeploymentAgent, which handles auth, access control, and chat persistence.
Create a deployment
curl -X POST https://api.okrapdf.com/v1/deployments \
-H "Authorization: Bearer $OKRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"documentId": "doc-abc123",
"guestAccess": "ask",
"chatPersistence": "persisted"
}'
{
"deploymentId": "7km2x9p4ab",
"completionUrl": "/7km2x9p4ab/completion",
"config": {
"documentId": "doc-abc123",
"guestAccess": "ask",
"chatPersistence": "persisted",
"dataSource": "live"
},
"status": "initialized"
}
Deployment IDs are 10-character base36 strings that always start with a digit. This guarantees they never collide with named routes like /v1/... or /document/....
URL namespace
Every deployment endpoint lives under /{deploymentId}/:
| Endpoint | Method | Description |
|---|
/{id}/completion | POST | Ask a question, get an answer |
/{id}/status | GET | Deployment config and guest count |
/{id}/tokens | POST/GET | Create or list access tokens |
/{id}/tokens/{hint} | DELETE | Revoke a token |
/{id}/events | GET | Replay persisted chat events |
/{id}/document-status | GET | Underlying document processing status |
/{id}/manifest | GET | Frozen document manifest (collections) |
/{id}/pg_1.png | GET | Page 1 image |
/{id}/pg_2.md | GET | Page 2 markdown |
/{id}/pages/1/image.png | GET | Page image (legacy compat) |
Page resources
Clients only need the deployment ID to access page images and markdown. No document ID required.
# Page image (immutable, CDN-cached)
curl https://api.okrapdf.com/7km2x9p4ab/pg_1.png -o page1.png
# Page markdown
curl https://api.okrapdf.com/7km2x9p4ab/pg_3.md
# With shimmer placeholder fallback
curl https://api.okrapdf.com/7km2x9p4ab/d_shimmer/pg_1.png
Images are served from R2 with Cache-Control: public, max-age=31536000, immutable. First request goes through the DO; CDN caches it after that.
Access control
Deployments have three guest access levels:
| Level | Can view status | Can chat | Default |
|---|
none | No | No | Yes |
read | Yes | No | |
ask | Yes | Yes | |
Owner access (via API key) always has full permissions.
Access tokens
Create scoped tokens for guests:
# Create a guest token (5 uses, 1 hour)
curl -X POST https://api.okrapdf.com/7km2x9p4ab/tokens \
-H "Authorization: Bearer $OKRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"role": "ask", "maxUses": 5, "expiresInMs": 3600000}'
{
"token": "abc123...",
"tokenHint": "abc123...",
"role": "ask",
"expiresAt": 1709312400000
}
Guests use the token as a Bearer token:
curl -X POST https://api.okrapdf.com/7km2x9p4ab/completion \
-H "Authorization: Bearer abc123..." \
-H "Content-Type: application/json" \
-d '{"prompt": "What is total revenue?", "guestId": "user-42"}'
Revoke a token and it stops working instantly:
curl -X DELETE https://api.okrapdf.com/7km2x9p4ab/tokens/abc123 \
-H "Authorization: Bearer $OKRA_API_KEY"
Chat persistence
| Mode | Behavior |
|---|
ephemeral | Messages live only during the WebSocket connection |
persisted | Messages stored in SQLite, replayable via /events |
With persisted chat, each guest gets isolated conversation history (scoped by guestId). Replay events with:
curl https://api.okrapdf.com/7km2x9p4ab/events?guestId=user-42 \
-H "Authorization: Bearer $OKRA_API_KEY"
Collection deployments
Deploy over multiple documents by passing a collectionId instead of documentId:
curl -X POST https://api.okrapdf.com/v1/deployments \
-H "Authorization: Bearer $OKRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"collectionId": "col-q4-earnings",
"guestAccess": "ask"
}'
Collection deployments freeze a manifest at creation time. Queries fan out to all documents in the manifest and return aggregated results.
curl -X POST https://api.okrapdf.com/9abc123def/completion \
-H "Authorization: Bearer $OKRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "Compare revenue across all filings"}'
{
"results": [
{ "docId": "doc-nvidia", "answer": "Revenue was $26.9B..." },
{ "docId": "doc-amd", "answer": "Revenue was $5.6B..." }
],
"completed": 2,
"failed": 0,
"totalCost": 0.0042
}
Redaction
Set redactionRole at deployment creation to control PII visibility:
curl -X POST https://api.okrapdf.com/v1/deployments \
-H "Authorization: Bearer $OKRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"documentId": "doc-abc123",
"guestAccess": "ask",
"redactionRole": "viewer"
}'
| Role | Behavior |
|---|
admin | No redaction (default) |
viewer | PII masked per document’s redaction config |
public | Strictest masking |
The deployment decides the role (who’s looking), the document enforces it (what to mask). See Redaction for configuring what gets masked.
Data flow
Next steps