Document Upload With Client-Side Encryption
Walk through uploading an encrypted document from authentication through enclave processing to search verification.
Zero-Knowledge Guarantee: Every document you upload is encrypted on your device before it reaches the server. The server stores ciphertext, blind index tokens, and operational metadata (timestamps, processing status, size category) — it can never read your document content, metadata, or search queries. For more on this principle, see Zero-Knowledge Model.
Prerequisites
Before you begin, make sure you have:
- A registered account with an active access token (see Getting Started for authentication setup)
- A cryptography library capable of:
- Symmetric authenticated encryption
- Key wrapping
- HMAC-based token generation
- The platform public key (fetched in Step 1 below)
Overview
| Step | Where | What Happens |
|---|---|---|
| 1. Get platform public key | Server → Client | Fetch the platform's public key for wrapping your document encryption key |
| 2. Reserve a document slot | Client → Server | Obtain a server-authoritative document ID and commitment nonce |
| 3. Encrypt your document | Your device | Generate a key, encrypt with the commitment nonce in the AAD, wrap the key, compute search tokens |
| 4. Create the document record | Client → Server | Send encrypted metadata, wrapped keys, and search tokens |
| 5. Upload encrypted content | Client → Server | Send the raw encrypted bytes |
| 6. Confirm processing | Client → Server | Poll until the enclave finishes verification and seal generation |
| 7. Verify searchability | Client → Server | Compute a search token locally, query the server, decrypt results |
Server-side processing (enclave verification, seal generation, and entity index generation) happens automatically after step 5.
Step 1 — Get The Platform Public Key
Before encrypting, fetch the platform's public key. You will use this key to wrap your document encryption key so the secure enclave can process your document later.
curl https://api.kyndex.co/v1/public-keys/serverResponse:
{
"key_id": "pmk-2026-01",
"key_spec": "ECC_NIST_P256",
"ecc_public_key": "<base64-public-key>",
"algorithm": "hybrid",
"description": "Platform Master Key for DEK wrapping (classical ECC portion)"
}This endpoint is unauthenticated. The key changes rarely — cache it locally and refresh only when you receive a key rotation signal.
Step 2 — Reserve A Document Slot
Reserving a slot gives you a server-authoritative document ID and a commitment nonce that binds your encryption to this specific upload. The reservation expires in 5 minutes.
You must reserve before encrypting. The commitment_nonce from this step must be incorporated
into the encryption AAD in Step 3. This binds the ciphertext to the specific document slot and
prevents reuse.
curl -X POST https://api.kyndex.co/v1/documents/reservations \
-H 'Authorization: Bearer <access_token>'Response (201 Created):
{
"document_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"commitment_nonce": "<base64-16-bytes>",
"expires_in_seconds": 300
}Use document_id as the reservation_id in Step 4. Incorporate the commitment_nonce into your encryption AAD in Step 3.
Why Two-Phase Upload?
- The document ID is assigned by the server (compliance requirement)
- The commitment nonce provides temporal binding — it proves that encryption happened after the reservation was created
- The AAD binding prevents the ciphertext from being used with a different document
Think of the commitment nonce like a timestamp on a notarised document — it proves the encryption happened after the reservation was issued, preventing a pre-encrypted file from being swapped in.
Step 3 — Encrypt Your Document
All encryption happens on your device. The server never sees plaintext content, metadata, or keys. Your device needs to perform five operations:
Generate A Document Encryption Key
Create a fresh, random symmetric encryption key for this document. Every document gets its own unique key — no two documents share a key.
Encrypt The Document Content
Encrypt the raw document bytes using your new key with authenticated encryption. Include the commitment_nonce from Step 2 and the key-wrapping timestamp (wrap_ts) in the authentication context (AAD) to bind the ciphertext to this specific upload and prevent key replay.
Wrap The Key
Wrap your document encryption key twice:
- With your personal master key — so you can decrypt the document later
- With the platform public key (from Step 1) — so the secure enclave can process it
This dual wrapping ensures that both you and the enclave can access the key, but the server itself never can. Think of it as making two copies of a house key — one for yourself, one for the locksmith who needs to inspect the house. The server carries both copies but can't use either.
Compute Blind Index Tokens
Generate one-way cryptographic tokens for each searchable value (document type, text content, dates, tags). These tokens enable you to search your encrypted documents later without the server learning your search terms.
Each token is computed using your personal search key and a normalization step that ensures consistent matching (case-insensitive, whitespace-normalized). For more on how this works, see Encrypted Search.
Encrypt Document Metadata
Encrypt the document's metadata (title, filename, type, tags) separately using the same document key. The server stores only the encrypted metadata blob.
Step 4 — Create The Document Record
Send the encrypted metadata, wrapped keys, identity tokens, and search tokens to the server.
curl -X POST https://api.kyndex.co/v1/documents \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"reservation_id": "<document_id-from-step-2>",
"metadata_encrypted": "<base64-encrypted-metadata>",
"wrapped_dek_umk": "<base64-key-wrapped-with-your-master-key>",
"wrapped_dek_pmk": "<base64-key-wrapped-with-platform-key>",
"owner_token": "<base64-32-bytes>",
"doc_token": "<base64-32-bytes>",
"wrap_ts": 1710288000,
"consumer_tokens": [
{ "token": "<base64-blind-index>", "index_type": "doc_type" },
{ "token": "<base64-blind-index>", "index_type": "text_content" }
],
"size_bucket": 2
}'Request Fields
| Field | Required | Description |
|---|---|---|
reservation_id | Yes | The document_id returned from the reservation step |
metadata_encrypted | Yes | Base64-encoded encrypted document metadata |
wrapped_dek_umk | Yes | Document key wrapped with your personal master key (base64) |
wrapped_dek_pmk | Yes | Document key wrapped with the platform public key (base64) |
owner_token | Yes | Cryptographic owner identity token (base64, 32 bytes) |
doc_token | Yes | Cryptographic document identity token (base64, 32 bytes) |
wrap_ts | Yes | Unix epoch seconds when DEK was wrapped |
consumer_tokens | No | Array of blind index tokens for searchable encryption |
size_bucket | No | Document size category (1–5) |
Response (201 Created):
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "pending_upload",
"created_at": "2026-01-15T10:30:00.000Z",
"upload_instructions": "Upload encrypted blob to /v1/documents/{id}/content using PUT"
}The document is now in pending_upload status — the metadata and keys are stored, and the server is waiting for the encrypted content.
Step 5 — Upload Encrypted Content
Send the raw encrypted document bytes as a binary body. The Content-Length header is required.
curl -X PUT https://api.kyndex.co/v1/documents/<document-id>/content \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/octet-stream' \
-H 'Content-Length: <byte-count>' \
--data-binary @encrypted-document.binResponse (201 Created):
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "pending_ocr",
"message": "Document uploaded successfully, processing queued"
}The document transitions to pending_ocr and server-side processing begins automatically.
The maximum upload size is 100 MB. There is a secondary 10 MB threshold that affects how content is returned on download — files under 10 MB are returned as a direct binary stream, while larger files are served as a streaming response. The 10 MB threshold has no effect on upload.
What Happens Next: The secure enclave unwraps the document key using the platform's private key, temporarily decrypts the document for verification and seal generation, then discards all plaintext. The encrypted blob remains the only stored copy. No server operator can observe or interfere with this process.
Step 6 — Confirm Enclave Processing
After upload, the document moves through several processing states automatically. Poll the document status to track progress:
curl https://api.kyndex.co/v1/documents/<document-id> \
-H 'Authorization: Bearer <access_token>'Document Processing States
| State | Description |
|---|---|
pending_upload | Record created, waiting for encrypted content |
pending_ocr | Content uploaded, queued for enclave processing |
processing | Secure enclave is actively processing the document |
processed | Enclave processing complete — verification seal generated |
failed | Processing failed — the document must be re-uploaded |
Once status is processed, enclave processing is complete. Consumer blind index tokens submitted at creation are immediately available for personal search. Documents processed in an organization context also receive entity-scoped indexes generated by the enclave during this step.
For a deeper look at what happens at each stage, see Document Lifecycle.
Step 7 — Verify Searchability
Once your document reaches processed status, verify that it is searchable by performing a search using blind index tokens.
Compute a search token on your device (using the same search key and normalization process you used during upload), then send it to the search endpoint:
curl -X POST https://api.kyndex.co/v1/search \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"token": "<base64-search-token-32-bytes>",
"scope": "consumer",
"index_types": ["doc_type", "text_content"]
}'Response:
{
"documents": [
{
"doc_token": "<base64-document-reference>",
"metadata_encrypted": "<base64-encrypted-metadata>",
"wrapped_dek_umk": "<base64-key-wrapped-with-your-master-key>"
}
],
"count": 1
}The server returns encrypted results — decrypt them on your device using your personal master key. The server never learns what you searched for or what the results contain.
Search Scopes
consumer— search your personal documents. Results include your personally-wrapped key.entity— search within an organization. Requiresentity_token. Results include an organization-scoped wrapped key.
For the full explanation of how encrypted search works, see Encrypted Search.
Error Handling
All error responses follow the RFC 9457 Problem Details format with Content-Type: application/problem+json:
{
"type": "https://api.kyndex.co/errors/INVALID_REQUEST",
"title": "Bad Request",
"status": 400,
"detail": "The request body failed validation",
"instance": "/v1/documents"
}Common Errors During Upload
| Status | Error | What To Do |
|---|---|---|
| 400 | Invalid or expired reservation | Reservations expire after 5 minutes. Create a new one and re-encrypt with the new commitment nonce. |
| 401 | Unauthorized | Your access token has expired. Refresh it and retry. |
| 413 | Payload too large | The encrypted document exceeds the 100 MB limit. Reduce the document size before encrypting. |
| 429 | Rate limited | Too many requests. Wait and retry with exponential backoff. |
Related Resources
Now that you've uploaded your first document, explore these related guides and concepts:
- Zero-Knowledge Model — understand why the server can never access your data
- Document Lifecycle — follow a document through every stage from upload to deletion
- Encrypted Search — learn how search works without revealing your queries
- Getting Started — authentication setup and platform overview