Skip to content

InfoPackage API

InfoPackages enable ephemeral, encrypted contact exchange and group invitations via QR codes or shareable links. All encryption happens client-side and the server only stores encrypted blobs with no knowledge of their contents.

Use cases:

  • Contact sharing via QR codes
  • Group invite links

Uploads an encrypted InfoPackage for sharing. The client must encrypt the package before upload.

POST /v1/infopackages/upload
POST /v1/infopackages/upload HTTP/1.1
Authorization: Bearer {access_token}
Content-Type: application/json
{
"ciphertext": "base64_of_encrypted_infopackage_content",
"package_type": "identity",
"ttl_seconds": 86400,
"max_uses": 100
}

Parameters:

  • ciphertext: Base64-encoded encrypted InfoPackage

    • Encryption: Client encrypts with random 32-byte key (ChaCha20-Poly1305)
    • Privacy: Server stores ciphertext only, cannot decrypt
    • Key distribution: Decryption key embedded in QR code or URL fragment
  • package_type: Type hint (identity or group_invite)

    • For client UX (allows displaying “Add Contact” vs “Join Group”)
    • Not required for security
  • ttl_seconds: Time-to-live in seconds (min: 60, max: 2592000 / 30 days)

  • max_uses: Optional download limit (min: 1, max: 1000, null = unlimited)

{
"url_segment": "a1b2c3d4-e5f6-4978-b695-04132e1f2d3c",
"expires_at": 1759086400,
"max_uses": 100
}

Response fields:

  • url_segment: Random identifier for fetching (UUIDv4)

    • Client constructs full URL: https://{server}/v1/infopackages/{url_segment}
  • expires_at: Unix timestamp when package expires

  • max_uses: Maximum number of downloads allowed (null if unlimited)

Client responsibilities:

  • Construct full URL from server domain + url_segment
  • Keep decryption key locally (never send to server)
  • Embed key in QR code data or URL fragment
{
"error": "INVALID_CONFIG",
"message": "TTL must be between 60 seconds and 30 days",
"code": 4013
}
{
"error": "INFOPACKAGE_RATE_LIMIT",
"message": "InfoPackage upload limit exceeded. Maximum 10 per hour, 50 per day.",
"code": 4009,
"retry_after": 1800
}

Retrieves an encrypted InfoPackage by URL segment. No authentication required, anyone with the URL can fetch it.

GET /v1/infopackages/{url_segment}
GET /v1/infopackages/55b15587-7200-4eae-89ca-be368e5d514e HTTP/1.1
{
"ciphertext": "base64_encrypted_content",
"package_type": "identity",
"created_at": 1759000000,
"expires_at": 1759086400,
"remaining_uses": 99
}

Response fields:

  • ciphertext: Base64-encoded encrypted package content (server cannot decrypt)
  • package_type: Type hint (identity or group_invite)
  • created_at: Unix timestamp when package was created
  • expires_at: Unix timestamp when package expires
  • remaining_uses: Downloads remaining (null if unlimited)

Client processing:

  1. Fetch encrypted package from URL
  2. Extract decryption key from QR code or URL fragment
  3. Decrypt ciphertext locally
  4. Parse JSON based on package_type
  5. Display confirmation prompt
  6. Perform action if user confirms
{
"error": "NOT_FOUND",
"message": "InfoPackage not found or expired"
}

Causes:

  • URL segment doesn’t exist
  • Package already expired (TTL passed)
  • Package was manually revoked
{
"error": "INFOPACKAGE_EXHAUSTED",
"message": "InfoPackage has reached maximum uses"
}

Manually revokes an InfoPackage before expiration. Useful for revoking shared links.

DELETE /v1/infopackages/{url_segment}
DELETE /v1/infopackages/d3c7e5ed-3fbd-405c-9d1d-5b05499d7e04 HTTP/1.1
Authorization: Bearer {access_token}

Authentication required: Must be the original uploader.

{
"revoked": true,
"revoked_at": 1759000500
}
{
"error": "UNAUTHORIZED",
"message": "You can only revoke InfoPackages you created"
}
{
"error": "NOT_FOUND",
"message": "InfoPackage not found or already expired"
}

These structures are encrypted client-side before upload. The server never sees this data in plaintext.

Used for contact exchange via QR codes.

struct IdentityInfoPackage {
user_id: UserId, // Blake3 hash of user public key
user_public_key: Ed25519PublicKey, // Ed25519 public key
default_persona: Persona, // Default display persona
personas: HashMap<PersonaId, Persona>, // Additional personas
devices: Vec<DeviceInfo>, // User's devices
created_at: u64, // Unix timestamp
}

See Cryptid’s Identity System for more information about the Persona and DeviceInfo structs.

Used for group invitations via shareable links.

struct GroupInviteInfoPackage {
group_id: GroupId, // UUIDv4
group_name: Option<String>,
group_picture: Option<GroupPicture>,
group_description: Option<String>,
welcome_message: Vec<u8>, // MLS Welcome message
invited_by: UserId, // Inviter's user ID
invited_by_name: String, // Inviter's display name
created_at: u64, // Unix timestamp
}

What servers know:

  • URL segment was created (random UUIDv4, no semantic meaning)
  • When package expires
  • Number of times fetched (if max_uses tracking enabled)
  • Type hint (identity or group_invite)

What servers DON’T know:

  • Identity data inside package (encrypted)
  • Group details inside package (encrypted)
  • Who fetched the package (downloads are anonymous)
  • Who is adding whom as contact
  • Who is joining which groups
  • Any plaintext content whatsoever

Encryption:

  • Algorithm: ChaCha20-Poly1305
  • Key: Random 32 bytes, never sent to server
  • Key distribution: Embedded in QR code or URL fragment only
  • Server stores ciphertext only (cannot decrypt)

Expiration:

  • Automatic cleanup via TTL
  • Manual revocation via DELETE endpoint
  • Usage limit enforcement (if max_uses specified)

Denial of Service:

  • Rate limited: 10 uploads/hour, 50 uploads/day per user
  • TTL limits: 60 seconds minimum, 30 days maximum
  • Max uses limits: 1-1000 downloads per package

For shareable links, embed the decryption key in the URL fragment:

https://server.com/invite/a1b2c3d4-e5f6-4978-b695-04132e1f2d3c#32_byte_key_hex
^ key in fragment

Why use fragment (#)?

  • Everything after # is NOT sent to server in HTTP requests
  • Browser/app extracts key client-side only
  • Server only sees: /invite/a1b2c3d4-e5f6-4978-b695-04132e1f2d3c
  • Network observers cannot see the key

QR codes contain a JSON structure with URL and decryption key:

{
"infopackage_url": "https://server.com/invite/abc123",
"infopackage_key": "64_char_hex_encoded_32_bytes",
"package_type": "identity",
"display_name": "Alice"
}

Display flow:

  1. Scanner sees “Alice” immediately (from display_name)
  2. User confirms: “Add Alice as contact?”
  3. App fetches encrypted package and decrypts with key
  4. Adds contact with full identity details

For contact sharing:

  • Use 24-48 hour TTL (ephemeral)
  • Allow multiple uses (max_uses: 100)
  • Revoke old QR codes when generating new ones
  • Generate new QR codes regularly for privacy

For group invites:

  • Use 7-day TTL (enough time to share)
  • Limit uses (max_uses: 10-50)
  • Revoke after group is full or event ends
  • One invite per event/purpose

For security:

  • Always use fresh encryption keys (never reuse)
  • Securely delete keys after package expires
  • Don’t log or persist keys server-side
  • Validate ciphertext size (prevent DoS)