Skip to content

Real-time events

Signing pages, kiosks and consoles receive live updates from an AWS AppSync Events API instead of polling: a participant viewed the document, signed, the signed PDF is ready. Real-time events are meant for screens people look at. For your back end, use webhooks: they are signed, retried and recorded, real-time events are best effort.

Channel Who subscribes Authorization
/cases/{tenantId}/{caseId}, or /cases/{tenantId}/* your console a tenant token; {tenantId} must be the token’s sub
/participants/{participantId} the signing page of a participant the participant token of the signing link (the apiToken parameter of signerUrl)
/devices/{tenantId}/{deviceId} a kiosk device the device token from POST /tenant/devices

Only TrustInk publishes. Every other channel is refused, as are wildcards on participant and device channels and any attempt to publish with a token. A tenant can subscribe to its own cases only.

<realtime host> and <http host> below are the hosts of the TrustInk events API of your environment.

  1. Open wss://<realtime host>/event/realtime with two subprotocols: aws-appsync-event-ws and header-<base64url JSON>, where the JSON is {"host": "<http host>", "Authorization": "<token>"}.

  2. Send {"type": "connection_init"} and wait for connection_ack.

  3. Subscribe:

    {"type": "subscribe", "id": "sub-1", "channel": "/participants/7a1c…",
    "authorization": {"host": "<http host>", "Authorization": "<token>"}}

    AppSync answers subscribe_success or subscribe_error.

  4. Events arrive as {"type": "data", "id": "sub-1", "event": "<JSON string>"}. Keep the connection while ka (keep-alive) messages keep coming, at least every 5 minutes.

const header = btoa(JSON.stringify({host: httpHost, Authorization: token}))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const socket = new WebSocket(`wss://${realtimeHost}/event/realtime`, ['aws-appsync-event-ws', `header-${header}`]);
socket.onopen = () => socket.send(JSON.stringify({type: 'connection_init'}));
socket.onmessage = ({data}) => {
const message = JSON.parse(data);
if (message.type === 'connection_ack') {
socket.send(JSON.stringify({
type: 'subscribe', id: 'sub-1', channel: `/cases/${tenantId}/${caseId}`,
authorization: {host: httpHost, Authorization: token},
}));
}
if (message.type === 'data') {
const event = JSON.parse(message.event);
console.log(event.type, event.data);
}
};

The protocol is AWS’s; see the AppSync Events WebSocket protocol for the details.

{"type": "participant.signed", "occurredAt": "2026-09-24T08:00:00Z", "tenantId": "…", "caseId": "…",
"participantId": "…", "data": {"signedAt": "2026-09-24T08:00:00.000Z"}}
type Channels When data
participant.invited case the case starts waiting for the participant {}
participant.viewed case the participant opened the document {}
participant.signed case, the participant’s the participant signed {signedAt}
case.revision.created case, the participant’s the signature is in the document’s next PAdES revision {revision, signatureCount}
case.completed case, every participant’s the case is done and the signed PDF ready {status: "DONE", signedDocumentAvailable}
case.expired case, every participant’s validUntil passed {status: "EXPIRED", signedDocumentAvailable: false}
case.cancelled case, every participant’s you cancelled the case {status: "CANCELLED", signedDocumentAvailable: false}
case.failed case, every participant’s the document could not be signed {status: "FAILED", signedDocumentAvailable: false}
device.open_case the device’s a kiosk case is ready for its next participant {caseId, participantId, signerUrl, title}

participantId is set on the participant.* and device.open_case events. No event carries a link to a document: a page that sees signedDocumentAvailable fetches the PDF with its own token (GET /participant/{participantId}/signedDocument). The signerUrl of device.open_case holds the participant’s token, which is why only the device’s own token may subscribe to its channel.

For the kiosk hand-off with device.open_case, see in-person signing.