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:

  1. Sign in to the web app with your @xray.tech account.
  2. Navigate to the Settings tab in the main navigation.
  3. Enter a name for your key (e.g. "Zapier Integration") and click Generate New Key.
  4. 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: p0p4, 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

ParamDescription
activeOnlytrue to restrict to active statuses and exclude archived/done
clientNameFilter 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

FieldTypeRequiredNotes
titlestringYesMax 120 characters
descriptionstringNoMax 500 characters
originstringNoDefault: Human. See Field Values
originDetailstringNoE.g., "Claude Code", "Zapier webhook"
assignedTostringNoEmail address. Defaults to authenticated user
requestedBystringNoEmail address. Defaults to authenticated user
prioritystringNoDefault: P2: Medium Priority. Canonical value or alias — see Field Values
projectstringNoAirtable 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

FieldTypeNotes
titlestring
descriptionstring
prioritystringCanonical value or alias — see Field Values
statusstringSee Field Values
triageStatusstringSee Field Values
triageNotesstring
dueDatestring | nullFormat: YYYY-MM-DD; null clears
startDatestring | nullFormat: YYYY-MM-DD; null clears
hoursEstimatednumber
assignedTostringEmail address or collaborator ID (usr...)
projectstringAirtable record ID
originDetailstring

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

FieldTypeRequired
projectIdstringYes
statusstringNo — 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

FieldTypeRequired
assignedTostring (email or usr... ID)Yes
reasonstringNo

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

FieldTypeRequiredNotes
taskIdsstring[]Yes
actionstringYesaccept, reject, or done
projectIdsstring[]For acceptFirst ID is linked to every task
reasonstringNoFor 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

ParamDescription
allSet to true to fetch all meetings globally.
clientAirtable Client record ID or client name (fuzzy match).
start / endISO 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:

StatusMeaningExample
400Bad request / validation (invalid enum values included){ "error": "Validation failed", "fields": { "priority": "Must be one of: P0: Urgent & Critical, ..." } }
401Not authenticated{ "error": "Not authenticated" }
403Not permitted (e.g. resolving someone else's proposal){ "error": "Only the task owner can reject proposals" }
404Not found{ "error": "Task not found" }
502Airtable upstream error{ "error": "Failed to fetch tasks from Airtable" }