Tenant REST API
The Tenant REST API gives you direct HTTP access to your TrainAR account.
The Tenant REST API gives you direct HTTP access to your TrainAR account. All endpoints are available at the base URL https://api.trainar.ai/v1/ and authenticated with a tak_* API key.
Every request must include:
Authorization: Bearer tak_your_key_here
Content-Type: application/json
Responses are always JSON. Errors follow a consistent shape:
{ "error": "ERROR_CODE", "message": "Human-readable description" }
Tasks
Tasks are operational work items that can be created manually, via integrations, or via the API. The task lifecycle is open -> in_progress -> completed (or cancelled).
Create a task
POST /api-tenant-tasks
Required scope: write:tasks
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | Task title |
description |
string | No | Longer description or notes |
priority |
string | No | low, medium (default), high, or urgent |
due_date |
ISO-8601 string | No | Target completion date |
assigned_to |
UUID | No | User ID within your tenant to assign the task to |
external_id |
string | No | Your system's reference ID (for matching records) |
Response: 201 Created
{
"id": "uuid",
"title": "Service boiler at 12 Elm Street",
"description": null,
"status": "open",
"priority": "medium",
"source": "api",
"external_id": "JOB-4921",
"assigned_to": { "user_id": "uuid", "name": "Jordan Smith" },
"due_date": "2026-06-01T00:00:00.000Z",
"created_at": "2026-05-18T10:00:00.000Z",
"completed_at": null,
"session_id": null
}
Example:
curl -X POST \
https://api.trainar.ai/v1/api-tenant-tasks \
-H "Authorization: Bearer tak_your_key_here" \
-H "Content-Type: application/json" \
-d '{"title":"Service boiler at 12 Elm Street","external_id":"JOB-4921","priority":"high"}'
Error codes:
| Status | Code | Cause |
|---|---|---|
| 400 | BAD_REQUEST |
Missing title, invalid priority, or assigned_to user not in tenant |
| 403 | INSUFFICIENT_PERMISSIONS |
Key lacks write:tasks scope |
Creating a task fires a task.created webhook to any subscribed endpoints.
List tasks
GET /api-tenant-tasks
Required scope: read:tasks
Query parameters:
| Parameter | Type | Description |
|---|---|---|
status |
string | Filter by status: open, in_progress, completed, cancelled |
source |
string | Filter by source: manual, api, integration |
assigned_to |
UUID | Filter by assigned user |
from |
ISO-8601 string | Created at or after this timestamp |
to |
ISO-8601 string | Created at or before this timestamp |
limit |
integer | Max results per page, 1-100 (default 50) |
offset |
integer | Pagination offset (default 0) |
Response: 200 OK
{
"tasks": [ /* array of task objects, same shape as create response */ ],
"total": 142,
"limit": 50,
"offset": 0
}
Results are ordered by created_at descending (newest first).
Example:
curl "https://api.trainar.ai/v1/api-tenant-tasks?status=open&limit=20" \
-H "Authorization: Bearer tak_your_key_here"
Update a task
PATCH /api-tenant-tasks?task_id=<uuid>
Required scope: write:tasks
Query parameter:
| Parameter | Required | Description |
|---|---|---|
task_id |
Yes | UUID of the task to update |
Request body (all fields optional -- send only what you want to change):
| Field | Type | Description |
|---|---|---|
title |
string | New title |
description |
string | Updated description |
status |
string | New status: open, in_progress, completed, cancelled |
priority |
string | low, medium, high, urgent |
due_date |
ISO-8601 string | Updated due date |
assigned_to |
UUID or null | Reassign or unassign |
Response: 200 OK -- same task object shape as create.
Example:
curl -X PATCH \
"https://api.trainar.ai/v1/api-tenant-tasks?task_id=abc-123" \
-H "Authorization: Bearer tak_your_key_here" \
-H "Content-Type: application/json" \
-d '{"status":"in_progress"}'
Error codes:
| Status | Code | Cause |
|---|---|---|
| 400 | BAD_REQUEST |
Invalid field value or missing task_id |
| 404 | NOT_FOUND |
Task not found or belongs to a different tenant |
If the status changes, a task.status_changed webhook fires. If the new status is completed, a task.completed webhook fires in addition.
Sessions
Read session history and update session-level metadata. Sessions themselves are created by the on-glasses Core App; this API is for retrieval and limited post-hoc edits.
List sessions
GET /api-tenant-sessions
Required scope: read:sessions
Query parameters:
| Parameter | Type | Description |
|---|---|---|
status |
string | Filter by status (e.g. completed, in_progress, failed) |
role |
string | Filter by session role: trainer or trainee |
from |
ISO-8601 string | Started at or after this timestamp |
to |
ISO-8601 string | Started at or before this timestamp |
limit |
integer | 1-100 (default 50) |
offset |
integer | Pagination offset (default 0) |
Response: 200 OK
Returns a paginated list of sessions with metadata (name, status, role, duration, started/completed timestamps, video URL, procedure count). Use the Tenant MCP get_session tool for the full per-session detail including summary and procedures.
Update a session
PATCH /api-tenant-sessions?session_id=<id>
Required scope: write:sessions (not exposed in the standard API-key UI — contact us if you need this for an integration).
Allowed body fields: session_name, session_description, session_summary, duration_seconds, procedure_count (alias: procedures_count), video_url, thumbnail_url. All optional — send only what changes.
Error codes:
| Status | Code | Cause |
|---|---|---|
| 400 | BAD_REQUEST |
Missing/invalid session_id or body field |
| 400 | CONFLICTING_ALIASES |
Both procedure_count and procedures_count sent with different values |
| 404 | NOT_FOUND |
Session doesn't exist in your tenant |
| 405 | METHOD_NOT_ALLOWED |
Only GET and PATCH are accepted |
Users
Invite a user
POST /api-tenant-users
Required scope: manage:users
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | Email address for the invitation |
full_name |
string | Yes | Display name |
roles |
string[] | Yes | Array of roles. Currently accepts ["admin"]. Trainer/trainee assignment happens via seat assignment after the user accepts their invitation. |
Response: 201 Created
{
"success": true,
"user_id": "uuid",
"email": "jordan@example.com",
"full_name": "Jordan Smith",
"roles": ["admin"],
"message": "User invited successfully"
}
An invitation email is sent to the address automatically. The user sets their password on first login.
Example:
curl -X POST \
https://api.trainar.ai/v1/api-tenant-users \
-H "Authorization: Bearer tak_your_key_here" \
-H "Content-Type: application/json" \
-d '{"email":"jordan@example.com","full_name":"Jordan Smith","roles":["admin"]}'
Error codes:
| Status | Code | Cause |
|---|---|---|
| 400 | BAD_REQUEST |
Missing or invalid fields |
| 403 | FORBIDDEN |
Key lacks manage:users scope |
| 409 | CONFLICT |
Email already exists in this tenant, is deactivated (use reactivate endpoint), or belongs to another organisation |
List users
GET /api-tenant-users
Required scope: read:users
Query parameters:
| Parameter | Type | Description |
|---|---|---|
role |
string | Filter by role: trainer, trainee, tenant_admin |
status |
string | Filter by status: active, inactive |
limit |
integer | 1-100 (default 50) |
offset |
integer | Pagination offset (default 0) |
Response: 200 OK
{
"users": [
{
"id": "uuid",
"email": "jordan@example.com",
"full_name": "Jordan Smith",
"role": "trainer",
"status": "active",
"seat": {
"id": "uuid",
"device_name": "HoloLens-01",
"device_token_set": true
},
"last_session_at": "2026-05-17T14:30:00.000Z",
"total_sessions": 12,
"total_minutes": 186,
"created_at": "2026-03-01T09:00:00.000Z"
}
],
"total": 8,
"limit": 50,
"offset": 0
}
seat is null if no seat has been assigned to the user yet.
Skills
Execute a skill
POST /api-tenant-skills-execute
Required scope: write:skills
Executes a TrainAR skill server-to-server. The skill runs against your tenant's knowledge base. If you do not provide a session_context, the endpoint auto-resolves one from the first active assigned seat in your tenant.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
skill_id |
UUID | Yes | ID of the skill to execute. Find IDs in Dashboard → Skills or via the MCP list_skills tool. |
arguments |
object | Yes | Input parameters the skill expects (shape varies per skill) |
session_context |
object | No | Optional session context override (see below) |
session_context fields (all optional if auto-resolving):
| Field | Type | Description |
|---|---|---|
seat_id |
UUID | Seat to associate the execution with |
user_id |
UUID | User running the skill |
role |
string | trainer or trainee |
session_id |
UUID | Link to an open session |
Response: 200 OK -- shape varies by skill.
Example:
curl -X POST \
https://api.trainar.ai/v1/api-tenant-skills-execute \
-H "Authorization: Bearer tak_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"skill_id": "skill-uuid-here",
"arguments": { "query": "How do I bleed a radiator?" }
}'
Error codes:
| Status | Code | Cause |
|---|---|---|
| 400 | BAD_REQUEST |
Missing skill_id or arguments, or no active seat to auto-resolve |
| 400 | DISABLED |
Skill exists but is inactive |
| 403 | NOT_ENTITLED |
Skill is not in your tenant's bundle allocation |
| 404 | NOT_FOUND |
Skill ID does not exist |
Webhooks
Webhook subscription is managed through the same API. Rather than repeating the full reference here, see Webhooks for the complete guide including the subscribe, check, and delete endpoints, payload shapes, and HMAC signature verification.
Quick reference:
| Method | Path | Scope | Action |
|---|---|---|---|
POST |
/webhook-subscribe |
manage:webhooks |
Subscribe a new endpoint |
GET |
/webhook-subscribe?endpoint_id=<id> |
manage:webhooks |
Check whether an endpoint exists |
DELETE |
/webhook-subscribe?endpoint_id=<id> |
manage:webhooks |
Unsubscribe an endpoint |
Common error responses
| Status | Code | Meaning |
|---|---|---|
| 400 | BAD_REQUEST |
Validation error — see message for the specific field |
| 401 | MISSING_AUTH / INVALID_KEY |
No Authorization header, malformed header, or key not found |
| 403 | INSUFFICIENT_PERMISSIONS / NOT_ENTITLED |
Valid key but insufficient scope, or skill not in tenant's bundle allocations |
| 404 | NOT_FOUND |
Record does not exist or belongs to another tenant |
| 405 | METHOD_NOT_ALLOWED |
Wrong HTTP method for the endpoint |
| 409 | CONFLICT |
Duplicate record (invite endpoint) |
| 500 | INTERNAL_ERROR |
Server-side error — retry with backoff |
All error bodies use the same shape: { "error": "CODE", "message": "Human-readable description" }. There are no code, field, or trace_id sub-fields.