Kyndex
Guides

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

StepWhoWhat Happens
0. ReserveAdminObtain a delivery ID and commitment nonce from the server
1. CreateAdminEncrypt the DEK for the recipient and submit the delivery
2. DiscoverRecipientQuery for pending deliveries addressed to your encryption key
3. Accept / DenyRecipientDecrypt, re-wrap the key, and accept — or deny without decrypting
4. List ReceivedRecipientRetrieve 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>"
}
FieldDescription
entity_tokenOne-way HMAC token identifying the target entity (32 bytes, base64)
doc_tokenOne-way HMAC token identifying the document being delivered (32 bytes, base64)
FieldDescription
delivery_idUUID for this delivery slot — pass to POST /v1/issuances in Step 1
commitment_nonce16-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:

FieldDescription
delivery_mlkem_ekRecipient's ML-KEM-1024 encapsulation key — used to encrypt the DEK (base64)
delivery_dsa_vkRecipient'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:

  1. Derive your admin DSA keypair — derive a per-entity admin delivery keypair from your blind index key and entity ID.
  2. Compute hash-lock commitmentsSHA-256(recipient_mlkem_ek) and SHA-256(recipient_dsa_vk). The server uses these at acceptance time to verify the correct recipient is responding.
  3. 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, and aad_ts in the AEAD authentication data.
  4. 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.

FieldDescription
entity_tokenOne-way HMAC token identifying the target entity (32 bytes, base64)
doc_tokenOne-way HMAC token identifying the document (32 bytes, base64)
aad_tsUnix timestamp (seconds) at the moment you encrypted — captured as Math.floor(Date.now() / 1000). Passed verbatim; included in AEAD authentication data
admin_delivery_vkYour admin hybrid verifying key (ML-DSA-65 + Ed25519, 1984 bytes, base64) — stored so the server can verify the capability signature at acceptance
ephemeral_pubkeyHybrid KEM ciphertext for recipient decryption (ML-KEM-1024 + X25519, 1600 bytes, base64)
encrypted_payloadAEAD ciphertext containing the wrapped DEK, capability payload, and admin signature (base64)
pending_recipient_ek_hashSHA-256(recipient_mlkem_ek) — 32 bytes, base64. Binds this delivery to a specific recipient encryption key
pending_recipient_dsa_hashSHA-256(recipient_dsa_vk) — 32 bytes, base64. Binds this delivery to a specific recipient signing key
delivery_idUUID from the reservation step
expires_atOptional 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>"
    }
  ]
}
FieldDescription
entity_tokenQuery parameter — your HMAC entity token for the entity context to check (32 bytes, base64)
FieldDescription
delivery_tokenOpaque token identifying this delivery — used as the path parameter in Step 3
ephemeral_pubkeyKEM ciphertext (1600 bytes, base64) — use with your ML-KEM private key to recover the shared secret
encrypted_payloadAEAD ciphertext containing the DEK, capability payload, and admin signature
aad_tsTimestamp the admin embedded in the AEAD AAD — verify it is reasonable before accepting
commitment_nonce16-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:

  1. Unwrap the DEK — decrypt the DEK from the payload using your personal key.
  2. 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.
  3. 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"
}
FieldDescription
status"accepted"
doc_tokenOne-way HMAC token identifying the document (32 bytes, base64)
entity_tokenOne-way HMAC token identifying the entity (32 bytes, base64)
wrapped_dek_umkDEK re-wrapped under your personal master key (base64) — stored and returned in Step 4
capability_payloadAdmin-signed authorization payload extracted from the decrypted envelope (base64)
admin_signatureAdmin hybrid signature over capability_payload — extracted from the decrypted envelope (3373 bytes, base64)
recipient_dsa_vkYour 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_signatureYour hybrid signature over the canonical accept message (3373 bytes, base64)

The server runs three checks before transitioning the delivery:

  1. Hash-lockSHA-256(recipient_dsa_vk) must match pending_recipient_dsa_hash stored at creation. Prevents key substitution.
  2. Proof-of-possession — your recipient_signature must verify over "kyndex-delivery-accept-v1" || delivery_token || owner_token using recipient_dsa_vk. Proves you hold the private key.
  3. Admin capabilityadmin_signature must verify over capability_payload using the admin_delivery_vk stored 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"
    }
  ]
}
FieldDescription
wrapped_dek_umkDEK wrapped with your personal master key — unwrap locally to decrypt the document
doc_tokenOne-way HMAC document token — use to look up the document record
entity_tokenOne-way HMAC entity token — identifies which entity this delivery came from
accepted_atTimestamp when you accepted the delivery

Delivery Status Reference

StatusMeaningWho Can Act
pendingDelivery created, waiting for recipientRecipient (accept, deny)
acceptedRecipient accepted — wrapped DEK is in received list
deniedRecipient declined the delivery
expiredTTL 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

EndpointMethodAuthPurpose
/v1/issuances/reservationsPOSTBearer / cookieReserve a delivery slot (admin only)
/v1/issuancesPOSTBearer / cookieCreate the delivery (admin only)
/v1/issuances?entity_token=...GETBearer / cookieDiscover pending deliveries (recipient)
/v1/issuances/{delivery_token}PATCHBearer / cookieAccept or deny a delivery (recipient)
/v1/issuances/receivedGETBearer / cookieList accepted deliveries with wrapped keys

All endpoints require an unlocked session. There are no unauthenticated delivery endpoints.

Error Handling

StatusWhenWhat To Do
400Invalid parameters — bad base64, wrong byte length, malformed UUIDCheck field lengths against the schema
401Missing or expired access tokenRefresh your token or log in again
403Not an entity admin (reserve/create), or admin capability signature invalidVerify your role and that capability_payload and admin_signature were extracted correctly
404Delivery token not found, or recipient key mismatch during acceptanceVerify the delivery token and that your recipient_dsa_vk matches what was committed
409Delivery already accepted (duplicate acceptance attempt)Each delivery can only be accepted once
429Rate limit exceededBack 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 SeesThe Server Does NOT See
An encrypted payload and KEM ciphertextThe document encryption key
Opaque HMAC tokens for entity and documentWhich entity member sent the delivery
Hash-lock commitments (SHA-256 of recipient keys)The recipient's identity
Delivery status transitionsWhether the recipient decrypted the payload before denying
Timestamps and expiryThe 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.

On this page