Start an interactive pairing request
const url = 'https://example.com/api/plugins/pairing';const options = { method: 'POST', headers: {'Content-Type': 'application/json'}, body: '{"pluginId":"example","displayName":"example","codeChallenge":"example","codeChallengeMethod":"example","client":{"executablePath":"example","processId":1,"sdkVersion":"example"}}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://example.com/api/plugins/pairing \ --header 'Content-Type: application/json' \ --data '{ "pluginId": "example", "displayName": "example", "codeChallenge": "example", "codeChallengeMethod": "example", "client": { "executablePath": "example", "processId": 1, "sdkVersion": "example" } }'A plugin creates a short-lived PKCE-style pairing request over loopback, unauthenticated - the request itself is what a plugin has instead of a credential. The desktop UI approves or rejects it out of band; the plugin polls GET …/{requestId} and then redeems it.
Request Bodyrequired
Section titled “Request Bodyrequired”Mirrors MacroDeck.Plugin.Protocol.Handshake.PluginPairingRequest.
object
Reverse-domain package id, validated with MacroDeckId.IsValidOwnerId.
PKCE code challenge, derived from the plugin’s code verifier.
Mirrors MacroDeck.Plugin.Protocol.Handshake.PluginPairingChallengeMethods. Currently only “S256”.
Mirrors MacroDeck.Plugin.Protocol.Handshake.PluginPairingClientInfo. Every field is self-reported by the requesting process and is not verified by the host.
object
Examplegenerated
{ "pluginId": "example", "displayName": "example", "codeChallenge": "example", "codeChallengeMethod": "example", "client": { "executablePath": "example", "processId": 1, "sdkVersion": "example" }}Responses
Section titled “Responses”The pairing request was created.
Mirrors MacroDeck.Plugin.Protocol.Handshake.PluginPairingResponse.
object
Identifies the request but is not a credential and authorises nothing - it is safe in a URL, and the verifier is what proves possession.
Examplegenerated
{ "requestId": "example", "expiresAt": "2026-04-15T12:00:00Z", "pollIntervalSeconds": 1}The request body does not match the expected shape.
Mirrors MacroDeck.Plugin.Protocol.Errors.ProtocolError.
object
One of the codes v1 speaks. Drift anchor - see x-macrodeck-error-codes above.
Default English text keyed by code (MacroDeck.Plugin.Protocol.Errors.ProtocolErrorMessages). The UI localises from the code, not this string.
object
Example
{ "code": "PROTOCOL_VERSION_UNSUPPORTED"}The caller did not arrive on a loopback remote address. Plugin endpoints are served on both the public and private listeners, but only local processes may reach them. On the pairing endpoints, on POST /api/plugins/registration, and on POST /api/plugins/sessions for a plugin holding a development credential, this status also means Developer Mode is switched off in the desktop app - such a refusal carries “reason”: “developer_mode_disabled” in the error’s details, and GET /api/plugins/protocol reports the switch ahead of time.
Mirrors MacroDeck.Plugin.Protocol.Errors.ProtocolError.
object
One of the codes v1 speaks. Drift anchor - see x-macrodeck-error-codes above.
Default English text keyed by code (MacroDeck.Plugin.Protocol.Errors.ProtocolErrorMessages). The UI localises from the code, not this string.
object
Example
{ "code": "PROTOCOL_VERSION_UNSUPPORTED"}A plugin is already registered with this identity.
Mirrors MacroDeck.Plugin.Protocol.Errors.ProtocolError.
object
One of the codes v1 speaks. Drift anchor - see x-macrodeck-error-codes above.
Default English text keyed by code (MacroDeck.Plugin.Protocol.Errors.ProtocolErrorMessages). The UI localises from the code, not this string.
object
Example
{ "code": "PROTOCOL_VERSION_UNSUPPORTED"}Too many requests.
Mirrors MacroDeck.Plugin.Protocol.Errors.ProtocolError.
object
One of the codes v1 speaks. Drift anchor - see x-macrodeck-error-codes above.
Default English text keyed by code (MacroDeck.Plugin.Protocol.Errors.ProtocolErrorMessages). The UI localises from the code, not this string.
object
Example
{ "code": "PROTOCOL_VERSION_UNSUPPORTED"}