> ## Documentation Index
> Fetch the complete documentation index at: https://trygradient.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Sessions API

> Read assessment sessions, send reminders, change deadlines, advance or checkpoint a phase, and reopen a session after it's submitted.

# Sessions

A session represents one candidate's attempt at one assessment. Sessions progress through a defined lifecycle from invitation to review.

**Session statuses:** `invited` → `setup` → `in_progress` → `submitted` → `scoring` → `scored` → `reviewed`

## List Sessions

```
GET /api/sessions
```

<ParamField query="limit" type="integer" default="20">
  Results per page (1-100)
</ParamField>

<ParamField query="offset" type="integer" default="0">
  Offset for pagination
</ParamField>

<ParamField query="assessmentId" type="string">
  Filter by assessment
</ParamField>

<ParamField query="candidateId" type="string">
  Filter by candidate
</ParamField>

<ParamField query="status" type="string">
  Filter by status: `invited`, `setup`, `in_progress`, `submitted`, `scoring`, `scored`, `reviewed`
</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://app.trygradient.ai/api/sessions?status=submitted" \
    -H "Authorization: Bearer gai_your_key"
  ```

  ```javascript Node.js theme={null}
  const res = await fetch(
    'https://app.trygradient.ai/api/sessions?status=submitted',
    { headers: { Authorization: `Bearer ${apiKey}` } }
  );
  const { sessions, total } = await res.json();
  ```
</CodeGroup>

<Accordion title="Response">
  ```json theme={null}
  {
    "sessions": [
      {
        "id": "uuid",
        "assessmentId": "uuid",
        "candidateId": "uuid",
        "token": "abc123",
        "status": "submitted",
        "setupStartedAt": "2025-01-15T10:00:00Z",
        "taskStartedAt": "2025-01-15T10:15:00Z",
        "submittedAt": "2025-01-15T11:00:00Z",
        "dueDate": null,
        "createdAt": "2025-01-15T09:00:00Z",
        "candidateName": "Jane Smith",
        "candidateEmail": "jane@example.com",
        "assessmentName": "Senior PM Assessment"
      }
    ],
    "total": 8,
    "limit": 20,
    "offset": 0
  }
  ```
</Accordion>

***

## Get Session

```
GET /api/sessions/:sessionId
```

Returns a single session with candidate and assessment metadata.

<Accordion title="Response">
  ```json theme={null}
  {
    "session": {
      "id": "uuid",
      "assessmentId": "uuid",
      "candidateId": "uuid",
      "token": "abc123",
      "status": "scored",
      "candidateName": "Jane Smith",
      "candidateEmail": "jane@example.com",
      "assessmentName": "Senior PM Assessment",
      "setupStartedAt": "2025-01-15T10:00:00Z",
      "taskStartedAt": "2025-01-15T10:15:00Z",
      "submittedAt": "2025-01-15T11:00:00Z"
    }
  }
  ```
</Accordion>

***

## Delete Session

```
DELETE /api/sessions/:sessionId
```

Permanently deletes a session and all associated messages, events, and scores.

<Warning>This action is irreversible.</Warning>

<Accordion title="Response">
  ```json theme={null}
  {
    "ok": true
  }
  ```
</Accordion>

***

## Send Reminder

```
POST /api/sessions/:sessionId/remind
```

Emails the candidate a due-date reminder for an incomplete session (status `invited`, `setup`, or `in_progress`) and stamps `reminderSentAt`. Uses the session's due date, falling back to the assessment's.

Fails with `400` when the candidate has already submitted or there is no due date to remind about, and with `429` when a reminder was already sent within the last 24 hours.

<Accordion title="Response">
  ```json theme={null}
  {
    "ok": true,
    "reminderSentAt": "2026-07-12T09:30:00Z"
  }
  ```
</Accordion>

***

## Bulk Send Reminder

```
POST /api/sessions/bulk/remind
```

Sends the reminder to every listed session in one request instead of one request per session. This route is dispatch-and-collect: it always returns `200`, even when some items fail, and each item carries its own outcome. The single-session route above still exists for the per-row action.

<ParamField body="sessionIds" type="string[]" required>
  Up to 500 session ids. Duplicates are removed before processing.
</ParamField>

<Accordion title="Response">
  ```json theme={null}
  {
    "results": [
      { "id": "session-1", "ok": true },
      { "id": "session-2", "ok": false, "error": "Candidate has already submitted" }
    ]
  }
  ```
</Accordion>

***

## Change Deadline

```
PATCH /api/sessions/:sessionId/due-date
```

Changes one candidate's due date after invite. Send `dueDate` as a calendar day (`YYYY-MM-DD`); the server resolves it to end-of-day in the org timezone, the same rule the invite flows use. Send an empty or `null` `dueDate` to clear the deadline.

<Accordion title="Request">
  ```json theme={null}
  {
    "dueDate": "2026-09-30"
  }
  ```
</Accordion>

<Accordion title="Response">
  ```json theme={null}
  {
    "ok": true,
    "dueDate": "2026-09-30T23:59:59.000Z"
  }
  ```

  `dueDate` is `null` when the deadline was cleared.
</Accordion>

***

## Bulk Change Deadline

```
POST /api/sessions/bulk/due-date
```

Changes the due date for every listed session in one request instead of one request per session, with the same `dueDate` rules as Change Deadline above. This route is dispatch-and-collect: it always returns `200`, even when some items fail, and each item carries its own outcome. The single-session route above still exists for the per-row action.

<ParamField body="sessionIds" type="string[]" required>
  Up to 500 session ids. Duplicates are removed before processing.
</ParamField>

<ParamField body="dueDate" type="string">
  Calendar day (`YYYY-MM-DD`) or day and time (`YYYY-MM-DDTHH:mm`), applied to every session in the list. Empty or `null` clears the deadline for all of them.
</ParamField>

<Accordion title="Response">
  ```json theme={null}
  {
    "results": [
      { "id": "session-1", "ok": true },
      { "id": "session-2", "ok": false, "error": "Session not found" }
    ]
  }
  ```
</Accordion>

***

## Advance Phase

```
POST /api/sessions/:sessionId/advance-phase
```

Advances a multi-phase assessment session to the next phase. Saves the current phase's deliverable state and resets the timer for the next phase. Called by the candidate workspace when the candidate clicks "Next Phase."

<Note>This endpoint is authenticated via the candidate session token, not an API key.</Note>

<ParamField body="deliverableState" type="object">
  The current phase's deliverable content (email state, document state, etc.)
</ParamField>

<ParamField body="timeRemainingAtEnd" type="integer" default="0">
  Seconds remaining on the current phase timer when the candidate advanced
</ParamField>

<Accordion title="Response">
  ```json theme={null}
  {
    "ok": true,
    "newPhaseIndex": 1
  }
  ```
</Accordion>

***

## Checkpoint Phase

```
POST /api/sessions/:sessionId/checkpoint
```

Saves the current phase's deliverable state to the database without advancing or submitting. Enables interim saves so candidate work survives browser crashes and tab closes. The checkpoint is debounced client-side (3s) and fires after prompt edits and workflow runs.

<Note>This endpoint is authenticated via the candidate session token, not an API key.</Note>

<ParamField body="deliverableState" type="object" required>
  The current phase's deliverable content (workflow state, email state, etc.)
</ParamField>

<ParamField body="recordSnapshot" type="boolean" default="false">
  When `true`, also persist a point-in-time snapshot of the deliverable for the replay/history timeline, not just the latest interim save.
</ParamField>

<Accordion title="Response">
  ```json theme={null}
  {
    "ok": true
  }
  ```
</Accordion>

***

## Reopen Session

```
POST /api/sessions/:sessionId/reopen
```

Reopens a finished session at a chosen phase and gives the lost time back. Use it when a candidate loses the rest of an assessment to something outside their control, for example a sign-out that let the later phases expire empty.

The phase clock does **not** start here. The chosen phase is put in a paused state, the session goes back to `in_progress`, and any score is marked superseded so a stale report cannot mail. The clock starts only when the candidate opens their link and resumes. That can be hours later, and no time is spent in between.

Phases before `fromPhaseIndex` keep their work. The chosen phase and every phase after it are cleared, so the candidate does that work again.

<Note>
  Owner or admin only, and a human actor only. An API key has no role, so it gets `403`. The action is gated at the `sessions.reopen` capability, the same tier as a permanent delete, because it discards work and supersedes a score a reviewer may already have read.
</Note>

<ParamField body="fromPhaseIndex" type="integer" required>
  Zero-based index of the phase to reopen at. Must be within the assessment's phase list.
</ParamField>

<Accordion title="Request">
  ```json theme={null}
  {
    "fromPhaseIndex": 1
  }
  ```
</Accordion>

<Accordion title="Response">
  ```json theme={null}
  {
    "ok": true,
    "phaseIndex": 1,
    "phaseId": "phase-b",
    "phaseName": "Draft the reply",
    "assessUrl": "https://app.trygradient.ai/assess/abc123"
  }
  ```

  Send `assessUrl` to the candidate: The reopened session waits until they open it. Errors: `400` when `fromPhaseIndex` is not an integer of 0 or more, is out of range, or the assessment has no phases; `400` when the session has not started; `404` when the session is not in your organization.
</Accordion>

***

## Get Human Review

```
GET /api/sessions/:sessionId/review
```

Returns the current human review of a session, or `null` when nobody has recorded one. For an organization whose use case is **hiring**, every submitted session must be reviewed by a person; the AI score is advisory and this record is the decision. Reviewer and above, or an API key.

<Accordion title="Response">
  ```json theme={null}
  {
    "review": {
      "decision": "moving_on",
      "note": "Strong on the tradeoff. Move to onsite.",
      "reviewedAt": "2026-09-15T10:00:00Z",
      "reviewedBy": "uuid"
    }
  }
  ```

  `reviewedBy` is `null` when an API key recorded the decision or the member was later removed. Errors: `404` when the session is not in your organization.
</Accordion>

***

## Record Human Review

```
PUT /api/sessions/:sessionId/review
```

Marks a submitted session as reviewed and records the outcome: `moving_on` means the candidate is moving on to the next round, `not_moving_on` means they are not. Member and above, or an API key. Calling it again overwrites the decision on the session; every call appends a `session.review_recorded` entry to the organization's audit log, which cannot be changed, so the full history of decisions is kept there.

Until a decision is recorded, the member who invited the candidate receives review reminders by email (48 hours after submission, then every 3 days, at most 5) when the organization's use case is hiring.

<ParamField body="decision" type="string" required>
  `moving_on` or `not_moving_on`.
</ParamField>

<ParamField body="note" type="string">
  The reason, up to 2000 characters. Required when `decision` is `not_moving_on`; optional otherwise. Trimmed; an empty string clears it.
</ParamField>

The audit entry also records what the reviewer looked at before deciding (see [Review Activity](#review-activity)). For an API key every one of those is empty, and the entry says so.

<Accordion title="Request">
  ```json theme={null}
  {
    "decision": "not_moving_on",
    "note": "Did not catch the conflicting dates in the data room."
  }
  ```
</Accordion>

<Accordion title="Response">
  ```json theme={null}
  {
    "ok": true,
    "review": {
      "decision": "not_moving_on",
      "note": "Did not catch the conflicting dates in the data room.",
      "reviewedAt": "2026-09-15T10:00:00Z",
      "reviewedBy": "uuid"
    }
  }
  ```

  Errors: `400` when `decision` is not one of the two values, `note` is not a string, `note` is empty on a `not_moving_on` decision, or the candidate has not submitted yet; `403` for a reviewer-tier member or a candidate; `404` when the session is not in your organization.
</Accordion>

***

## Review Activity

Records what a signed-in member looks at on a session: the read-only workspace (Review work), the session replay, and the Score Breakdown tab. When a review outcome is recorded, the audit entry stores a summary of it: when the work and the replay were first opened, how long each was visible, and whether the score was seen before the work.

```
POST /api/sessions/:sessionId/review-activity
```

Reports that the signed-in member has a review surface open. The app sends it once when the surface opens and then every 30 seconds while the tab is visible. Each heartbeat credits the time since the last one, capped at 60 seconds, so a hidden tab earns nothing.

<ParamField body="surface" type="string" required>
  `workspace`, `replay`, or `score`.
</ParamField>

<Accordion title="Response">
  ```json theme={null}
  {
    "ok": true
  }
  ```

  Errors: `400` for an unknown surface; `403` for an API key or a candidate; `404` when the session is not in your organization.
</Accordion>

***

## Resume Reopened Phase

```
POST /api/sessions/:sessionId/resume
```

Starts the clock on a phase that an admin reopened. Called by the candidate workspace when the candidate clicks Resume.

<Note>This endpoint is authenticated via the candidate session token, not an API key. It is the only route that can start a paused clock, so an admin who opens the review render never spends the candidate's minutes.</Note>

Safe to call more than once. If the clock is already running, the call changes nothing and returns the running clock, so a double-click, a reload, or a second tab cannot restart the phase.

<Accordion title="Response">
  ```json theme={null}
  {
    "ok": true,
    "phaseIndex": 1,
    "startedAt": "2026-08-03T12:00:00Z",
    "timeRemaining": 1800
  }
  ```

  `timeRemaining` is in seconds.
</Accordion>

***

## Get Deliverable Versions

```
GET /api/sessions/:sessionId/deliverable-versions
```

Returns every saved version of the session's deliverable, oldest first. The
deliverable holds one document per phase, so a draft the candidate replaces
leaves no trace in the submitted state. This endpoint reads the
`deliverable_snapshot` events behind it, which record the full deliverable
about every 15 seconds and only when it changed.

Without `versionId` the response lists the versions and their metadata, with no
content. Supply `versionId` to read one version's full state. Content is split
out because one phase can hold hundreds of versions of a whole document.

`toolCallsSince` lists the AI's deliverable-writing tool calls recorded between
the previous version and this one. It is an observation, not a statement about
who wrote the version: the candidate can type in the same window, and a tool
call can fail.

<ParamField query="versionId" type="string">
  Read one version's full state instead of the list. The id comes from a row of the list.
</ParamField>

<Accordion title="Response (list)">
  ```json theme={null}
  {
    "versions": [
      {
        "id": "uuid",
        "atMs": 1705312800000,
        "phaseId": "brief",
        "deliverableType": "email",
        "chars": 1840,
        "deltaChars": -620,
        "toolCallsSince": ["rewrite_email"],
        "shrank": true
      }
    ]
  }
  ```
</Accordion>

<Accordion title="Response (one version)">
  ```json theme={null}
  {
    "version": {
      "id": "uuid",
      "atMs": 1705312800000,
      "deliverableType": "email",
      "state": { "subject": "...", "body": "..." }
    }
  }
  ```
</Accordion>

## Download Session Package

```
GET /api/sessions/:sessionId/export-package
```

Downloads one session as a zip file (`Content-Type: application/zip`,
`Content-Disposition: attachment`). The zip contains:

* Each submitted deliverable as a file. A docx phase and an email phase become
  a `.docx`. An xlsx or pptx phase keeps its own file type.
* The candidate's chat with the AI agent, as a `.docx`. Tool calls, skill
  use, and connector queries show as marker lines.
* The scores, as an `.xlsx` with three tabs. **Overview** has the candidate,
  the submit time, the time taken, the total score and level, the assessment
  summary, and one row per category and sub-dimension with score, level, and
  summary. **Behaviors** has one row per judged behavior: category,
  sub-category, question, weight, verdict, evidence, and judge rationale.
  **Priority Skills** has the evidence-only findings. It has no score.

Available only for a session scored on rubric v7. Any other session gets
`400`. Org-scoped; admin or API-key auth (read tier). The scores come from the
same projections as the candidate review page, so the workbook and the page
agree.

<Accordion title="Response - 200 OK (application/zip)">
  ```
  Content-Disposition: attachment; filename="Jane Smith - Account Brief - Session Package.zip"

  Jane Smith - Account Brief.docx
  Jane Smith - First Email.docx
  Jane Smith - Reflection.docx
  Jane Smith - AI Chat Transcript.docx
  Jane Smith - Scores.xlsx
  ```
</Accordion>

<Accordion title="Response - 400 (not a v7 score)">
  ```json theme={null}
  { "error": { "code": "validation", "message": "The session package is available only for a session scored on rubric v7." } }
  ```
</Accordion>

## Record Session Chunk

```
POST /api/sessions/:sessionId/recording
```

Appends one flushed batch of rrweb DOM-recording events (cursor, clicks, scroll, typing) as an ordered chunk, streamed from the candidate workspace every 5s. Fire-and-forget — a dropped chunk never surfaces to the candidate.

<Note>This endpoint is authenticated via the candidate session token, not an API key.</Note>

<ParamField body="events" type="array" required>
  Batch of rrweb events for this chunk
</ParamField>

<ParamField body="seq" type="integer" required>
  Client-assigned monotonic chunk sequence number (0-based, incremented once per flush)
</ParamField>

<ParamField body="startedTs" type="string" required>
  ISO timestamp of the first event in the batch
</ParamField>

<ParamField body="endedTs" type="string" required>
  ISO timestamp of the last event in the batch
</ParamField>

<Accordion title="Response">
  ```json theme={null}
  {
    "ok": true
  }
  ```
</Accordion>

***

## Get Session Recording

```
GET /api/sessions/:sessionId/recording
```

Returns the concatenated rrweb screen recording for a session (cursor, clicks, scroll, typing), used by the staff replay player. Recordings are only present for sessions taken after screen recording shipped; `hasRecording` is `false` otherwise.

<Accordion title="Response">
  ```json theme={null}
  {
    "sessionId": "uuid",
    "hasRecording": true,
    "events": [{ "type": 2, "timestamp": 1705312800000 }],
    "startedMs": 1705312800000,
    "endedMs": 1705313400000,
    "chunkCount": 12
  }
  ```
</Accordion>

***

## Preview Feedback Report

```
GET /api/sessions/:sessionId/feedback-preview
```

Admin-only preview of the candidate feedback report — the same data the candidate sees at their `/assess/:token/feedback` link, rendered on the candidate detail page. Unlike the candidate-facing route, this does not gate on release: it's available as soon as the session is scored, whether or not feedback has actually been released to the candidate. Read-only; never sends anything or changes release state.

<Accordion title="Response">
  ```json theme={null}
  {
    "sessionId": "uuid",
    "hasRecording": true,
    "feedbackText": "...",
    "headline": "...",
    "strengths": [{ "text": "...", "dimensionId": "deliverable" }],
    "weaknesses": [{ "text": "...", "dimensionId": "ai_fluency" }],
    "aiUseSuggestions": [{ "text": "...", "impact": "..." }],
    "assessmentName": "Senior PM Assessment",
    "role": "Senior PM",
    "finalScore": 82,
    "axes": [
      {
        "id": "deliverable",
        "name": "Deliverable",
        "score": 3.2,
        "max": 4,
        "subSkills": [
          { "id": "communication", "name": "Communication", "score": 3, "max": 4, "level": "strong", "source": "ai", "why": "Led with a clear recommendation" }
        ]
      }
    ],
    "feedbackReleasedAt": null,
    "release": {
      "releasedAt": null,
      "sendFeedback": false,
      "delayHours": 24,
      "scoredAt": "2025-01-15T10:00:00Z",
      "candidateEmail": "jane@example.com",
      "releaseDueAt": "2025-01-16T10:00:00Z"
    },
    "managerGrades": {
      "direction": { "level": "medium", "numericLevel": 2, "by": "user-uuid", "at": "2025-01-16T09:00:00Z", "note": "Detail is not the same as good direction." }
    }
  }
  ```

  The `release` object mirrors the eligibility signals the release cron uses, so the Feedback tab can explain why a scored session's feedback hasn't gone out yet.

  `managerGrades` is the reviewer's own 0-4 grades for this session, keyed by sub-criterion id (`scores.scoring_provenance.manager`). These label the AI's score for comparison and never change it — see [Record a calibration grade](/docs/api-reference/scoring).
</Accordion>

***

## Edit Feedback Report Content

```
PATCH /api/sessions/:sessionId/feedback-report
```

Admin-only. Edits the **prose** of a candidate's feedback report: the intro paragraph, the "What stood out" bullets, and the "Where to focus next" items. Every key is optional; only what you send is written, so one section can be updated without touching the others.

Reorder and delete are expressed by sending the whole array in the order you want, minus anything you dropped. Each item is rebuilt against the known shape before it's stored, so unknown keys are discarded. An item with blank `text` is dropped, and an empty array is accepted (deleting every item is a legitimate edit).

Scores are **not** editable here by design. To disagree with a number, record a calibration grade instead — that labels the AI's score without changing the candidate's result.

<Accordion title="Request">
  ```json theme={null}
  {
    "feedbackText": "Your verification instinct is the strongest part of this session...",
    "strengths": [{ "text": "They pressure-tested the AI's output.", "dimensionId": "ai_fluency_v5" }],
    "aiUseSuggestions": [
      {
        "text": "Try telling the assistant to read the full body of every email.",
        "impact": "+~6 pts in Deliverable Quality",
        "category": "Deliverable Quality",
        "evidenceQuote": "Can you verify the email for inconsistencies",
        "rewrite": "Open and read the full body of every email in the inbox.",
        "realWorldTransfer": "Subject lines bury real deadlines.",
        "whenMs": 1784210327921
      }
    ]
  }
  ```
</Accordion>

<Accordion title="Response">
  ```json theme={null}
  {
    "feedbackText": "Your verification instinct is the strongest part of this session...",
    "strengths": [{ "text": "They pressure-tested the AI's output.", "dimensionId": "ai_fluency_v5" }],
    "aiUseSuggestions": [{ "text": "Try telling the assistant to read the full body of every email." }]
  }
  ```

  Returns the saved content. Errors: `404` when the session has not been scored, `400` when a list is not an array or exceeds 20 items.
</Accordion>

<Note>
  The candidate report is live-read, so an edit to an **already-released** report is visible to the candidate immediately. This route does not gate on `feedback_released_at`; the admin UI warns before saving in that case.
</Note>

***

## Preview Feedback Email

```
GET /api/sessions/:sessionId/email-preview
```

Admin-only preview of the candidate feedback email — builds the exact subject and HTML `sendFeedbackEmail` would send (same template, same score-derived content), but returns it as JSON instead of sending. Never calls the email provider.

<Accordion title="Response">
  ```json theme={null}
  {
    "subject": "Your Gradient feedback report is ready, Jane.",
    "html": "<!DOCTYPE html>...",
    "to": "jane@example.com"
  }
  ```
</Accordion>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.