Kyndex
Guides

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

Loading diagram…

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

StepWhoWhat Happens
0. ReserveGrantor (document owner)Obtain a grant ID and commitment nonce from the server
1. CreateGrantor (document owner)Encrypt and submit a grant for a specific recipient
2. DiscoverGrantee (recipient)Query for grants using view tags, attempt decryption
3. ClaimGranteeProvide a cryptographic claim token to mark intent
4. Accept / DenyGrantorReview and approve or reject the claim
5. AccessGranteeDecrypt the document and submit search tokens
6. Revoke / ExpireEither party or automaticEnd 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:

  1. The encrypted payload cannot be replayed to a different grant slot
  2. The server can cryptographically commit to a specific grant context before you encrypt
  3. 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 in POST /grants to create the actual grant.
  • commitment_nonce — 16 random bytes (base64-encoded). Incorporate this into the grant payload's AAD before encrypting.
  • expires_in_seconds — always 60 seconds. 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_nonce

This 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

  1. Look up the recipient's public key — fetch the recipient's encryption key so you can seal the grant for them specifically.
  2. 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.
  3. Set an expiration — all grants must have a time limit. The default is 7 days.
  4. 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-keys

Response:

{
  "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 that SHA-256(mldsa_vk) matches the pending_grantee_dsa_hash recorded at creation.
  • signature — a hybrid signature over kyndex-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:

  1. Decrypt the document — use the key material from the grant to decrypt the document content on your device.
  2. 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

  1. Apex Bank shares its user ID with Alice out-of-band (e.g., via the bank's portal).
  2. Alice fetches Apex Bank's public key using GET /users/{apexBankUserId}/public-keys.
  3. 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
  }'
  1. Apex Bank's system discovers the grant by querying GET /grants?view_tags=0x2A and attempting decryption on the results.
  2. Apex Bank claims the grant with PUT /grants/{id}/claim, providing its cryptographic claim token and proof-of-possession.
  3. Alice approves the claim with PATCH /grants/{id} and { "status": "accepted" }.
  4. Apex Bank decrypts and verifies the passport scan using the key material from the grant.
  5. 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

StatusMeaningWho Can Act
unclaimedGrant created, waiting for a recipient to discover and claimGrantor (poll status, revoke)
pending_acceptanceClaimed by a recipient, waiting for grantor approvalGrantor (accept, deny, revoke)
activeAccepted — recipient has access to the documentGrantor (revoke), Grantee (self-revoke, access)
deniedGrantor rejected the claim
revoked_by_grantorGrantor manually revoked access
revoked_by_granteeGrantee voluntarily gave up access
revoked_by_ttlGrant expired automatically

API Endpoint Summary

EndpointMethodAuthPurpose
/grants/reservationsPOSTBearer tokenReserve a grant ID and commitment nonce
/users/{userId}/public-keysGETNoneFetch recipient's public encryption keys
/grantsPOSTBearer tokenCreate a grant for a document
/grants?view_tags=...GETNoneDiscover grants by view tags
/grants/{id}/claimPUTBearer tokenClaim a discovered grant
/grants/{id}PATCHBearer tokenAccept, deny, or revoke a grant
/grants/{id}GETBearer tokenGet grant details including status
/grants/{id}/claimDELETENoneGrantee self-revoke via claim token
/documents/consumer-indexesPOSTBearer tokenSubmit search tokens for a granted doc

Error Handling

StatusWhenWhat To Do
400Invalid parameters (e.g., wrong key length, bad base64)Check request body against the schema
401Missing or expired access tokenRefresh your token or log in again
403Not the document owner, or proof-of-possession failedVerify you own the document and your cryptographic proof is correct
404Grant not found, wrong token, or permission deniedVerify the grant ID and your authorization token
409Grant in wrong state for this operation (e.g., revoking a denied grant)Check the current status before retrying
429Rate limit exceededBack 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 SeesThe Server Does NOT See
An encrypted grant payload existsWho 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 transitionsThe contents of the shared document
Expiration timestampWhether 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.

  • 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

On this page