# Public API

Read your organization's members, departments, requests and leave from other tools with an API key.

Crew has a small **read-only** API for connecting your own tools: a staff page, a spreadsheet, a
bot. It never changes anything, and it never returns contact details, notes, disciplinary records or
leave reasons.

> [!WARNING]
> The API sends your organization's data to systems you control. Your organization is responsible
> for keeping keys safe and for what those systems do with the data.

## Creating an API key

Super Users open **Settings → Integrations → API keys** and create a key with:

- a **name**, so you know what it's for (for example _Website staff page_);
- one or more **scopes**: what the key may read.

| Scope              | Gives access to                               |
| ------------------ | --------------------------------------------- |
| `employees:read`   | `/v1/employees` (members, no contact details) |
| `departments:read` | `/v1/departments`                             |
| `requests:read`    | `/v1/requests`                                |
| `leave:read`       | `/v1/leave` (who is away, no reasons)         |

`/v1/organization` works with any key.

The key looks like `crew_<server ID>_<secret>` and is **shown once**. Store it somewhere safe, such as
your server's environment variables, never in a website's public code. The list shows each key's
last four characters and when it was last used. **Revoke** a key at any time; it stops working
immediately. An organization can have up to 10 active keys.

## Making requests

Send the key as a bearer token:

```sh
curl https://crew.noyralabs.com/v1/employees \
  -H "Authorization: Bearer crew_123456789012345678_yourSecretHere"
```

- All endpoints are `GET` and return JSON.
- The API is for **servers**, not browsers: there's no CORS, so a web page can't call it directly.
  Call it from your backend and pass on what you need.
- Each key can make **240 requests a minute**. Above that you get `429` with `Retry-After: 60`.
- Responses aren't cached (`Cache-Control: no-store`). Cache on your side if you call often.

## Endpoints

### `GET /v1/organization`

```json
{ "guildId": "123456789012345678", "name": "Lumen Collective", "memberCount": 48 }
```

`memberCount` counts everyone who isn't terminated.

### `GET /v1/employees`

Scope `employees:read`. Every member, as `{ "data": [ … ] }`:

```json
{
  "data": [
    {
      "id": "1001",
      "name": "Maya Chen",
      "title": "Team Lead",
      "departmentId": "3f6b2c1e-8a4d-4c2e-9b1f-2d7e5a9c0b41",
      "unitId": null,
      "managerId": "1000",
      "employmentStatus": "active",
      "workerType": "internal",
      "hireDate": "2026-01-15",
      "discordId": "234567890123456789"
    }
  ]
}
```

`employmentStatus` is `active`, `on_leave` or `terminated`; `workerType` is `internal`, `external` or
`contractor`. Terminated members are included, so filter them out if you only want current staff.

### `GET /v1/employees/{id}`

Scope `employees:read`. One member, same fields, or `404`.

### `GET /v1/departments`

Scope `departments:read`. Departments with their units, sorted by name:

```json
{
  "data": [
    {
      "id": "3f6b2c1e-8a4d-4c2e-9b1f-2d7e5a9c0b41",
      "name": "Support",
      "units": [{ "id": "9c1d7e2a-5b3f-4e8a-a6c2-7f0e1d4b8a53", "name": "Weekend team" }]
    }
  ]
}
```

### `GET /v1/requests`

Scope `requests:read`. The 500 most recent requests, newest first:

```json
{
  "data": [
    {
      "id": "b2e4f6a8-1c3d-4e5f-8a9b-0c1d2e3f4a5b",
      "number": "REQCREW000042",
      "type": "leave",
      "status": "approved",
      "employeeId": "1001",
      "submittedAt": "2026-10-20T09:12:00.000Z",
      "decidedAt": "2026-10-20T15:40:00.000Z"
    }
  ]
}
```

No request details or reasons are included. Decided requests disappear after 7 days, like in the app.

### `GET /v1/leave`

Scope `leave:read`. Approved leave that ended in the last 30 days, is happening now or is coming up:

```json
{
  "data": [
    {
      "id": "e7d9c1b3-2a4f-4b6d-9e8c-1f3a5b7d9c2e",
      "employeeId": "1001",
      "startDate": "2026-11-02",
      "endDate": "2026-11-06"
    }
  ]
}
```

## Errors

Errors are JSON with an `error` message:

| Status | Meaning                                                                     |
| ------ | --------------------------------------------------------------------------- |
| `401`  | The key is missing, malformed or revoked.                                   |
| `403`  | The key lacks the scope for this endpoint, or the organization is archived. |
| `404`  | No such endpoint or record.                                                 |
| `429`  | Too many requests. Wait a minute.                                           |
| `500`  | Something went wrong on Crew's side. Try again later.                       |

Prefer pushing over polling? See [Webhooks](/crew/webhooks).

Source: https://docs.noyralabs.com/crew/api
