Pairing events
When EVENT_URL and EVENT_SECRET are set, the agent POSTs a JSON event after a pairing request is created and after it is approved or denied. On the dashboard host that URL is https://<dashboard-host>/api/pairing-events. The navbar count is the open-request total from the latest accepted event.
A pairing code lasts 60 minutes. expiresAt is that deadline.
What the agent sends
The JSON body uses these fields, in this order. decision is present when type is pairing_decided and the value is approved or denied.
| Field | Meaning |
|---|---|
type |
pairing_requested or pairing_decided |
agent |
Public host from the agent URL, or the tenant id |
requestId |
device_hash, the hash of the device code |
code |
The code a person reads, such as WDJB-4827 |
name |
client_name from the device request. An empty string when it is absent |
alias |
client_id from the device request. An empty string when it is absent |
ip |
Client IP. A decided event whose row leaves the IP blank sends an empty string |
country |
Cloudflare cf.country. An empty string when it is absent |
createdAt |
When the request was created, ISO 8601 |
expiresAt |
When the code expires, ISO 8601 |
pending |
Count of pending requests still inside the 60-minute window, read when the event is sent |
decision |
approved or denied. Only on pairing_decided |
A request that just opened:
{
"type": "pairing_requested",
"agent": "northwind.a2a.exposed",
"requestId": "device-hash",
"code": "WDJB-4827",
"name": "Support",
"alias": "support",
"ip": "203.0.113.10",
"country": "DE",
"createdAt": "2026-10-16T12:00:00.000Z",
"expiresAt": "2026-10-16T13:00:00.000Z",
"pending": 1
}
A decided event is the same object with "type": "pairing_decided" and "decision": "approved" or "decision": "denied" after pending.
Signature
x-a2a-timestamp is unix seconds. x-a2a-signature is sha256= plus the HMAC-SHA256 hex of the timestamp, a dot, and the raw JSON body. The key is EVENT_SECRET.
import { createHmac } from "node:crypto";
const event = {
type: "pairing_requested",
agent: "northwind.a2a.exposed",
requestId: "device-hash",
code: "WDJB-4827",
name: "Support",
alias: "support",
ip: "203.0.113.10",
country: "DE",
createdAt: "2026-10-16T12:00:00.000Z",
expiresAt: "2026-10-16T13:00:00.000Z",
pending: 1,
};
const secret = process.env.EVENT_SECRET;
const body = JSON.stringify(event);
const timestamp = String(Math.floor(Date.now() / 1000));
const signature = `sha256=${createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex")}`;
The POST sends content-type: application/json, x-a2a-timestamp, and x-a2a-signature. The receiver replies HTTP 204.
The dashboard stores the count when the timestamp is within 5 minutes and the signature matches a known agent. An unsigned, bad-signature, stale, replayed, or unknown-agent event leaves the count unchanged.
An accepted event updates the open dashboard. The account's tabs receive the new total on a WebSocket at /app/pending-live, and the navbar badge, the tab title, the agent cards, and a visible pairing list follow it. Events reach open tabs instantly. Focusing the window, or returning to the tab, reads GET /app/pending-count and updates the badge. That read uses the signed-in session. It waits for an agent that does not send events, then returns the stored total. An agent that already sends events skips that wait.
Approval itself is on approving a request.