Skip to main content
Send and receive end-to-end encrypted direct messages on X: set up keys, initialize a conversation, send a message, and decrypt inbound traffic. X Chat apps use two pieces together:
Prerequisites
  • Developer account and an app configured for OAuth 2.0
  • User access token with dm.read, dm.write, tweet.read, and users.read

1. Install dependencies

The PyPI package is chatxdk; import it as chat_xdk. Requires Python 3.10+.
Create an API client with your user OAuth 2.0 access token:

2. Initialize the Chat XDK with existing keys

This step loads keys you already have—use it when this identity completed first-time setup before:
  • Secure key backup: construct the SDK with the juicebox_config from your public-key record, then unlock with your passcode to recover the private keys (for example, on a new device).
  • Key blob: import_keys with a blob you previously exported via export_keys.
Then set your registered public-key version (public_key_version on your record). Setting up for the first time? Construct the SDK the same way but skip unlock/import_keys, and continue to step 3 to create, back up, and register your keys.
Server and bot samples often use a key blob (export_keys / import_keys). Client apps often use secure key backup (setup / unlock with a passcode). See the Chat XDK reference for both paths.
Bringing your own keys? import_keys only accepts the opaque blob produced by export_keys from the Chat XDK—it is a versioned, private serialization of the full key state, not raw or PEM-encoded P-256 keys. You cannot construct this blob yourself: generate keys through generate_keypairs (step 3), export the blob once, and store it base64-encoded. Hand-crafted or modified blobs fail to import.

3. Create and register keys (first-time setup)

Skip this step if you loaded existing keys in step 2. Otherwise, one-time setup for a new identity does three things:
  1. Create the keypairsgenerate_keypairs produces the identity and signing keypairs.
  2. Register the public keys — POST the registration payload to the add-public-key endpoint so others can encrypt to you and verify your signatures.
  3. Store the private keyssetup with a passcode writes them to secure key backup (clients), or export_keys returns a key blob for you to store securely (servers and bots).
Use a strong passcode for secure key backup. Losing the passcode or an unprotected key blob can prevent decrypting past messages.

4. Set up conversation keys

Call prepare_conversation_key_change with your user ID, your signing key version, and every participant’s identity public key. One call generates a fresh conversation key, encrypts it for each participant, and signs the change. POST the result to the add conversation keys endpoint (POST /2/chat/conversations/{id}/keys)—the body needs conversation_key_version, conversation_participant_keys (SDK encrypted_key → API encrypted_conversation_key), and action_signatures (required; the API rejects the call without them). Keep the raw conversation key for sending. The response returns the canonical conversation id (data.conversation_id—the hyphen-joined pair for a 1:1, or the g-prefixed id for a group) and the key change’s data.sequence_id. Use that returned id for subsequent requests instead of reconstructing it client-side. The same call also rotates keys later: pass the existing conversation id to prepare_conversation_key_change and POST with the newer key version. Rotate when you suspect the conversation key was exposed—rotation protects future messages only; messages encrypted under earlier key versions stay readable to anyone who holds those versions.
Verify fetched keys before wrapping. prepare_conversation_key_change encrypts the fresh conversation key to whatever public keys you pass. Check each fetched record first with verify_key_binding(identity, signing, signature)—passing the record’s public_key, signing_public_key, and identity_public_key_signature fields from the public-keys API—so a substituted identity key cannot receive the conversation key.

5. Send a message

Encrypt with the raw conversation key bytes. On the send request, map: Use a hyphenated conversation id in the URL path when the API requires it (:-). The SDK itself is flexible: encrypt_message and encrypt_reply accept the id in any form you hold—A:B from events, A-B from listings or URL paths (in either order), or just the recipient’s user id—and canonicalize it before signing. Group ids (prefixed with g) pass through unchanged.

6. Receive and decrypt

Use webhooks or the activity stream for live traffic, or page conversation events for history.
  • Live payload fields: encoded_event, optional conversation_key_change_event
  • History: GET /2/chat/conversations/{id}/events — prefer decrypt_events on all events plus meta.conversation_key_events
  • Pass the sender’s public keys into decrypt for signature verification (map API fields into SigningKeyEntry; see Chat XDK)
  • JavaScript uses camelCase event types (message); other languages use "Message" and snake_case fields in JSON
Complete poll-and-reply bots for every language: chat-xdk/examples.

Best practices

  • Cache raw conversation keys and sender public keys; refresh on signature verification failures
  • Deduplicate live deliveries with event_uuid
  • Page event history until pagination is complete so key-change metadata is not missed
  • Do not log passcodes, private keys, or message plaintext in production
  • In web apps, keep OAuth tokens (and key backup realm token minting) on a server; prefer holding private keys only in the client Chat XDK

Next steps

Chat XDK

Methods and types for all language bindings

Media

Encrypted images and file attachments

Groups

Multi-participant conversations and metadata

Real-time events

Webhooks and activity delivery