Optional local RPC client
Discord\RPC\IpcClient
is an optional, low-level client for Discord's
local IPC protocol. Construct and connect it explicitly in a CLI application.
Creating a bot client does not start RPC, and RPC does not start a bot or connect
to the Gateway. It uses the ReactPHP event loop and returns promises for
asynchronous operations.
This foundation provides framing, a handshake, command correlation and raw event delivery. It does not provide typed command payload builders or typed RPC domain objects. RPC responses can differ from HTTP API objects; do not assume that a raw RPC response satisfies a DiscordPHP Part's contract.
Transport and platforms
On Unix-like systems, discovery considers discord-ipc-0
through
discord-ipc-9
under each configured directory, in this order:
XDG_RUNTIME_DIR
, TMPDIR
, TMP
, TEMP
, then /tmp
.
The default connector uses asynchronous Unix sockets.
Windows is unsupported by the default connector. Applications may inject a
React\Socket\ConnectorInterface
that asynchronously returns a
React\Socket\ConnectionInterface
for native paths such as
\\?\pipe\discord-ipc-0
. No native Windows pipe adapter, TCP bridge or
process proxy is shipped. Deprecated RPC over WebSocket is outside this module.
Path discovery and an injected stream fixture do not establish that the module
works with an installed Discord client on any platform.
Connecting and sending commands
Create the client on the loop owned by your CLI application. Register listeners
before connecting; the connection promise resolves only after READY
.
use Discord\RPC\IpcClient;
use React\EventLoop\Loop;
$loop = Loop::get();
$rpc = new IpcClient($loop);
$rpc->on('dispatch', function (string $event, $data, array $raw) {
// Handle a raw RPC dispatch; validate the event's data in your application.
});
$rpc->on('error', function (\Throwable $error) {
// Record an operational error without logging tokens or authorization codes.
});
$rpc->connect('YOUR_NUMERIC_APPLICATION_ID')->then(
function (IpcClient $rpc) {
// IPC is ready; authorization and authentication are separate steps.
},
function (\Throwable $error) {
// Connection or handshake failed.
}
);
$loop->run();
After authenticating with the necessary scopes and eligibility, use
request($command, $args, $event, $timeout)
. It returns the complete raw
response envelope, including cmd
, nonce
and data
where present.
The library supplies the nonce and correlates replies. The caller owns each
command's arguments and response interpretation.
$rpc->request('GET_GUILD', ['guild_id' => $guildId])->then(
function (array $response) {
$guildData = $response['data'];
}
);
$rpc->request('SUBSCRIBE', ['channel_id' => $channelId], 'MESSAGE_CREATE');
$rpc->request('UNSUBSCRIBE', ['channel_id' => $channelId], 'MESSAGE_CREATE');
SUBSCRIBE
and UNSUBSCRIBE
use the same command correlation mechanism.
There is no local subscription registry or automatic subscription replay.
Reconnect explicitly after a disconnect, then authenticate and restore any
subscriptions your application still needs. Pending commands are rejected on
disconnect. close()
closes the session; sendPing($payload)
sends a
protocol ping and does not constitute a platform health check.
Authorization and confidential token exchange
authorize(['rpc', 'identify'])
sends AUTHORIZE
and returns its raw
response. authenticate($accessToken)
sends AUTHENTICATE
. These helpers
do not make an application eligible for restricted scopes. Discord documents
approval and tester restrictions; verify access with Discord for your own app.
A bot token is not an RPC OAuth2 access token.
authorizeAndAuthenticate($scopes, $redirectUri, $exchange, $timeout)
chains
authorization, an injected asynchronous token exchange, and authentication.
Only one such flow may be active per client; concurrent flows reject. Call
close()
when finished to release the owned connection and deadlines.
Implement Discord\RPC\TokenExchangeInterface
:
public function exchange(
string $code,
string $clientId,
string $redirectUri
): \React\Promise\PromiseInterface;
The promise must resolve to a nonempty access-token string. Host the confidential OAuth2 exchange securely in infrastructure you control. Never ship an application client secret in a distributed desktop client. This module does not implement the token endpoint, store tokens, refresh tokens, or ship a client secret. Your exchange implementation owns transport, server authentication, redirect validation and token handling. Keep authorization codes and access tokens out of logs.
API reference
All timeout values below are seconds.
new IpcClient(?LoopInterface $loop = null, ?ConnectorInterface $connector = null, array $options = []);
connect(string $clientId, int $version = 1): PromiseInterface;
request(string $command, array $args = [], ?string $event = null, float $timeout = 15): PromiseInterface;
authorize(array $scopes = ['rpc', 'identify'], float $timeout = 15): PromiseInterface;
authenticate(string $accessToken, float $timeout = 15): PromiseInterface;
authorizeAndAuthenticate(array $scopes, string $redirectUri, TokenExchangeInterface $exchange, float $timeout = 15): PromiseInterface;
close(): void;
sendPing(string $payload = ''): void;
Listeners receive ready
(data, raw READY envelope), dispatch
(event name,
data, raw envelope), error
(Throwable
), and close
(nullable
CloseException
). pong
receives the payload bytes. READY
and ERROR
are handled by the lifecycle and error paths rather than ordinary dispatch
notifications. Command failures reject the request promise; handle its
rejection even when you have installed an error
listener.
The constructor accepts these resource and scheduling options:
| Option | Default | Meaning |
|---|---|---|
paths
|
IpcClient::getCandidatePaths()
|
Ordered local Unix socket URIs or Discord native pipe paths. |
connect_timeout
|
10.0
|
Overall connection and READY deadline in seconds. |
max_frame_bytes
|
1048576
|
Maximum payload bytes per frame. |
max_buffer_bytes
|
2097168
|
Maximum buffered incoming bytes; at least frame limit plus eight. |
max_pending_requests
|
128
|
Maximum concurrent pending commands. |
max_queue_bytes
|
4194304
|
Maximum buffered outgoing bytes. |
frames_per_tick
|
32
|
Per-tick frame processing budget. |
Unknown options and invalid limits are rejected. Request deadlines include queue time. Cancelling an unsent request removes it from the queue; cancelling a command already sent cannot undo its effect. The injected token exchange must support cancellation so a deadline or disconnect can abort pending exchange work. Use positive finite timeout values.
Coverage and verification boundaries
scripts/rpc-coverage.json
inventories the documented command and event names.
The audit dated 2026-10-20 counts 18 client commands and 25 events, plus the
server DISPATCH
envelope: 44 named entries. SELECT_TEXT_CHANNEL
and
ACTIVITY_INVITE
are included. The inventory records raw envelope support,
convenience helpers, absent typed wrappers, and fixture evidence separately.
It is not a full-support percentage or an assertion that all commands work
against Discord.
Offline stream fixtures can verify framing, handshakes, correlation, timeouts,
disconnect behavior and dispatch forwarding. tests/RPC/IpcSocketTest.php
also
exercises the default connector and real event loop against a temporary local
Unix socket server: path fallback, fragmented READY, binary PING/PONG,
out-of-order replies and shutdown when READY never arrives. These tests skip
where PHP has no Unix socket transport, including the Windows CI environment.
They cannot prove compatibility with an installed Discord client, native Windows
pipe support, user consent, app approval, scope eligibility, or command-specific
behavior on Discord. No live authentication or live command integration is claimed.
Sources
The inventory was reconciled against the official references on 2026-10-20:
- RPC for IPC, payloads, commands, events and authorization.
- Opcodes and status codes for RPC errors and close codes. Error and close code namespaces are distinct.
- OAuth2 scopes for scope names and approval restrictions.
The RPC RELATIONSHIP_UPDATE
prose names relationships_read
, while the
OAuth2 scope table names relationships.read
. The inventory preserves this
source discrepancy; it does not silently infer which spelling the live RPC
server accepts. New projects should also assess Discord's recommended Social
SDK against their requirements.