Entity Deliveries
Push an encrypted document key directly to an entity member — admin initiates, recipient accepts or denies.
Entity admins can deliver a document encryption key directly to a specific member without a grant. The recipient decides whether to accept. The server stores the encrypted payload but cannot read the key, identify the parties, or determine which document is involved.
Prerequisites
Before starting, make sure you have:
- An active session with admin role in the target entity (see Entity Onboarding)
- The target member's delivery public keys — fetched from
GET /v1/entities/{id}/memberships - The document encryption key (DEK) you want to deliver
Overview
| Step | Who | What Happens |
|---|---|---|
| 0. Reserve | Admin | Obtain a delivery ID and commitment nonce from the server |
| 1. Create | Admin | Encrypt the DEK for the recipient and submit the delivery |
| 2. Discover | Recipient | Query for pending deliveries addressed to your encryption key |
| 3. Accept / Deny | Recipient | Decrypt, re-wrap the key, and accept — or deny without decrypting |
| 4. List Received | Recipient | Retrieve all accepted deliveries with their wrapped keys |
Step 0 — Reserve A Delivery Slot (Admin)
Before encrypting, reserve a delivery slot with the server. This gives you a server-authoritative delivery_id and a commitment_nonce — a 16-byte random value the server generates to bind the encrypted payload to this specific slot.
Why Reservation?
Including the commitment nonce in the AEAD authentication data means the ciphertext is cryptographically bound to this delivery slot. Any attempt to replay the encrypted payload on a different delivery ID fails immediately — the integrity tag will not verify.
Reserve A Slot
curl -X POST https://api.kyndex.co/v1/issuances/reservations \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"entity_token": "<base64-32-bytes>",
"doc_token": "<base64-32-bytes>"
}'Response (201 Created):
{
"delivery_id": "11111111-2222-3333-4444-555555555555",
"commitment_nonce": "<base64-16-bytes>"
}| Field | Description |
|---|---|
entity_token | One-way HMAC token identifying the target entity (32 bytes, base64) |
doc_token | One-way HMAC token identifying the document being delivered (32 bytes, base64) |
| Field | Description |
|---|---|
delivery_id | UUID for this delivery slot — pass to POST /v1/issuances in Step 1 |
commitment_nonce | 16-byte server-generated nonce (base64) — incorporate into your AEAD authentication data before encrypting |
5-Minute Window: The reservation expires after 5 minutes. Encrypt and submit your delivery payload within that window. If it lapses, reserve again — the new nonce must be incorporated into a fresh encryption.
Step 1 — Create The Delivery (Admin)
With the reservation in hand, encrypt the DEK for the recipient and submit it. Only entity admins can create deliveries.
Fetch Recipient Delivery Keys
Before encrypting, fetch the target member's delivery public keys from the member list:
curl https://api.kyndex.co/v1/entities/{id}/memberships \
-H 'Authorization: Bearer <access_token>'Each member record in the response includes:
| Field | Description |
|---|---|
delivery_mlkem_ek | Recipient's ML-KEM-1024 encapsulation key — used to encrypt the DEK (base64) |
delivery_dsa_vk | Recipient's hybrid DSA verifying key — used to compute the hash-lock commitment (1984 bytes, base64) |
Only members who have completed the membership claim step have delivery keys. Members without them are excluded from the list.
Build The Encrypted Payload
Your device performs these steps before submitting:
- Derive your admin DSA keypair — derive a per-entity admin delivery keypair from your blind index key and entity ID.
- Compute hash-lock commitments —
SHA-256(recipient_mlkem_ek)andSHA-256(recipient_dsa_vk). The server uses these at acceptance time to verify the correct recipient is responding. - Encrypt the DEK — hybrid KEM (ML-KEM-1024 + X25519) encapsulates a shared secret using the recipient's
delivery_mlkem_ek. The DEK, capability payload, and admin signature are encrypted as the payload with the commitment nonce, tokens, andaad_tsin the AEAD authentication data. - Record
aad_ts— capture your current Unix timestamp (seconds). This value must be passed verbatim in the request and is included in the AEAD authentication data.
Submit The Delivery
curl -X POST https://api.kyndex.co/v1/issuances \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"entity_token": "<base64-32-bytes>",
"doc_token": "<base64-32-bytes>",
"aad_ts": 1743724800,
"admin_delivery_vk": "<base64-1984-bytes>",
"ephemeral_pubkey": "<base64-1600-bytes>",
"encrypted_payload": "<base64>",
"pending_recipient_ek_hash": "<base64-32-bytes>",
"pending_recipient_dsa_hash": "<base64-32-bytes>",
"delivery_id": "11111111-2222-3333-4444-555555555555"
}'Response (201 Created):
{
"delivery_token": "<base64-32-bytes>",
"status": "pending",
"expires_at": "2026-04-15T00:00:00.000Z",
"created_at": "2026-04-08T00:00:00.000Z"
}A Location header pointing to the new resource is also returned.
| Field | Description |
|---|---|
entity_token | One-way HMAC token identifying the target entity (32 bytes, base64) |
doc_token | One-way HMAC token identifying the document (32 bytes, base64) |
aad_ts | Unix timestamp (seconds) at the moment you encrypted — captured as Math.floor(Date.now() / 1000). Passed verbatim; included in AEAD authentication data |
admin_delivery_vk | Your admin hybrid verifying key (ML-DSA-65 + Ed25519, 1984 bytes, base64) — stored so the server can verify the capability signature at acceptance |
ephemeral_pubkey | Hybrid KEM ciphertext for recipient decryption (ML-KEM-1024 + X25519, 1600 bytes, base64) |
encrypted_payload | AEAD ciphertext containing the wrapped DEK, capability payload, and admin signature (base64) |
pending_recipient_ek_hash | SHA-256(recipient_mlkem_ek) — 32 bytes, base64. Binds this delivery to a specific recipient encryption key |
pending_recipient_dsa_hash | SHA-256(recipient_dsa_vk) — 32 bytes, base64. Binds this delivery to a specific recipient signing key |
delivery_id | UUID from the reservation step |
expires_at | Optional ISO 8601 expiration timestamp. Omit to accept the default 7-day expiration |
Targeted Delivery: The two hash-lock fields (pending_recipient_ek_hash,
pending_recipient_dsa_hash) ensure only the intended recipient can accept. Even if someone else
intercepts the encrypted payload, they cannot produce the matching verifying key and signature at
acceptance time.
Step 2 — Discover Pending Deliveries (Recipient)
Recipients query for deliveries addressed to their encryption key. Unlike grant discovery, there is no tag scanning or false-positive filtering — the server resolves your membership, hashes your stored delivery_mlkem_ek, and returns only deliveries addressed to that exact key.
curl 'https://api.kyndex.co/v1/issuances?entity_token=<base64-32-bytes>' \
-H 'Authorization: Bearer <access_token>'Response (200 OK):
{
"count": 1,
"deliveries": [
{
"delivery_token": "<base64-32-bytes>",
"entity_token": "<base64-32-bytes>",
"doc_token": "<base64-32-bytes>",
"aad_ts": 1743724800,
"ephemeral_pubkey": "<base64-1600-bytes>",
"encrypted_payload": "<base64>",
"commitment_nonce": "<base64-16-bytes>"
}
]
}| Field | Description |
|---|---|
entity_token | Query parameter — your HMAC entity token for the entity context to check (32 bytes, base64) |
| Field | Description |
|---|---|
delivery_token | Opaque token identifying this delivery — used as the path parameter in Step 3 |
ephemeral_pubkey | KEM ciphertext (1600 bytes, base64) — use with your ML-KEM private key to recover the shared secret |
encrypted_payload | AEAD ciphertext containing the DEK, capability payload, and admin signature |
aad_ts | Timestamp the admin embedded in the AEAD AAD — verify it is reasonable before accepting |
commitment_nonce | 16-byte nonce the server committed at reservation time — present in the AEAD AAD |
To decrypt: use your ML-KEM private key and the ephemeral_pubkey KEM ciphertext to recover the shared secret, then use the shared secret (combined with the X25519 component) to decrypt encrypted_payload. The decrypted payload contains the DEK wrapped for your personal master key, the capability payload, and the admin's signature over it.
Step 3 — Accept Or Deny (Recipient)
Once you have decrypted the payload, you decide whether to accept the delivery. Both transitions use the same PATCH endpoint — the status field in the request body determines which path is taken.
Accept The Delivery
Accepting requires re-wrapping the DEK under your personal master key and providing proof-of-possession:
- Unwrap the DEK — decrypt the DEK from the payload using your personal key.
- Re-wrap under your UMK — wrap the DEK with your personal master key (UMK) to produce
wrapped_dek_umk. This is the form stored for your later use. - Sign the accept message — sign
"kyndex-delivery-accept-v1" || delivery_token(32 bytes) || owner_token(32 bytes)with your hybrid DSA private key (ML-DSA-65 + Ed25519).
curl -X PATCH https://api.kyndex.co/v1/issuances/{delivery_token} \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"status": "accepted",
"doc_token": "<base64-32-bytes>",
"entity_token": "<base64-32-bytes>",
"wrapped_dek_umk": "<base64>",
"capability_payload": "<base64>",
"admin_signature": "<base64-3373-bytes>",
"recipient_dsa_vk": "<base64-1984-bytes>",
"recipient_signature": "<base64-3373-bytes>"
}'Response (200 OK):
{
"status": "accepted"
}| Field | Description |
|---|---|
status | "accepted" |
doc_token | One-way HMAC token identifying the document (32 bytes, base64) |
entity_token | One-way HMAC token identifying the entity (32 bytes, base64) |
wrapped_dek_umk | DEK re-wrapped under your personal master key (base64) — stored and returned in Step 4 |
capability_payload | Admin-signed authorization payload extracted from the decrypted envelope (base64) |
admin_signature | Admin hybrid signature over capability_payload — extracted from the decrypted envelope (3373 bytes, base64) |
recipient_dsa_vk | Your hybrid verifying key (ML-DSA-65 + Ed25519, 1984 bytes, base64) — the server checks that SHA-256 of this matches the hash-lock committed at create time |
recipient_signature | Your hybrid signature over the canonical accept message (3373 bytes, base64) |
The server runs three checks before transitioning the delivery:
- Hash-lock —
SHA-256(recipient_dsa_vk)must matchpending_recipient_dsa_hashstored at creation. Prevents key substitution. - Proof-of-possession — your
recipient_signaturemust verify over"kyndex-delivery-accept-v1" || delivery_token || owner_tokenusingrecipient_dsa_vk. Proves you hold the private key. - Admin capability —
admin_signaturemust verify overcapability_payloadusing theadmin_delivery_vkstored at creation. Proves the admin authorised this specific delivery.
All three must pass. If any fails, the delivery remains pending.
Deny The Delivery
Denial requires no cryptographic material — just the status discriminant:
curl -X PATCH https://api.kyndex.co/v1/issuances/{delivery_token} \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"status": "denied"
}'Response (200 OK):
{
"status": "denied"
}Denying does not reveal whether the recipient decrypted the payload — the server only records the state transition.
Step 4 — List Received Deliveries (Recipient)
Once accepted, deliveries appear in your received list with the wrapped DEK ready for local use:
curl https://api.kyndex.co/v1/issuances/received \
-H 'Authorization: Bearer <access_token>'Response (200 OK):
{
"deliveries": [
{
"delivery_token": "<base64-32-bytes>",
"doc_token": "<base64-32-bytes>",
"entity_token": "<base64-32-bytes>",
"wrapped_dek_umk": "<base64>",
"accepted_at": "2026-04-08T12:00:00.000Z"
}
]
}| Field | Description |
|---|---|
wrapped_dek_umk | DEK wrapped with your personal master key — unwrap locally to decrypt the document |
doc_token | One-way HMAC document token — use to look up the document record |
entity_token | One-way HMAC entity token — identifies which entity this delivery came from |
accepted_at | Timestamp when you accepted the delivery |
Delivery Status Reference
| Status | Meaning | Who Can Act |
|---|---|---|
pending | Delivery created, waiting for recipient | Recipient (accept, deny) |
accepted | Recipient accepted — wrapped DEK is in received list | — |
denied | Recipient declined the delivery | — |
expired | TTL elapsed — set by a server alarm, not a database sweep | — |
Expiry is enforced by a server-side alarm registered at create time. The delivery defaults to 7 days unless an expires_at was specified at creation.
API Endpoint Summary
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/v1/issuances/reservations | POST | Bearer / cookie | Reserve a delivery slot (admin only) |
/v1/issuances | POST | Bearer / cookie | Create the delivery (admin only) |
/v1/issuances?entity_token=... | GET | Bearer / cookie | Discover pending deliveries (recipient) |
/v1/issuances/{delivery_token} | PATCH | Bearer / cookie | Accept or deny a delivery (recipient) |
/v1/issuances/received | GET | Bearer / cookie | List accepted deliveries with wrapped keys |
All endpoints require an unlocked session. There are no unauthenticated delivery endpoints.
Error Handling
| Status | When | What To Do |
|---|---|---|
400 | Invalid parameters — bad base64, wrong byte length, malformed UUID | Check field lengths against the schema |
401 | Missing or expired access token | Refresh your token or log in again |
403 | Not an entity admin (reserve/create), or admin capability signature invalid | Verify your role and that capability_payload and admin_signature were extracted correctly |
404 | Delivery token not found, or recipient key mismatch during acceptance | Verify the delivery token and that your recipient_dsa_vk matches what was committed |
409 | Delivery already accepted (duplicate acceptance attempt) | Each delivery can only be accepted once |
429 | Rate limit exceeded | Back off and retry after the Retry-After period |
All errors follow the RFC 7807 Problem Details format.
What The Server Sees (And Doesn't)
| The Server Sees | The Server Does NOT See |
|---|---|
| An encrypted payload and KEM ciphertext | The document encryption key |
| Opaque HMAC tokens for entity and document | Which entity member sent the delivery |
| Hash-lock commitments (SHA-256 of recipient keys) | The recipient's identity |
| Delivery status transitions | Whether the recipient decrypted the payload before denying |
| Timestamps and expiry | The relationship between admin and recipient |
Think Of It Like A Sealed Envelope With A Named Lock: The admin seals the envelope with a padlock that only one person's key can open. The post office delivers it, records when it was collected or refused, but cannot open the envelope — and cannot tell who the sender and recipient are from the outside.
Related Resources
- Entities & Memberships — entity roles, membership claim, and delivery key population
- Entity Onboarding — creating entities, adding members, and fetching the member list
- Grants & Sharing — alternative sharing mechanism using view tags and consent-based claims
- Key Hierarchy — how entity, member, and document keys are structured