Skip to content

HomeDocsPairing events

Updated 2026-10-23 · applies to a2a-exposed 0.8.11

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.