---
title: "List public projects"
method: GET
path: "/api/v1/public/{agencyId}/projects"
tags: ["Jobs Portal"]
---

# List public projects

`GET /api/v1/public/{agencyId}/projects`

## Request

- Base URL: `https://api.recruitwithatlas.com`
- URL: `https://api.recruitwithatlas.com/api/v1/public/{agencyId}/projects`
- Auth: none declared
- TypeScript: <https://skmtc.dev/recruitwithatlas/apis/atlas-api/docs/api/v1/public/:agencyId/projects/get.ts> (a function with typed inputs; its response is checked against the schema)

Use this endpoint to retrieve a paginated list of public projects for an agency without authentication.

This endpoint is limited to projects where `public` is `true` and `state` is `active`. It is intended for use in public-facing integrations such as career pages or job boards, where you want to show a list of open roles.

Each item in the list has the same shape as the response from **Get public project details** — so you can render a full role card from the list response without making an extra request per project. This includes the `owner` (the lead consultant on the role, as `{ name, email }`) so you can display a contact next to each job. `owner.email` is `null` unless your agency has opted in to publishing project owners' emails. `owner` is `null` when the project has no owner, or when the recorded owner does not belong to the project's agency.

**What you need to provide:**
- The **agency ID** in the URL path - the agency whose public projects you want to list

**Incremental sync:**
Pass `updatedAfter` with an ISO 8601 timestamp to receive only projects whose `updatedAt` is later than that timestamp. Each returned item includes `updatedAt`; persist the largest value you see and pass it on the next call as your sync cursor.

**Pagination:**
Results are returned in pages. Use the `page` and `pageSize` query parameters to move through large result sets. The response includes a `pagination` object so you know how many results exist in total.

## Path parameters

- `agencyId` string, uuid, required: Agency ID

## Query parameters

- `updatedAfter` string, date-time: Only projects updated after this ISO 8601 timestamp (inclusive)
- `page` integer: Page number (1-indexed)
- `pageSize` integer: Items per page (max 100)
- `perPage` integer: Alias for pageSize

## Response `200`

Paginated list of public projects

- object
  - `status` 'ok', required
  - `data` object[], required
    - `id` string, uuid, required
    - `jobRole` string, required: Job role/title
    - `jobNumber` string, nullable, required: Job reference number
    - `state` 'active' | 'closed' | 'on_hold' | 'talent_pool' | 'lead' | 'pitch' | 'opportunity', required: Project state
    - `jobDescription` string, nullable, required: Plain text job description
    - `jobDescriptionFormatted` string, nullable, required: Job description in formatted HTML
    - `contractType` 'full_time' | 'part_time' | 'contract' | 'non_exec' | 'null', nullable, required: Contract type
    - `workMode` 'office' | 'hybrid' | 'remote' | 'null', nullable, required: Work mode
    - `seniority` 'partner' | 'board' | 'founder' | 'cxo' | 'vp' | 'director' | 'manager' | 'senior' | 'middle' | 'junior' | 'null', nullable, required: Seniority level
    - `func` string, nullable, required: Job function
    - `hireTarget` integer, nullable, required: Target number of hires
    - `visaSupport` boolean, nullable, required: Whether visa support is offered
    - `salary` string, nullable, required: Salary or salary range
    - `salaryCurrency` string, nullable, required: Salary currency code
    - `location` object, nullable, required: Project location
      - `city` string, nullable, required: City/locality
      - `region` string, nullable, required: Region/state
      - `country` string, nullable, required: Country
      - `formattedAddress` string, nullable, required: Full formatted address
    - `customAttributes` object[], required: Custom attribute values for this project
      - `attributeId` string, uuid, required: Custom attribute definition ID
      - `attributeName` string, nullable, required: Attribute name
      - `attributeType` 'options' | 'text_block' | 'text_line' | 'number_input' | 'integer' | 'date' | 'null', nullable, required: Attribute type — drives the shape of each entry in `values`.
      - `values` union[], required: All values recorded for this attribute. For single-value attributes the array has one entry; for multi-select `options` attributes it may have several.
        - union: A single value entry. Shape depends on `attributeType`: - `text_line` / `text_block` → string - `integer` / `number_input` → number - `date` → ISO `YYYY-MM-DD` string - `options` → `{ optionId, optionValue }` object
          - string
          - number
          - object
            - `optionId` string, uuid, required: Selected option ID
            - `optionValue` string, nullable, required: Display value of the option
    - `createdAt` string, nullable, required: ISO 8601 creation date
    - `startedAt` string, nullable, required: Start date (YYYY-MM-DD)
    - `owner` object, nullable, required: Project owner (lead consultant), or null when the project has no owner or the recorded owner does not belong to the project's agency
      - `name` string, required: Owner name
      - `email` string, nullable, required: Owner email. `null` unless the agency has opted in to publishing project owners' emails
    - `updatedAt` string, nullable, required: ISO 8601 timestamp of the project's last modification. Persist the largest value seen across a response as the next sync cursor for `updatedAfter`.
  - `pagination` object, required
    - `page` integer, required: Current page number
    - `pageSize` integer, required: Items per page
    - `total` integer, required: Total matching items
    - `totalPages` integer, required: Total number of pages

## Other responses

- `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.
- `500`: Internal server error

## Changes

- **2026-11-03** `158ee454751c`: 2 breaking, 4 warning
  - the response property `data/items/owner/email` became nullable for the status `200`
  - removed the required property `data/items/owner/userId` from the response with the `200` status
  - added the new `undefined` enum value to the `data/items/contractType` response property for the response status `200`
  - added the new `undefined` enum value to the `data/items/customAttributes/items/attributeType` response property for the response status `200`
  - …2 more
- **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/public/:agencyId/projects/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)
