Creating And Claiming Grants
Share documents securely and anonymously using time-limited grants that provide consent-based access.
Share documents securely and anonymously using Literal's grant system. This guide walks through the full consumer → institution grant flow — from creating a time-limited grant to claiming, accessing, and revoking it.
Prerequisites: You should be familiar with Literal authentication and document uploads. For the conceptual model behind grants, see Grants & Sharing.
Overview
A grant is an encrypted package that gives a recipient everything needed to access a specific document — but only the intended recipient can open it. The server stores grants but cannot read them, identify recipients, or see which documents are being shared.
Grants are:
- Consent-based — you choose who gets access and must explicitly approve each claim
- Time-limited — every grant has a mandatory expiration (defaults to 7 days)
- Zero-knowledge — the server cannot determine who is sharing with whom
- Revocable — either party can end access at any time
The Grant Lifecycle At A Glance
| Step | Who | What Happens |
|---|---|---|
| 0. Reserve | Grantor (document owner) | Obtain a grant ID and commitment nonce from the server |
| 1. Create | Grantor (document owner) | Encrypt and submit a grant for a specific recipient |
| 2. Discover | Grantee (recipient) | Query for grants using view tags, attempt decryption |
| 3. Claim | Grantee | Provide a cryptographic claim token to mark intent |
| 4. Accept / Deny | Grantor | Review and approve or reject the claim |
| 5. Access | Grantee | Decrypt the document and submit search tokens |
| 6. Revoke / Expire | Either party or automatic | End access and clean up associated data |
Step 0 — Reserve A Grant Slot (Grantor)
Before creating a grant, you must reserve a grant slot with the server. This provides you with a server-authoritative grant_id and a commitment_nonce — a unique, random value that binds the grant to a specific context.
Why Reservation?
The commitment nonce prevents replay attacks and limits the attack surface for pre-computed payloads. By binding the encrypted grant payload to a specific grant ID and nonce via authentication associated data (AAD), you ensure that:
- The encrypted payload cannot be replayed to a different grant slot
- The server can cryptographically commit to a specific grant context before you encrypt
- Any attempt to reuse the payload on a different grant ID fails immediately
Without this binding, an attacker could intercept an encrypted grant payload and re-submit it multiple times on the same document, potentially bypassing recipient constraints.
Reserve A Grant Slot
curl -X POST https://api.kyndex.co/v1/grants/reservations \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"document_id": "doc_abc123"
}'Response (201 Created):
{
"grant_id": "c4acbdde-d5e6-4b72-a8b9-c0d1e2f3a4b5",
"commitment_nonce": "rN7Yc2K5qPxWsT9vL1mN",
"expires_in_seconds": 60
}The reservation verifies that you own the document and returns:
grant_id— the UUID for this grant. Use this inPOST /grantsto create the actual grant.commitment_nonce— 16 random bytes (base64-encoded). Incorporate this into the grant payload's AAD before encrypting.expires_in_seconds— always60seconds. Your grant payload must be encrypted and submitted within this window.
60-Second Window: The reservation expires quickly to limit the opportunity for pre-computed
payload attacks. After calling POST /grants/reservations, you have 60 seconds to encrypt the grant
payload and submit it via POST /grants. If the window closes, reserve again and
generate a fresh encryption with the new nonce.
Incorporate The Nonce Into AAD
When encrypting the grant payload, include the commitment nonce in your authentication associated data (AAD):
GRANT_AAD = "kyndex-grant-aad-v1:" + grant_id + ":" + commitment_nonceThis binds the ciphertext to the specific grant context. When you submit the grant in Step 1, the server will verify that the payload's integrity tag matches this AAD — preventing any attempt to reuse the payload on a different grant.
Step 1 — Create A Grant (Grantor)
When you want to share a document, you create a grant containing everything the recipient needs to decrypt it — but sealed so only that specific recipient can open it.
What Your Client Does
- Look up the recipient's public key — fetch the recipient's encryption key so you can seal the grant for them specifically.
- Generate grant cryptographic material — your client wraps the document's encryption key with the recipient's public key, computes a view tag (a single-byte marker for efficient discovery), and encrypts the package.
- Set an expiration — all grants must have a time limit. The default is 7 days.
- Optionally target the grant — for targeted grants, include hash commitments so only the intended recipient can claim it.
Look Up Recipient Public Keys
Before creating a grant, fetch the recipient's public encryption keys:
curl https://api.kyndex.co/v1/users/{userId}/public-keysResponse:
{
"user_id": "usr_def456uvw",
"mlkem_public_key": "<base64-1568-bytes>",
"x25519_public_key": "<base64-32-bytes>"
}This endpoint is unauthenticated — grant senders look up recipients without needing their own session.
Submit The Grant
curl -X POST https://api.kyndex.co/v1/grants \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"document_id": "<uuid-of-document-being-shared>",
"view_tag": 165,
"ephemeral_pubkey": "<base64-1600-bytes>",
"encrypted_payload": "<base64-encrypted-grant-payload>",
"grantor_token": "<base64-32-bytes>",
"doc_token": "<base64-32-bytes>",
"pending_grantee_ek_hash": "<base64-32-bytes>",
"pending_grantee_dsa_hash": "<base64-32-bytes>",
"expires_at": "2026-04-01T00:00:00.000Z",
"max_claims": 1
}'Response (201 Created):
{
"id": "c4acbdde-d5e6-4b72-a8b9-c0d1e2f3a4b5",
"view_tag": 165,
"status": "unclaimed",
"expires_at": "2026-04-01T00:00:00.000Z",
"one_time_use": false,
"max_claims": 1,
"created_at": "2026-03-01T10:30:00.000Z"
}pending_grantee_ek_hash is always required — it is the SHA-256 of the recipient's ML-KEM encryption key and binds the grant to that specific recipient. pending_grantee_dsa_hash is optional; when included, it creates a targeted grant that requires the recipient to also prove possession of the corresponding signing key during the claim step, forming a dual hash-lock commitment.
The grant is now stored on the server in unclaimed status, waiting for the recipient to discover it.
Zero-Knowledge Property: The server stores the encrypted grant but cannot read the payload,
identify the recipient, or determine which document is being shared. The view_tag is
intentionally ambiguous — roughly 1 in 256 grants will match any given tag.
Step 2 — Discover Grants (Grantee)
The recipient doesn't need to know a grant exists in advance. Instead, the recipient periodically checks for unclaimed grants using view tags — small markers derived from cryptographic keys.
curl 'https://api.kyndex.co/v1/grants?view_tags=0xA5,0xB2,0xFF'Response:
{
"count": 3,
"view_tags_queried": ["0xA5", "0xB2", "0xFF"],
"grants": [
{
"grant_id": "c4acbdde-d5e6-4b72-a8b9-c0d1e2f3a4b5",
"doc_token": "<base64-document-token>",
"view_tag": 165,
"ephemeral_pubkey": "<base64-1600-bytes>",
"encrypted_payload": "<base64-encrypted-payload>"
}
]
}This endpoint requires no authentication — recipients can query anonymously.
The recipient attempts to decrypt each returned grant with their private key. Only the grant actually intended for them will decrypt successfully. Failed decryptions are simply discarded.
Step 3 — Claim The Grant (Grantee)
Once the recipient finds and decrypts their grant, they claim it by providing a unique cryptographic claim token:
curl -X PUT https://api.kyndex.co/v1/grants/{grantId}/claim \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"grant_claim_token": "<base64-32-bytes>"
}'Response:
{
"status": "pending_acceptance"
}The claim token is unique per grant — the server cannot link claims from the same person across different grants.
For targeted grants (those created with pending_grantee_ek_hash), the grantee must also provide proof-of-possession fields:
curl -X PUT https://api.kyndex.co/v1/grants/{grantId}/claim \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"grant_claim_token": "<base64-32-bytes>",
"mldsa_vk": "<base64-1984-bytes>",
"signature": "<base64-3373-bytes>"
}'mldsa_vk— the hybrid ML-DSA-65 + Ed25519 verifying key (1984 bytes, base64). The server checks thatSHA-256(mldsa_vk)matches thepending_grantee_dsa_hashrecorded at creation.signature— a hybrid signature overkyndex-grant-claim-v1 || grant_id || grant_claim_token(3373 bytes, base64). Proves the claimant holds the corresponding private key.
Important: The encrypted payload is withheld until the grantor accepts the claim. The grantee cannot access the document key until Step 4 completes.
Step 4 — Accept Or Deny (Grantor)
The grantor reviews and approves or rejects the claim using a single PATCH endpoint with a status field.
Accept The Claim
curl -X PATCH https://api.kyndex.co/v1/grants/{grantId} \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"status": "accepted",
"grantor_token": "<base64-32-bytes>"
}'Response:
{
"id": "c4acbdde-d5e6-4b72-a8b9-c0d1e2f3a4b5",
"status": "active"
}The grant is now active. The grantee can decrypt the document and submit search tokens so the document appears in their personal search results.
Deny The Claim
curl -X PATCH https://api.kyndex.co/v1/grants/{grantId} \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"status": "denied",
"grantor_token": "<base64-32-bytes>"
}'Response:
{
"id": "c4acbdde-d5e6-4b72-a8b9-c0d1e2f3a4b5",
"status": "denied"
}Check Grant Status
The grantor can poll the current status of a grant at any time:
curl 'https://api.kyndex.co/v1/grants/{grantId}?grantor_token=<base64-32-bytes>' \
-H 'Authorization: Bearer <access_token>'Response:
{
"status": "active"
}Possible status values: unclaimed, pending_acceptance, active, denied, revoked_by_grantor, revoked_by_grantee, revoked_by_ttl.
Step 5 — Access The Document (Grantee)
With an active grant, the recipient can:
- Decrypt the document — use the key material from the grant to decrypt the document content on your device.
- Submit consumer search tokens — so the document appears in your personal search results. These tokens are scoped to your specific grant, enabling precise cleanup when access ends.
To register search tokens for a granted document:
curl -X POST https://api.kyndex.co/v1/documents/consumer-indexes \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"doc_token": "<base64-document-token>",
"tokens": [
{ "token": "<base64-hmac>", "index_type": "doc_field" },
{ "token": "<base64-hmac>", "index_type": "doc_date" }
]
}'This requires an active session — unlike grant discovery, submitting consumer indexes is authenticated.
Step 6 — Revoke Access
Access can end in three ways.
Grantor Revoke
The document owner can revoke a grant at any time:
curl -X PATCH https://api.kyndex.co/v1/grants/{grantId} \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"status": "revoked",
"grantor_token": "<base64-32-bytes>"
}'Response:
{
"id": "c4acbdde-d5e6-4b72-a8b9-c0d1e2f3a4b5",
"status": "revoked_by_grantor"
}Grantee Self-Revoke
The recipient can voluntarily give up access. This requires no login session — only the claim token:
curl -X DELETE https://api.kyndex.co/v1/grants/{grantId}/claim \
-H 'Content-Type: application/json' \
-d '{
"grant_claim_token": "<base64-32-bytes>"
}'Response: 204 No Content
Automatic Expiry
When the grant's time limit is reached, the system automatically revokes it and cleans up all associated search tokens. The grant status transitions to revoked_by_ttl.
Atomic Revocation: In all three cases, the grant is marked as revoked before any cleanup begins. This guarantees that access is immediately terminated, even if cleanup takes additional time.
Case Study — Time-Limited Document Sharing
Here's a complete scenario showing a consumer sharing a verified document with an institution for 48 hours.
Scenario
Alice (consumer) needs to share a verified passport scan with Apex Bank (institution) for a compliance check. Alice wants to grant access for exactly 48 hours.
Flow
- Apex Bank shares its user ID with Alice out-of-band (e.g., via the bank's portal).
- Alice fetches Apex Bank's public key using
GET /users/{apexBankUserId}/public-keys. - Alice creates a targeted grant with a 48-hour expiration:
curl -X POST https://api.kyndex.co/v1/grants \
-H 'Authorization: Bearer <alice_token>' \
-H 'Content-Type: application/json' \
-d '{
"document_id": "<passport-document-uuid>",
"view_tag": 42,
"ephemeral_pubkey": "<base64>",
"encrypted_payload": "<base64>",
"grantor_token": "<base64>",
"doc_token": "<base64>",
"pending_grantee_ek_hash": "<base64>",
"pending_grantee_dsa_hash": "<base64>",
"expires_at": "2026-03-12T10:30:00.000Z",
"max_claims": 1
}'- Apex Bank's system discovers the grant by querying
GET /grants?view_tags=0x2Aand attempting decryption on the results. - Apex Bank claims the grant with
PUT /grants/{id}/claim, providing its cryptographic claim token and proof-of-possession. - Alice approves the claim with
PATCH /grants/{id}and{ "status": "accepted" }. - Apex Bank decrypts and verifies the passport scan using the key material from the grant.
- After 48 hours, the grant expires automatically — Apex Bank loses access and all associated search tokens are cleaned up. Alice can also revoke early if needed.
Both Parties Stay In Control
- Alice chose exactly which document to share, with whom, and for how long. She can revoke access at any point before the 48-hour window expires.
- Apex Bank can verify the passport scan directly by decrypting the original — without relying on Literal to vouch for authenticity.
- The server facilitated the exchange but never saw the passport content, who was sharing with whom, or the relationship between the parties.
Grant Status Reference
| Status | Meaning | Who Can Act |
|---|---|---|
unclaimed | Grant created, waiting for a recipient to discover and claim | Grantor (poll status, revoke) |
pending_acceptance | Claimed by a recipient, waiting for grantor approval | Grantor (accept, deny, revoke) |
active | Accepted — recipient has access to the document | Grantor (revoke), Grantee (self-revoke, access) |
denied | Grantor rejected the claim | — |
revoked_by_grantor | Grantor manually revoked access | — |
revoked_by_grantee | Grantee voluntarily gave up access | — |
revoked_by_ttl | Grant expired automatically | — |
API Endpoint Summary
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/grants/reservations | POST | Bearer token | Reserve a grant ID and commitment nonce |
/users/{userId}/public-keys | GET | None | Fetch recipient's public encryption keys |
/grants | POST | Bearer token | Create a grant for a document |
/grants?view_tags=... | GET | None | Discover grants by view tags |
/grants/{id}/claim | PUT | Bearer token | Claim a discovered grant |
/grants/{id} | PATCH | Bearer token | Accept, deny, or revoke a grant |
/grants/{id} | GET | Bearer token | Get grant details including status |
/grants/{id}/claim | DELETE | None | Grantee self-revoke via claim token |
/documents/consumer-indexes | POST | Bearer token | Submit search tokens for a granted doc |
Error Handling
| Status | When | What To Do |
|---|---|---|
400 | Invalid parameters (e.g., wrong key length, bad base64) | Check request body against the schema |
401 | Missing or expired access token | Refresh your token or log in again |
403 | Not the document owner, or proof-of-possession failed | Verify you own the document and your cryptographic proof is correct |
404 | Grant not found, wrong token, or permission denied | Verify the grant ID and your authorization token |
409 | Grant in wrong state for this operation (e.g., revoking a denied grant) | Check the current status before retrying |
429 | Rate limit exceeded | Back off and retry after the Retry-After period |
All errors follow the RFC 9457 Problem Details format.
What The Server Sees (And Doesn't)
| The Server Sees | The Server Does NOT See |
|---|---|
| An encrypted grant payload exists | Who the grant is for |
| A 1-byte view tag (ambiguous) | Which document is being shared |
| Opaque claim tokens (one-way) | The relationship between grantor and grantee |
| Grant status transitions | The contents of the shared document |
| Expiration timestamp | Whether two grants involve the same person |
Think Of It Like A Secure Drop Box: You place a sealed envelope in a numbered box. The recipient checks boxes matching their number, finds the one they can open, and claims it. The postal service sees boxes and envelopes but never reads the letters — and can't tell who's writing to whom.
Related Resources
- Grants & Sharing — the conceptual model behind grants, view tags, and the zero-knowledge sharing lifecycle
- Getting Started — authentication, first document upload, and API basics
- Core Concepts — zero-knowledge model, document lifecycle, key hierarchy, encrypted search, and more