Build your own integration
If Zapier and n8n don't cover what you need — or you're building a deep custom integration — TrainAR gives you two primitives to work with directly: webhooks t…
If Zapier and n8n don't cover what you need — or you're building a deep custom integration — TrainAR gives you two primitives to work with directly: webhooks to listen for events, and the Tenant REST API to make calls.
This page covers webhooks. For the full REST API, see the Developer section.
The event taxonomy
TrainAR fires the following events. Each is delivered as an HTTP POST to a URL you register.
| Event | Description |
|---|---|
task.created |
A new task was created — manually, via the API, or by an integration. |
task.status_changed |
A task's status moved between open / in_progress / completed / cancelled. |
task.completed |
A task was marked completed (a special case of task.status_changed). |
session.started |
An engineer began a training session on their glasses. |
session.completed |
A training session ended — payload includes score, duration, video URL, AI summary. |
skill.executed |
A skill was executed on the glasses (success or failure). |
The delivery envelope
Every event POST has the same outer shape:
{
"event": "task.completed",
"event_id": "evt-...uuid...",
"timestamp": "2026-05-18T10:00:00.000Z",
"data": { /* event-specific fields */ }
}
The event_id is idempotent — if the same logical event is delivered more than once (e.g. after a retry), the event_id will be identical. We recommend storing event_id on your side and ignoring duplicates.
Per-event payload shapes
task.created
{
"task_id": "uuid",
"title": "string",
"status": "open|in_progress|completed|cancelled",
"source": "string",
"external_id": "string|null",
"assigned_to": "uuid|null",
"created_at": "ISO-8601 string"
}
task.status_changed
{
"task_id": "uuid",
"title": "string",
"previous_status": "string",
"new_status": "string",
"external_id": "string|null",
"source": "string",
"session_id": "uuid|null",
"changed_by": "session|user|integration|api",
"changed_at": "ISO-8601 string"
}
task.completed — same fields as task.status_changed plus:
{
"completion_source": "session|user|integration|api",
"completed_at": "ISO-8601 string",
"session_summary": { /* see session.completed */ } | null
}
session.started
{
"session_id": "uuid",
"user_id": "uuid",
"user_name": "string",
"seat_id": "uuid",
"session_name": "string",
"video_url": "string",
"started_at": "ISO-8601 string"
}
session.completed
{
"session_id": "uuid",
"user_id": "uuid",
"user_name": "string",
"status": "string",
"started_at": "ISO-8601 string",
"ended_at": "ISO-8601 string",
"duration_minutes": 42,
"minutes_consumed": 42,
"video_url": "string",
"task_id": "uuid|null",
"task_title": "string|null",
"session_summary": { /* prose, score, notes */ } | null
}
skill.executed
{
"execution_id": "uuid",
"skill_id": "uuid",
"skill_name": "string",
"session_id": "uuid|null",
"user_id": "uuid|null",
"status": "success|failure",
"response_target": "string",
"duration_ms": 1234,
"executed_at": "ISO-8601 string"
}
Subscribing to events
Subscribe by making a POST to TrainAR's webhook endpoint with an API key that has the manage:webhooks scope.
POST https://api.trainar.ai/v1/webhook-subscribe
Authorization: Bearer tak_your_key_here
Content-Type: application/json
{
"url": "https://your-endpoint.example.com/trainar",
"events": ["task.completed", "session.completed"]
}
The response includes the endpoint id and a secret — a 64-character hex string used to sign every delivery. The secret is shown only once, so capture it immediately. If you lose it, generate a new endpoint.
Verifying signatures
Every delivery includes two headers:
X-Webhook-Timestamp— Unix timestamp at dispatch (seconds, string).X-Webhook-Signature— HMAC-SHA256 hex digest of"<timestamp>.<raw-body>"signed with the endpoint's secret.
Verify like this in Python:
import hmac, hashlib
def verify(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
expected = hmac.new(
secret.encode(),
f"{timestamp}.".encode() + body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
Reject any request whose signature doesn't match. Also reject requests where the timestamp is more than five minutes old — this defends against replay attacks.
Delivery model
- Max 3 attempts per event. The first attempt fires immediately; the second after ~10 seconds + jitter; the third after ~60 seconds + jitter.
- Per-attempt timeout: 15 seconds. Return a 2xx status within 15 seconds or the attempt is treated as failed.
- Endpoint disabling. Persistent failures (many events failing all 3 attempts) automatically disable the endpoint and email the tenant admin. Reactivate from the Dashboard.
What we won't deliver to
For security, TrainAR refuses to deliver to URLs that resolve to private IP ranges (RFC 1918), loopback, or link-local addresses. If you need to receive events on a private network, terminate at a public-facing reverse proxy first.
Listing + managing endpoints
Use the Tenant REST API:
GET /webhook-subscribe?endpoint_id=<id>— check whether an endpoint exists.DELETE /webhook-subscribe?endpoint_id=<id>— unsubscribe.
Or use Dashboard → Settings → API & Webhooks to manage endpoints from the UI.
Next steps
- Developer overview — the full Tenant REST API + Tenant MCP server reference.
- n8n integration — if your needs map onto n8n triggers and actions, this is faster than rolling your own.
- Zapier integration — same applies if you'd rather wire events through Zapier.