Skip to content

XChat ​

XChat is Twitter's end-to-end encrypted chat, a separate system from the legacy DMs API.

emusks has mostly reverse-engineered XChat for you. Set up an identity once, then call message(), and emusks handles all of the crypto.

js
import Emusks from "emusks";

const client = new Emusks();
await client.login("your_auth_token");

const identity = await client.xchat.createIdentity({ pin: "2468" });

const { sequenceId } = await client.xchat.message("44196397", "hello from xchat ^w^");
await client.xchat.react("44196397", sequenceId, "🔥");
await client.xchat.edit("44196397", sequenceId, "fixed typo");

Lookups work without an identity of your own:

js
await client.xchat.isOnXChat("44196397");  // true
await client.xchat.canMessage("44196397"); // can you reach them over XChat?
await client.xchat.fingerprint(await client.xchat.publicKey("44196397"));
// "8435:8166:a7fb:5c30:...:8e4b:b1c6"

Yes this was all fucking vibecoded, I'm not manually reading like 15 megabytes of minified JS (yes XChat loads 15 megabytes of JS) to write this. It works well though. Thanks Claude!

PageMethods
Setup & identity (this page)createIdentity, loadIdentity, recover
Messageslookups, sending, reading, conversations and message requests
CallsXChat audio and video calls, and Twitter Calls meetings (client.xcalls)
XChat advancedtoken, gql, socket
XChat protocolhow XChat works on the wire

Setup & identity ​

Looking things up on XChat only needs a logged-in session. Sending and reading need an XChat identity: two P-256 keypairs whose public halves are published to Twitter and whose private halves you keep. Create one once, save it, and load it on later runs.

js
const identity = await client.xchat.createIdentity({ pin: "2468" });
await fs.writeFile("xchat-identity.json", JSON.stringify(identity));

// later runs
await client.xchat.loadIdentity(JSON.parse(await fs.readFile("xchat-identity.json", "utf8")));

xchat.createIdentity(opts?) ​

Generates a new identity, publishes the public keys, backs the private keys up under your PIN and loads the identity into the client. It returns a plain object that contains your private keys, so store it somewhere safe and pass it to loadIdentity next time.

The backup uses Juicebox, like the official app, so the identity can be recovered with the PIN. Use a 4-digit PIN like the app does, so you can also unlock the identity from the official Twitter UI.

js
const identity = await client.xchat.createIdentity({ pin: "2468" });
const local = await client.xchat.createIdentity({ selfCustody: true });
OptionTypeDescription
pinstringYour passcode. Required unless selfCustody is set.
selfCustodybooleanSkip the PIN backup. You hold the only copy of the keys.
registrationMethodstringPublished method. Default CustomPin, or SelfCustody with selfCustody. ManagedPin also exists.
versionstringRequested key version. Default Date.now(); Twitter assigns the final one.

The Juicebox WebAssembly build is bundled and only loads when you create or recover a PIN-backed identity. If the backup fails, createIdentity deletes the key it just published and throws, so you never end up with a published key that has no backup.

WARNING

With selfCustody: true there is no PIN recovery. Lose the identity object and you lose access to that identity's chats.

xchat.loadIdentity(identity) ​

Loads a saved identity so you can send and read. This is much faster than recover, so prefer it when you have the object.

js
await client.xchat.loadIdentity(identity);
FieldTypeDescription
versionstringYour published key version
publicKeyB64stringIdentity public key, SPKI base64
signingPublicKeyB64stringSigning public key, SPKI base64
identityPrivateJwk, signingPrivateJwkobjectYour private keys as JWK
userIdstringOptional. Defaults to the logged-in user.

xchat.recover(pin) ​

Recovers your identity with only your PIN and the logged-in session, the way a new device does. It fetches your published key, pulls the backed-up private keys from Twitter's realms, rebuilds the keypairs and loads them.

js
const { identity } = await client.xchat.recover("2468");

for (const convo of await client.xchat.conversations()) {
  const { messages } = await client.xchat.read(convo.conversationId);
}

It returns { userId, version, recovered, identity }, where identity has the same shape as the createIdentity result. Save it and use loadIdentity next time to skip the realms. recover only works for PIN-backed identities. Next, see messages.

not affiliated with X Corp.