Skip to content

KeyPackage API

KeyPackages are one-time-use cryptographic bundles that enable adding devices to MLS groups. Each device uploads KeyPackages to a server, where they can be fetched when adding that device to a group.

KeyPackage Lifecycle:

  1. Device generates KeyPackages using its Device Identity keypair
  2. Device uploads KeyPackages to a server
  3. Server stores KeyPackages indexed by device_id
  4. Other devices fetch KeyPackages when inviting this device to groups
  5. After use, KeyPackage is deleted (one-time use only)

Upload a batch of MLS KeyPackages for this device.

POST /v1/keypackages/upload
POST /v1/keypackages/upload HTTP/1.1
Authorization: Bearer {access_token}
Content-Type: application/json
{
"device_id": "8f6b753d772275127557397be1edce476cd698ed5f09a2b4a70fb64a0577ab2a",
"keypackages": [
"base64_mls_keypackage_1",
"base64_mls_keypackage_2",
"base64_mls_keypackage_3"
]
}

Parameters:

  • device_id: Blake3 hash of device public key (64 hex chars)
  • keypackages: Array of base64-encoded MLS KeyPackages (max 100 per request)

Validation:

  • KeyPackages must be valid MLS format (server validates structure, not signature)
  • Each KeyPackage must be unique (no duplicates)
  • Device must be announced (have valid delivery address)
{
"uploaded": 3,
"total_available": 23,
"expires_at": 1763568947
}

Response fields:

  • uploaded: Number of KeyPackages successfully uploaded in this request
  • total_available: Total KeyPackages now available for this device
  • expires_at: Unix timestamp when KeyPackages expire (typically 30 days)

Recommended practice: Keep 50-100 KeyPackages available at all times. Upload more when count drops below 20.

{
"error": "INVALID_KEYPACKAGE",
"message": "KeyPackage format invalid or corrupted",
"code": 4015
}
{
"error": "TOO_MANY_KEYPACKAGES",
"message": "Maximum 100 KeyPackages per upload request",
"code": 4003
}

Retrieve available KeyPackages for a specific device. Used when inviting that device to a group.

GET /v1/keypackages/{device_id}
GET /v1/keypackages/8f6b753d772275127557397be1edce476cd698ed5f09a2b4a70fb64a0577ab2a?count=1 HTTP/1.1
Authorization: Bearer {access_token}

Query Parameters:

  • count: Number of KeyPackages to fetch (default: 1, max: 10)

Authentication required: Must have valid device announcement.

{
"device_id": "8f6b753d772275127557397be1edce476cd698ed5f09a2b4a70fb64a0577ab2a",
"keypackages": [
"base64_mls_keypackage_1"
],
"remaining": 22,
"fetched_at": 1763569257
}

Response fields:

  • device_id: Device these KeyPackages belong to
  • keypackages: Array of base64-encoded MLS KeyPackages
  • remaining: KeyPackages still available for this device
  • fetched_at: Unix timestamp of fetch

Consumption: Fetched KeyPackages are immediately marked as “consumed” and won’t be returned again (one-time use).

{
"error": "DEVICE_NOT_FOUND",
"message": "Device not registered or has no KeyPackages",
"code": 4014
}
{
"error": "INVALID_KEYPACKAGE",
"message": "Device has exhausted all KeyPackages. Ask device owner to upload more.",
"code": 4015
}

What to do: Wait for device owner to come online and upload more KeyPackages, or show error to user.


Batch fetch KeyPackages for multiple devices at once (for multi-device invitations).

POST /v1/keypackages/batch-fetch
POST /v1/keypackages/batch-fetch HTTP/1.1
Authorization: Bearer {access_token}
Content-Type: application/json
{
"devices": [
{
"device_id": "8f6b753d772275127557397be1edce476cd698ed5f09a2b4a70fb64a0577ab2a",
"count": 1
},
{
"device_id": "3e77f82208e44f22d6d0c7b46c435a063f97f5e9ca44947f79555c01925a1c2e",
"count": 1
}
]
}
{
"results": [
{
"device_id": "8f6b753d772275127557397be1edce476cd698ed5f09a2b4a70fb64a0577ab2a",
"keypackages": ["base64_keypackage"],
"success": true
},
{
"device_id": "3e77f82208e44f22d6d0c7b46c435a063f97f5e9ca44947f79555c01925a1c2e",
"keypackages": [],
"success": false,
"error": "NO_KEYPACKAGES_AVAILABLE"
}
],
"successful": 1,
"failed": 1
}

Use case: Inviting a user with multiple devices to a group in a single MLS commit.


Remove all KeyPackages for this device (used when rotating device keys or deregistering).

DELETE /v1/keypackages
DELETE /v1/keypackages HTTP/1.1
Authorization: Bearer {access_token}
{
"deleted": 22,
"device_id": "8f6b753d772275127557397be1edce476cd698ed5f09a2b4a70fb64a0577ab2a"
}

Generation:

let keypackage_bundle = device.generate_key_packages(100)?;
// keypackage_bundle contains:
// - public KeyPackages (upload to server)
// - private keys (store locally for welcome decryption)

Upload:

  • Generate 50-100 KeyPackages at device registration
  • Upload to server immediately
  • Monitor count and upload more when <20 remaining

Consumption:

  • Each KeyPackage used once (for adding device to one group)
  • After use, server deletes it (prevents replay)
  • Device uploads more as needed

Expiration:

  • KeyPackages expire after 30 days (unused)
  • Server automatically deletes expired KeyPackages
  • Device should refresh before expiration

Scenario: Alice@server1.com wants to invite Bob@server2.com to a group.

Flow:

  1. Alice fetches Bob’s InfoPackage (contains device_id + keypackage_server field)
  2. Alice’s server contacts Bob’s keypackage_server via federation
  3. Bob’s server returns KeyPackages for Bob’s devices
  4. Alice creates MLS commit with fetched KeyPackages

Federation endpoint: GET /federation/v1/keypackages/{device_id}

Server Validation:

  • Server validates KeyPackage structure (MLS format)
  • Server does NOT verify signatures (recipients do this)
  • Server does NOT decrypt KeyPackages (opaque blobs)

Privacy:

  • KeyPackages contain device_id (public)
  • KeyPackages contain ciphersuite info (public)
  • KeyPackages do NOT contain user identity info
  • Fetching is authenticated (prevents scraping)

Upload limits:

  • Max 100 KeyPackages per request
  • Max 500 KeyPackages stored per device
  • Max 10 upload requests per hour

Fetch limits:

  • Max 10 KeyPackages per fetch
  • Max 100 fetches per hour per device