API Reference
The XRAY Task Inbox API lets you create, read, update, and triage tasks, review AI meeting proposals, and query projects, clients, and meetings programmatically. All endpoints return JSON.
Authentication
Obtaining an API Key
API keys can be generated directly from the web interface:
- Sign in to the web app with your
@xray.tech account.
- Navigate to the Settings tab in the main navigation.
- Enter a name for your key (e.g. "Zapier Integration") and click Generate New Key.
- Copy the generated secret key. Keep it safe—it will not be shown again.
Note for Administrators: Global machine keys can still be hardcoded via the API_KEY_MAP environment variable if desired (format: key:email:Name).
Using Your API Key
Include the X-API-Key header in every request. Every /api/* endpoint accepts either the API key or a web session — there are no session-only API routes.
curl -H "X-API-Key: your-secret-key-here" \
-H "Content-Type: application/json" \
https://tasks.xray.tech/api/tasks
Web Authentication
Web users authenticate via Google OAuth. Sign in at the app root (/) with your @xray.tech account. The session cookie is sent automatically with browser requests.
Auth Check
GET /auth/me
Returns the current user or 401 if not authenticated. Session-cookie only — this is the one route that does not accept an API key.
// 200 OK
{ "user": { "email": "mark@xray.tech", "name": "Mark Campos" } }
// 401 Unauthorized
{ "error": "Not authenticated" }
Field Values
Tasks use fixed value sets. Unknown values are rejected with a 400 Validation failed response listing the valid options.
Status
Ready to Start | In Progress | Done | Cancelled | Someday/Maybe
Priority
P0: Urgent & Critical | P1: Top Priority | P2: Medium Priority | P3: Low Priority | P4: Lowest Priority
Friendly aliases are accepted on input and normalized to the canonical values above: p0–p4, urgent, critical, high, top, normal, medium, low, lowest (case-insensitive). Responses always contain the canonical value.
Triage Status
New | Accepted | Rejected | Needs More Info
Origin
Human | AI Agent | Automation
Tasks
GET /api/tasks
List inbox tasks — untriaged (Triage Status = "New"), not archived — scoped to the authenticated user.
Example
curl -H "X-API-Key: your-key" https://tasks.xray.tech/api/tasks
Response
{
"tasks": [
{
"id": "recXXXXXXXXXXXXXX",
"title": "Update onboarding docs",
"description": "The onboarding guide needs updating for new hires",
"status": "Ready to Start",
"priority": "P2: Medium Priority",
"triageStatus": "New",
"origin": "Human",
"originDetail": "Mark via Slack",
"triageNotes": "",
"assignedTo": { "id": "usr...", "email": "mark@xray.tech", "name": "Mark Campos" },
"requestedBy": null,
"assignedToAI": "mark@xray.tech",
"project": [],
"projectName": "",
"dueDate": null,
"startDate": null,
"hoursEstimated": null,
"hoursUsed": null,
"isArchived": false,
"linkToTaskInterface": "https://airtable.com/...",
"createdAt": "2026-03-10T14:30:00.000Z"
}
]
}
GET /api/tasks/mine
The user's active board: every task assigned to them that is not Done, Cancelled, Rejected, or archived. Tasks completed more than 30 days ago are excluded.
curl -H "X-API-Key: your-key" https://tasks.xray.tech/api/tasks/mine
GET /api/tasks/sent
Tasks the authenticated user requested for other people.
GET /api/tasks/search
Search tasks with server-side filters.
Parameters
| Param | Description |
activeOnly | true to restrict to active statuses and exclude archived/done |
clientName | Filter by client name |
GET /api/tasks/history
Recently resolved tasks for the current user. Optional ?days=30 (default 30).
GET /api/tasks/:id
Get a single task by its Airtable record ID.
{ "task": { "id": "recXXXXXXXXXXXXXX", "title": "...", ... } }
POST /api/tasks
Create a new inbox task. Sets Status = "Ready to Start" and Triage Status = "New" automatically.
Request Body
| Field | Type | Required | Notes |
title | string | Yes | Max 120 characters |
description | string | No | Max 500 characters |
origin | string | No | Default: Human. See Field Values |
originDetail | string | No | E.g., "Claude Code", "Zapier webhook" |
assignedTo | string | No | Email address. Defaults to authenticated user |
requestedBy | string | No | Email address. Defaults to authenticated user |
priority | string | No | Default: P2: Medium Priority. Canonical value or alias — see Field Values |
project | string | No | Airtable record ID of a project |
Example
curl -X POST \
-H "X-API-Key: your-key" \
-H "Content-Type: application/json" \
-d '{
"title": "Update client proposal",
"description": "Revise the SOW section with new pricing",
"origin": "AI Agent",
"originDetail": "Claude Code",
"priority": "P1: Top Priority",
"assignedTo": "mark@xray.tech"
}' \
https://tasks.xray.tech/api/tasks
Response (201 Created)
{ "ok": true, "task": { "id": "recNEW...", "title": "Update client proposal", ... } }
PATCH /api/tasks/:id
Partial update — only include fields you want to change. Send null to clear a clearable field (e.g. {"dueDate": null}).
Updatable Fields
| Field | Type | Notes |
title | string | |
description | string | |
priority | string | Canonical value or alias — see Field Values |
status | string | See Field Values |
triageStatus | string | See Field Values |
triageNotes | string | |
dueDate | string | null | Format: YYYY-MM-DD; null clears |
startDate | string | null | Format: YYYY-MM-DD; null clears |
hoursEstimated | number | |
assignedTo | string | Email address or collaborator ID (usr...) |
project | string | Airtable record ID |
originDetail | string | |
Example — Mark as done
curl -X PATCH \
-H "X-API-Key: your-key" \
-H "Content-Type: application/json" \
-d '{"status": "Done", "triageStatus": "Accepted"}' \
https://tasks.xray.tech/api/tasks/recXXXXXXXXXXXXXX
POST /api/tasks/:id/accept
Accept a task and link it to a project. Optionally sets an initial status (default "Ready to Start").
Request Body
| Field | Type | Required |
projectId | string | Yes |
status | string | No — see Field Values |
POST /api/tasks/:id/reject
Reject a task with a reason.
{ "reason": "Duplicate of existing task" }
POST /api/tasks/:id/needs-info
Request more information before triaging.
{ "note": "Which client is this for?" }
POST /api/tasks/:id/archive
Archive a task. No request body. Archived tasks disappear from all list views but remain in Airtable (reversible there).
POST /api/tasks/:id/delegate
Reassign a task, notifying the new assignee and recording the handoff in triage notes.
Request Body
| Field | Type | Required |
assignedTo | string (email or usr... ID) | Yes |
reason | string | No |
POST /api/tasks/:id/shortlink
Generate and save a Kutt shortlink for the task. No request body.
POST /api/tasks/bulk
Apply one action to many tasks at once.
Request Body
| Field | Type | Required | Notes |
taskIds | string[] | Yes | |
action | string | Yes | accept, reject, or done |
projectIds | string[] | For accept | First ID is linked to every task |
reason | string | No | For reject; defaults to "Rejected in bulk" |
Response
{ "ok": true, "successful": ["rec...", "rec..."], "failedCount": 0 }
Proposals
Proposals are AI-suggested changes extracted from meeting debriefs — the "Review Proposal" cards in the inbox. Accepting one makes the server apply the proposed change (create the task, update the target, etc.); rejecting dismisses the card.
GET /api/proposals
List open proposals (aiStatus = "To Consider") for the authenticated user, enriched with target task, meeting, and project context. Proposals whose linked task is already Done, Cancelled, or archived are skipped.
{
"proposals": [
{
"id": "recPROPOSAL123",
"name": "[NEW Task] Confirm renewal with client",
"type": "Status Update",
"aiStatus": "To Consider",
"proposedChangesObj": { "action": "create_task", ... },
"taskTitle": "...", // when targeting an existing task
"meetingData": { "title": "...", "date": "...", "url": "...", "authorName": "..." },
"projectData": { "id": "recPROJECT123", "name": "...", "clientId": "..." }
}
]
}
POST /api/proposals
Create a proposal targeting a task, project, or outcome. Requires at least one of taskId, projectId, outcomeId, plus type and a payload describing the change (actions include change_status, change_priority, reassign, change_due_date, change_description, and more).
POST /api/proposals/:id/accept
Accept a proposal — the server applies the proposed change. Only the proposal's owner can accept, and only while it is still "To Consider".
POST /api/proposals/:id/reject
Reject (dismiss) a proposal. Same ownership and state rules as accept.
{ "reason": "Duplicate suggestion from the same meeting" }
Projects, Clients & Outcomes
GET /api/projects?q=search
Search projects by name (min 2 characters). Returns up to 10 matches.
{ "projects": [{ "id": "recPROJECT123", "name": "Client Onboarding v2" }] }
GET /api/projects/mine
Projects managed by the current user.
GET /api/projects/:id
Combined context for one project: the project record plus its active tasks, completed tasks, and recent meetings.
{ "project": {...}, "activeTasks": [...], "completedTasks": [...], "meetings": [...] }
PATCH /api/projects/:id
Update project fields: status, priority, startDate, endDate, clientId, accountManager.
GET /api/clients
List all clients.
{ "clients": [{ "id": "recCLIENT123", "name": "ACME Corp" }] }
GET /api/clients/search?q=
Search clients by name. Empty query returns an empty list.
GET /api/outcomes?projectId=rec...
List outcomes for a project. projectId is required.
Meetings
GET /api/meetings/recent
Fetch recent meetings. By default, returns meetings you attended or created.
Parameters
| Param | Description |
all | Set to true to fetch all meetings globally. |
client | Airtable Client record ID or client name (fuzzy match). |
start / end | ISO date boundaries (e.g. 2026-01-01) |
Response
{
"meetings": [
{
"id": "recMEETING123",
"name": "Weekly Sync with ACME",
"startTime": "2026-03-25T10:00:00.000Z",
"duration": 3600,
"link": { "label": "View Transcript", "url": "https://..." },
"recordingType": "shared_screen_with_speaker_view",
"transcriptUrl": "https://...",
"projects": ["ACME Q1 Deliverables"],
"clients": ["ACME Corp"],
"users": "mark@xray.tech",
"summary": "Discussed the new onboarding flow.",
"keyProgress": "Frontend components are complete.",
"decisionsMade": "We will use Bolt.js for the Slack app.",
"nextActions": "- Mark to finalize the API docs.",
"actionable": "# Raw Action Items Markdown..."
}
]
}
Note: the timestamp key is startTime, and projects/clients contain name strings, not record IDs.
GET /api/meetings/pending
Meetings awaiting deep-match processing.
Settings & Health
GET /api/settings
The user's display and notification preferences.
PATCH /api/settings
Update preferences: showSentNav (display), notifications.channel / notifications.frequency.
GET /health
Health check (no auth required).
{ "status": "ok", "timestamp": "2026-03-10T15:00:00.000Z" }
Error Responses
All errors return a JSON object with an error field:
| Status | Meaning | Example |
| 400 | Bad request / validation (invalid enum values included) | { "error": "Validation failed", "fields": { "priority": "Must be one of: P0: Urgent & Critical, ..." } } |
| 401 | Not authenticated | { "error": "Not authenticated" } |
| 403 | Not permitted (e.g. resolving someone else's proposal) | { "error": "Only the task owner can reject proposals" } |
| 404 | Not found | { "error": "Task not found" } |
| 502 | Airtable upstream error | { "error": "Failed to fetch tasks from Airtable" } |