WebSocket Events
The Enjin Platform can notify your application when something changes — an on-chain transfer, a transaction changing state, a managed wallet being created, or a user linking their wallet. This page covers receiving those notifications in real time.
These notifications are delivered over a WebSocket — a connection your application opens once and keeps open, so the server can send it messages at any time. The Enjin Platform sends them through Pusher, a widely-used message-delivery service with a documented protocol and official client libraries for most languages.
For eligible on-chain notifications, Event Inbox provides a second delivery option: poll GetPendingEvents at a time that suits your application, or use it alongside WebSocket to recover missed notifications. The event names and notification data match across both transports, and a shared delivery uuid lets you deduplicate them. See Working with Events for the complete workflow.
Pusher is just the delivery mechanism. You connect using Enjin's public app key below and authenticate with your regular Enjin Platform API token — there's nothing to sign up for or configure on Pusher's side.
How it works
Your account's events are broadcast on a channel — a named stream you subscribe to, e.g. private-user.b1acd213-2b27-406e-a21b-02307b52eca0. The private- prefix matters: only you are allowed to listen to it, so before Pusher lets you subscribe, you must prove to it that the Enjin Platform knows you. That proof is the AuthenticatePusherSocket mutation, and the whole flow is:
- Connect to the WebSocket URI (see Connection details). Pusher immediately sends back a
pusher:connection_establishedmessage containing asocket_id— an identifier for this particular connection, e.g.1234567.1234567. - Authenticate by passing that
socket_idto theAuthenticatePusherSocketmutation, using your normal API token. The platform returns two things: yourchannelname, and anauthsignature — a token that tells Pusher "the platform vouches for this connection". - Subscribe to the returned
channel, presenting theauthsignature. From this moment, events arrive over the open connection as they happen.
The auth signature is tied to the socket_id it was issued for, and every new connection gets a new socket_id — so if the connection drops and you reconnect, you repeat steps 2 and 3.
Reconnecting restores live delivery. To recover on-chain notifications missed during the outage, drain Event Inbox after the subscription succeeds and acknowledge each successfully processed delivery. Notifications remain available for 14 days from dispatch; receiving them over WebSocket does not automatically acknowledge them.
If that sounds like a lot of bookkeeping: it mostly isn't yours to do. A first-party Pusher library (like pusher-js) handles the connection, the re-authentication on reconnect, and the keepalive for you — you just tell it how to call the mutation. That's the recommended setup, and the quick start below is exactly that. Only if no Pusher library exists for your stack do you need to speak the protocol yourself — see Subscribing over a raw WebSocket.
Quick start
A complete, runnable listener in Node.js. Install the official Pusher client:
npm install pusher-js
Save this as listen-for-events.mjs, fill in your API token, and run it with node listen-for-events.mjs:
import Pusher from 'pusher-js';
const API_TOKEN = '<YOUR_API_TOKEN>';
// Exchanges a socket_id for subscription credentials, using the
// AuthenticatePusherSocket mutation documented below.
async function authenticateSocket(socketId) {
const response = await fetch('https://platform.enjin.io/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_TOKEN}`
},
body: JSON.stringify({
query: `
mutation AuthenticatePusherSocket($id: String!) {
AuthenticatePusherSocket(id: $id) {
auth
channel
}
}
`,
variables: { id: socketId }
}),
});
const { data } = await response.json();
return data.AuthenticatePusherSocket;
}
const pusher = new Pusher('8ab7ab8c519e8f59b635', {
cluster: 'us2',
channelAuthorization: {
// pusher-js calls this whenever the private channel needs
// (re-)authorizing — including automatically after a reconnect.
customHandler: ({ socketId }, callback) => {
authenticateSocket(socketId)
.then(({ auth }) => callback(null, { auth }))
.catch((error) => callback(error, null));
}
}
});
pusher.connection.bind('connected', async () => {
// One extra call up front to learn the channel name for this account.
const { channel } = await authenticateSocket(pusher.connection.socket_id);
const subscription = pusher.subscribe(channel);
console.log(`Connected. Listening on ${channel}`);
subscription.bind_global((event, payload) => {
console.log(event, payload);
});
});
Leave it running and make something happen — for example, create a managed wallet with the CreateManagedWallet mutation (or from the Platform UI). Within a second or two you'll see:
Connected. Listening on private-user.b1acd213-2b27-406e-a21b-02307b52eca0
ManagedWalletRequested { externalId: 'example123' }
ManagedWalletCreated {
externalId: 'example123',
publicKey: '0x8faddcca50c311c6eb3d04ecfedf2ae30c60686cfce8addf55a1013ef706db23'
}
See On-chain notifications for events also available through Event Inbox, and Platform events for lifecycle notifications.
AuthenticatePusherSocket requires your platform API token, so the socket should be opened from your backend (or the mutation proxied through it) — never ship the token to a browser or game client.
Connection details
| Setting | Value |
|---|---|
| Pusher app key | 8ab7ab8c519e8f59b635 |
| Cluster | us2 |
| WebSocket URI | wss://ws-us2.pusher.com/app/8ab7ab8c519e8f59b635?protocol=7 |
Pusher libraries take the app key and cluster (as in the quick start above); the full WebSocket URI is what you connect to when working without a library.
AuthenticatePusherSocket
AuthenticatePusherSocket(id: String!): PusherSocketAuth!
Takes the socket_id of a connected socket and returns the credentials for subscribing to your account's private event channel. The API token you authenticate the GraphQL request with determines which account's channel is authorized.
- GraphQL
- cURL
- Javascript
- Node.js
- Python
mutation AuthenticatePusherSocket($id: String!) {
AuthenticatePusherSocket(id: $id) {
auth
channel
}
}
Variables:
{
"id": "1359883.3809896"
}
curl --location 'https://platform.enjin.io/graphql' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <YOUR_API_TOKEN>' \
-d '{"query":"mutation AuthenticatePusherSocket($id: String!) { AuthenticatePusherSocket(id: $id) { auth channel } }","variables":{"id":"1359883.3809896"}}'
fetch('https://platform.enjin.io/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer <YOUR_API_TOKEN>'
},
body: JSON.stringify({
query: `
mutation AuthenticatePusherSocket($id: String!) {
AuthenticatePusherSocket(id: $id) {
auth
channel
}
}
`,
variables: { id: '1359883.3809896' }
}),
})
.then(response => response.json())
.then(data => console.log(data));
const axios = require('axios');
axios.post('https://platform.enjin.io/graphql', {
query: `
mutation AuthenticatePusherSocket($id: String!) {
AuthenticatePusherSocket(id: $id) {
auth
channel
}
}
`,
variables: { id: '1359883.3809896' }
}, {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer <YOUR_API_TOKEN>'
}
})
.then(response => console.log(response.data))
.catch(error => console.error(error));
import requests
query = '''
mutation AuthenticatePusherSocket($id: String!) {
AuthenticatePusherSocket(id: $id) {
auth
channel
}
}
'''
variables = {'id': '1359883.3809896'}
response = requests.post(
'https://platform.enjin.io/graphql',
json={'query': query, 'variables': variables},
headers={
'Content-Type': 'application/json',
'Authorization': 'Bearer <YOUR_API_TOKEN>'
}
)
print(response.json())
Response:
{
"data": {
"AuthenticatePusherSocket": {
"auth": "8ab7ab8c519e8f59b635:c81898c22164955191f384677873977d2c8d64e270aae004cc576ee71944def3",
"channel": "private-user.b1acd213-2b27-406e-a21b-02307b52eca0"
}
}
}
Subscribing over a raw WebSocket
If a first-party Pusher library isn't available for your stack, you can speak the protocol directly — it's the same three steps from How it works, done by hand. In the incoming messages shown below, data is a JSON-encoded string, so it needs a second parse.
-
Connect to
wss://ws-us2.pusher.com/app/8ab7ab8c519e8f59b635?protocol=7. The server sends:{
"event": "pusher:connection_established",
"data": "{\"socket_id\":\"1359883.3809896\",\"activity_timeout\":120}"
} -
Call
AuthenticatePusherSocketwith thesocket_id, then send a subscribe frame with the returned values:{
"event": "pusher:subscribe",
"data": {
"channel": "private-user.b1acd213-2b27-406e-a21b-02307b52eca0",
"auth": "8ab7ab8c519e8f59b635:c81898c22164955191f384677873977d2c8d64e270aae004cc576ee71944def3"
}
} -
The server confirms with
pusher_internal:subscription_succeeded, and from then on each platform event arrives as a frame:{
"event": "BalancesTransfer",
"data": "{\"network\":\"ENJIN\",\"chain\":\"RELAY\",\"uuid\":\"f2342ed4-3504-4f81-b4b5-315635283652\",\"publicKey\":\"0xa7208d10c6622f3f7eca1551de8355fde9de577dbb308d38994ace561738a51f\"}",
"channel": "private-user.b1acd213-2b27-406e-a21b-02307b52eca0"
}
Your client is also responsible for keepalive: if the connection is quiet for longer than the activity_timeout reported in pusher:connection_established, send {"event":"pusher:ping","data":{}} and expect a pusher:pong back (the server may likewise ping you). And remember that a reconnect is a new socket — repeat the authenticate-and-subscribe handshake every time.
On-chain notifications
These notifications are delivered through both Pusher and Event Inbox for eligible wallets and collections associated with your account. Each delivery includes a UUID that is shared across the two transports. Use it to deduplicate processing and pass it to AcknowledgeEvents after processing succeeds.
All Pusher payloads in this group contain network, chain, uuid, and publicKey. Addresses are public keys (0x...). Additional fields depend on the event:
| Event | Change | Additional payload fields |
|---|---|---|
BalancesTransfer | ENJ is transferred | None |
MultiTokensTransferred | Tokens are transferred | id: full token ID (collectionId-tokenId) |
MultiTokensMinted | Tokens are minted | id: full token ID |
MultiTokensBurned | Tokens are burned | id: full token ID |
MarketplaceListingCreated | A marketplace listing is created | tokenId: full token ID; listingId: listing ID |
MarketplaceListingFilled | A marketplace listing is filled | tokenId: full token ID; listingId: listing ID |
MarketplaceListingCancelled | A marketplace listing is cancelled or removed under its minimum | tokenId: full token ID; listingId: listing ID |
NominationPoolsBond | An account bonds into a nomination pool | id: pool ID |
NominationPoolsUnbond | An account initiates unbonding from a nomination pool | id: pool ID |
For example, a Pusher client library delivers this decoded BalancesTransfer payload:
{
"network": "ENJIN",
"chain": "RELAY",
"uuid": "f2342ed4-3504-4f81-b4b5-315635283652",
"publicKey": "0xa7208d10c6622f3f7eca1551de8355fde9de577dbb308d38994ace561738a51f"
}
The same notification returned by GetPendingEvents(network: ENJIN, chain: RELAY) has this shape:
{
"uuid": "f2342ed4-3504-4f81-b4b5-315635283652",
"event": "BalancesTransfer",
"payload": {
"network": "ENJIN",
"chain": "RELAY",
"publicKey": "0xa7208d10c6622f3f7eca1551de8355fde9de577dbb308d38994ace561738a51f"
}
}
The inbox returns the delivery UUID separately from payload. Its event is the exact Pusher name, without the dots or spaces used for on-chain event logs. These notifications identify what changed; query the relevant account, token, listing, or pool for its current state.
Platform events
The following lifecycle notifications are delivered over WebSocket and are separate from the on-chain notifications retained in Event Inbox. In TransactionCreated and TransactionStateChanged, uuid identifies the transaction for GetTransaction, rather than an inbox delivery to acknowledge.
Event payloads are deliberately lightweight: they carry just enough to tell you what changed, and you fetch whatever else you need with a follow-up GraphQL query. For example, when TransactionStateChanged reports FINALIZED, query GetTransaction(uuid:) to read the on-chain outcome and emitted events — see Working with Events for that flow.
| Event | Fires when |
|---|---|
TransactionCreated | A transaction is created on the platform |
TransactionStateChanged | A transaction moves to a new lifecycle state |
DaemonConnectionStatusChanged | Your Wallet Daemon's connection to the platform changes |
ManagedWalletRequested | A managed wallet creation request is received |
ManagedWalletCreated | A managed wallet's keypair has been derived |
WalletLinked | A user's wallet completes linking to your account |
Account addresses in event payloads are formatted as public keys (0x...), not SS58-encoded addresses. The platform accepts public keys anywhere an address argument is expected, so you can pass them straight back into queries and mutations.
TransactionCreated
Fired when a transaction is created on your account — whether by your application, the Platform UI, or another API client.
{
"uuid": "575209c5-6eda-4d8f-82c3-9a7f71249817"
}
Follow up with GetTransaction(uuid:) for the transaction's action, state, and details.
TransactionStateChanged
Fired every time a transaction moves to a new lifecycle state — PENDING → BROADCAST → FINALIZED, or a terminal failure state. This replaces polling GetTransaction for state: drive your "pending → confirmed" UI straight off these events, and when the state reaches FINALIZED, fetch the emitted on-chain events as described in Working with Events.
{
"uuid": "0af09287-30ff-43c9-a13d-75ad1ea714c3",
"state": "FINALIZED"
}
DaemonConnectionStatusChanged
Fired when your project's Wallet Daemon connects to or disconnects from the platform. Useful for alerting: while the daemon is offline, transactions queue up unsigned.
{
"status": "ONLINE"
}
ManagedWalletRequested
Fired when a CreateManagedWallet request is received, before the Wallet Daemon has derived the keypair.
{
"externalId": "example123"
}
ManagedWalletCreated
Fired once the Wallet Daemon has derived the managed wallet's keypair — the point at which the wallet is usable. The payload already carries the wallet's public key, so no GetManagedWallet lookup is needed.
{
"externalId": "example123",
"publicKey": "0x8faddcca50c311c6eb3d04ecfedf2ae30c60686cfce8addf55a1013ef706db23"
}
WalletLinked
Fired when an end user completes linking their wallet to your account (see Sending Wallet Requests for the flow). The idempotencyKey identifies which CreateLinkingCode request was fulfilled, and publicKey is the wallet the user linked — the moment to associate that address with the user's record in your database, with no need to poll for the link.
{
"idempotencyKey": "287a471e-c868-418c-90a2-2d6f5e993ed4",
"publicKey": "0xd542de6771d0c6f9bc70600c94b6747f280cda654cdbe3e92a7f0c6e94c25fa3"
}