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

Create an API client with your user OAuth 2.0 access token:

2. Initialize the Chat XDK

Load private keys with Juicebox (PIN + juicebox_config from the API) or a key blob (import_keys). Then set your registered public-key version (public_key_version on your record).
Server and bot samples often use a key blob (export_keys / import_keys). Client apps often use Juicebox (setup / unlock with a PIN). See the Chat XDK reference for both paths.

3. Register public keys (one-time)

Generate keypairs, register public keys with X, then store private keys (setup with a PIN, or export_keys).
Use a strong PIN for Juicebox. Losing the PIN 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 PINs, private keys, or message plaintext in production
  • In web apps, keep OAuth tokens (and Juicebox 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