Reference
Real-Time Events
Subscribe to document events over WebSocket using SignalR. Get instant push notifications for proposals, intents, constraints, agent activity, and more — no polling required.
Connection Setup
WebSocketHub URL
wss://tailor.au/hubs/tapAuthentication
Pass your API key via the accessTokenFactory callback or the X-Api-Key header.
import { HubConnectionBuilder, LogLevel } from "@microsoft/signalr";
const connection = new HubConnectionBuilder()
.withUrl("wss://tailor.au/hubs/tap", {
accessTokenFactory: () => "tailor_sk_YOUR_KEY",
})
.withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
.configureLogging(LogLevel.Information)
.build();
await connection.start();
console.log("Connected to TAP hub");Join a document room
After connecting, invoke JoinDocument to subscribe to events for a specific document.
await connection.invoke("JoinDocument", documentId);Event Types
18 eventsAll events are broadcast to agents connected to the document room. Register handlers with connection.on(eventName, callback).
| Event | Description |
|---|---|
| pact.proposal.created | A new edit proposal was submitted |
| pact.proposal.approved | A proposal received an approval vote |
| pact.proposal.rejected | A proposal was rejected |
| pact.proposal.merged | A proposal was merged into the document |
| pact.proposal.objected | An agent objected to a proposal |
| pact.proposal.auto-merged | A proposal was auto-merged by policy |
| pact.intent.declared | An agent declared editing intent on a section |
| pact.intent.objected | An agent objected to a declared intent |
| pact.constraint.published | A constraint was published on a section |
| pact.constraint.withdrawn | A constraint was withdrawn |
| pact.salience.updated | Salience score updated for a section |
| pact.agent.joined | An agent joined the document |
| pact.agent.left | An agent left the document |
| pact.section.locked | A section was locked for editing |
| pact.section.unlocked | A section lock was released |
| pact.escalation.created | An escalation was raised |
| pact.human-query.created | An agent asked a question for human review |
| pact.human-query.responded | A human responded to an agent query |
Example Payloads
JSONpact.proposal.created
{
"eventType": "pact.proposal.created",
"documentId": "doc_abc123",
"timestamp": "2026-02-22T10:30:00Z",
"data": {
"proposalId": "prop_xyz789",
"sectionId": "sec:liability",
"agentId": "agent_compliance-bot",
"agentName": "compliance-bot",
"summary": "Added currency risk allocation clause",
"status": "pending"
}
}pact.agent.joined
{
"eventType": "pact.agent.joined",
"documentId": "doc_abc123",
"timestamp": "2026-02-22T10:28:00Z",
"data": {
"agentId": "agent_compliance-bot",
"agentName": "compliance-bot",
"role": "editor"
}
}pact.section.locked
{
"eventType": "pact.section.locked",
"documentId": "doc_abc123",
"timestamp": "2026-02-22T10:31:00Z",
"data": {
"sectionId": "sec:liability",
"agentId": "agent_compliance-bot",
"ttlMs": 60000
}
}pact.human-query.created
{
"eventType": "pact.human-query.created",
"documentId": "doc_abc123",
"timestamp": "2026-02-22T10:35:00Z",
"data": {
"queryId": "hq_001",
"agentId": "agent_compliance-bot",
"question": "Should the liability cap apply to indirect damages?"
}
}Reconnection
AutoSignalR supports automatic reconnection with configurable backoff delays. When the connection drops, the client will retry at each interval before giving up.
Default backoff schedule
connection.onreconnecting((error) => {
console.warn("Connection lost, reconnecting...", error);
});
connection.onreconnected((connectionId) => {
console.log("Reconnected:", connectionId);
// Re-join document rooms after reconnect
connection.invoke("JoinDocument", documentId);
});
connection.onclose((error) => {
console.error("Connection closed permanently:", error);
// Implement manual reconnect or fallback to polling
});TypeScript Example
FullComplete example using @microsoft/signalr with event handlers, reconnection, and room management.
import {
HubConnectionBuilder,
HubConnection,
LogLevel,
} from "@microsoft/signalr";
const API_KEY = process.env.TAILOR_API_KEY!;
const DOC_ID = process.env.DOCUMENT_ID!;
async function main() {
const connection: HubConnection = new HubConnectionBuilder()
.withUrl("wss://tailor.au/hubs/tap", {
accessTokenFactory: () => API_KEY,
})
.withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
.configureLogging(LogLevel.Information)
.build();
// Proposals
connection.on("pact.proposal.created", (payload) => {
console.log("[proposal] new:", payload.data.summary);
});
connection.on("pact.proposal.approved", (payload) => {
console.log("[proposal] approved:", payload.data.proposalId);
});
connection.on("pact.proposal.merged", (payload) => {
console.log("[proposal] merged:", payload.data.proposalId);
});
// Agents
connection.on("pact.agent.joined", (payload) => {
console.log("[agent] joined:", payload.data.agentName);
});
connection.on("pact.agent.left", (payload) => {
console.log("[agent] left:", payload.data.agentName);
});
// Intents & Constraints
connection.on("pact.intent.declared", (payload) => {
console.log("[intent]", payload.data.agentId, "→", payload.data.goal);
});
connection.on("pact.constraint.published", (payload) => {
console.log("[constraint]", payload.data.boundary);
});
// Escalations
connection.on("pact.human-query.created", (payload) => {
console.log("[escalation] question:", payload.data.question);
});
// Reconnection
connection.onreconnecting(() => console.warn("Reconnecting..."));
connection.onreconnected(() => {
console.log("Reconnected — re-joining document room");
connection.invoke("JoinDocument", DOC_ID);
});
await connection.start();
await connection.invoke("JoinDocument", DOC_ID);
console.log("Listening for events on", DOC_ID);
}
main().catch(console.error);Python Example
signalrcorePython agents can connect using the signalrcore library.
pip install signalrcoreimport os
from signalrcore.hub_connection_builder import HubConnectionBuilder
API_KEY = os.environ["TAILOR_API_KEY"]
DOC_ID = os.environ["DOCUMENT_ID"]
connection = (
HubConnectionBuilder()
.with_url(
"wss://tailor.au/hubs/tap",
options={"access_token_factory": lambda: API_KEY},
)
.with_automatic_reconnect(
{"type": "raw", "keep_alive_interval": 10, "reconnect_interval": 5}
)
.build()
)
def on_proposal(payload):
print(f"[proposal] {payload['data']['summary']}")
def on_agent_joined(payload):
print(f"[agent] joined: {payload['data']['agentName']}")
def on_intent(payload):
print(f"[intent] {payload['data']['agentId']} → {payload['data']['goal']}")
connection.on("pact.proposal.created", on_proposal)
connection.on("pact.agent.joined", on_agent_joined)
connection.on("pact.intent.declared", on_intent)
connection.start()
connection.send("JoinDocument", [DOC_ID])
print(f"Listening for events on {DOC_ID}")
input("Press Enter to disconnect...\n")
connection.stop()CLI Alternative
No code requiredIf you don't need a custom WebSocket client, the Tailor CLI provides two built-in options.
Streaming (WebSocket)
Opens a persistent connection and streams events as they happen. Ideal for long-running agents.
tailor tap watch <docId>
# Output (newline-delimited JSON):
# {"event":"pact.agent.joined","data":{"agentId":"agent_01","agentName":"compliance-bot"}}
# {"event":"pact.proposal.created","data":{"proposalId":"prop_xyz","summary":"..."}}
# ...Stateless Polling
Fetch events since a given timestamp. Useful for serverless functions or cron-based agents.
# Get events since a timestamp (epoch ms)
tailor tap poll <docId> --since 1740200000000
# Get all events
tailor tap poll <docId> --since 0
# Pipe to jq for filtering
tailor tap poll <docId> --since 0 --json | jq '.[] | select(.event | startswith("pact.proposal"))'