---
title: "List candidate stage transitions"
method: GET
path: "/api/v1/candidate-stage-events"
tags: ["Candidate stage events"]
---

# List candidate stage transitions

`GET /api/v1/candidate-stage-events`

## Request

- Base URL: `https://api.recruitwithatlas.com`
- URL: `https://api.recruitwithatlas.com/api/v1/candidate-stage-events`
- Auth: HTTP bearer
- TypeScript: <https://skmtc.dev/recruitwithatlas/apis/atlas-api/docs/api/v1/candidate-stage-events/get.ts> (a function with typed inputs; its response is checked against the schema)

Returns a keyset-paginated feed of candidate stage transitions (a candidate moving between pipeline stages, e.g. "Interviewing" -> "Offered"), oldest first, agency-wide unless narrowed by `projectId` or `candidateId`. Each row records the from/to stage (id and name), the candidacy with its person (name), the project with its company, the user who moved it and the candidate owner at the time (name and email), and when. Nested references are resolved even when the underlying record was later deleted; a reference is null only when it could not be resolved.

**Pagination (keyset):** Responses carry `pagination.hasMore` and `pagination.nextCursor` rather than a page number or total count. Request the first page without a cursor, then send the returned `nextCursor.cursorDate`/`cursorId` on each subsequent request. This stays O(pageSize) at any depth, so it is safe for large historical backfills.

**Backfill:** Loop the cursor until `hasMore` is `false`; optionally bound with `createdAfter` to cap the horizon.

**Per-candidacy history:** Pass `candidateId` for one candidacy's stage history, e.g. the date it moved into an interview stage, or `projectId` for every candidacy on a project in one call. The two filters can be combined. An id that matches nothing in your agency returns an empty page.

**Incremental sync:** Poll with `createdAfter` (the max `movedAt` from the previous run) and page the cursor within each poll. Overlap slightly and dedupe by `id` so a row written exactly at the boundary is never missed.

**Access:** Requires a key with BOTH `Candidates` and `Users` read scopes, since each row is enriched with the acting user and the candidate owner (name and email).

**Notes:** Moves of a candidate later removed from the project, or whose project or person was deleted, are included with `isReverted: true`; send `includeReverted=false` to leave them out. `stageFrom` is null on the first move into the pipeline. This feed is eventually consistent (populated asynchronously) and is not emitted for talent-pool or lead projects.

## Query parameters

- `projectId` string, uuid: Only stage moves of this project's candidacies.
- `candidateId` string, uuid: Only stage moves of this candidacy: the candidate `id` returned by `GET /projects/{id}/candidates` and `GET /candidates`, not a person id.
- `createdAfter` union: Only candidate stage events created after this point (inclusive). Accepts an ISO 8601 datetime or a date-only YYYY-MM-DD value (covering from the start of that UTC day)
  - string
  - string, date-time
- `createdBefore` union: Only candidate stage events created before this point (inclusive). Accepts an ISO 8601 datetime or a date-only YYYY-MM-DD value (covering through the end of that UTC day)
  - string
  - string, date-time
- `includeReverted` 'true' | 'false': Include stage moves that were later reverted (the candidate was removed from the project, or the project or person was deleted). Defaults to true; send false to return only effective transitions.
- `pageSize` integer: Items per page (max 100).
- `cursorDate` string, date-time: Keyset pagination cursor: the `pagination.nextCursor.cursorDate` returned by the previous request. Send together with `cursorId` to fetch the next page in stable order that stays O(pageSize) at any depth. Omit for the first page.
- `cursorId` string, uuid: Keyset pagination cursor: the `pagination.nextCursor.cursorId` returned by the previous request. Must be sent together with `cursorDate`.

## Response `200`

Paginated candidate stage transitions

- object
  - `status` 'ok', required
  - `data` object[], required
    - `id` string, uuid, required: Stable identifier for this stage-move event.
    - `type` 'candidate_stage_moved', required
    - `candidate` object, required: The candidacy that moved, with its person.
      - `id` string, uuid, required: The candidacy (candidate row on a project) this move belongs to.
      - `person` object, nullable, required: The person behind the candidacy. Null if the person could not be resolved.
        - `id` string, uuid, required
        - `firstName` string, nullable, required
        - `lastName` string, nullable, required
    - `project` object, nullable, required: Project the candidacy belongs to, with its company. Null when the move carried no project.
      - `id` string, uuid, required
      - `jobRole` string, required: The project job role / title.
      - `company` object, nullable, required: Company the project belongs to, when set.
        - `id` string, uuid, required
        - `name` string, required
    - `stageFrom` object, nullable, required: Stage the candidate moved from. Null on the first move into the pipeline.
      - `id` string, uuid, required
      - `name` string, nullable, required: Stage name (e.g. "Interviewing"). Null if the stage could not be resolved (e.g. it was deleted).
    - `stageTo` object, required: Stage the candidate moved to.
      - `id` string, uuid, required
      - `name` string, nullable, required: Stage name (e.g. "Interviewing"). Null if the stage could not be resolved (e.g. it was deleted).
    - `movedBy` object, nullable, required: User who performed the move. This may differ from `candidateOwner`. Null for system or automated moves, when the user could not be resolved, and for moves recorded before the acting user was captured (mid-September 2026).
      - `id` string, uuid, required
      - `name` string, required
      - `email` string, required
    - `candidateOwner` object, nullable, required: Owner of the candidacy at the time of the move. Event-based metrics credit the move to this user, or to `movedBy` when the candidacy had no owner. Moves before mid-August 2026 were credited to the acting user. Null when the candidacy had no owner, when the user could not be resolved, and for moves recorded before the owner was captured (June 2026).
      - `id` string, uuid, required
      - `name` string, required
      - `email` string, required
    - `movedAt` string, required: UTC ISO 8601 time the move occurred. Use this value as the cursor horizon for `createdAfter` polling.
    - `isReverted` boolean, required: Whether this move was later reverted: the candidate was removed from the project, or the project or person was deleted.
  - `pagination` object, required
    - `pageSize` integer, required
    - `hasMore` boolean, required: Whether more rows exist beyond this page. `true` means at least one further page is available — advance the `nextCursor` until it is `false`.
    - `nextCursor` object, nullable, required: Keyset cursor for the next page. Non-null when `hasMore` is `true` — send `cursorDate`/`cursorId` on the next request to page in stable order that stays O(pageSize) at any depth.
      - `cursorDate` string, required: Pass back as `cursorDate` to fetch the next page.
      - `cursorId` string, uuid, required: Pass back as `cursorId` to fetch the next page.

## Other responses

- `401`: Unauthorized - missing or invalid API key
- `404`: Resource not found
- `422`: Validation error - the request failed schema validation (`errors.fieldErrors`) or a business rule (`errors.formErrors`)
- `429`: Too many requests - the caller has exceeded the per-agency rate limit for the tier this endpoint counts against (default per minute: 1200 read / 400 write / 60 upload). Inspect the `RateLimit-*` headers — returned on every response, not only on 429s — and back off until the window resets. See the "Rate limits" section of the introduction for details.

## Changes

- **2026-10-29** `1e0f6672989f`: 2 info
  - added the new optional `query` request parameter `candidateId`
  - added the new optional `query` request parameter `projectId`
- **2026-10-15** `777040d954ab`: 1 info
  - added the required property `data/items/candidateOwner` to the response with the `200` status
- **2026-10-03** `cd4bc8bbd95e`: 1 info
  - added the optional property `error` to the response with the `422` status

[Operation history](https://skmtc.dev/recruitwithatlas/apis/atlas-api/changes/api/v1/candidate-stage-events/get.md)

---

[API](https://skmtc.dev/recruitwithatlas/apis/atlas-api.md) - [All operations](https://skmtc.dev/recruitwithatlas/apis/atlas-api/llms.txt) - [OpenAPI document](https://skmtc.dev/recruitwithatlas/apis/atlas-api/revisions/158ee454751c?raw)
