# REST API

HTTP REST API for browser code, mobile apps, and third-party integrations. Served under /admin/api/ by a single Next.js catch-all route handler.

> **Parts of this page need the release after 0.0.2-alpha.66:** the refresh cookie's `/admin/api/auth` scope (on 0.0.2-alpha.66 it is `/admin/api/auth/refresh`, so logout does not receive it), `403 EMAIL_NOT_VERIFIED`, and `GET /admin/api/auth/pending`.

The REST API exposes the same operations as the [Direct API](https://nextlyhq.com/docs/api-reference/direct-api.md) over HTTP. Every collection, single, user, role, permission, media file, email template, API key, and more is reachable through standard HTTP methods. Use it whenever your code does **not** run in a trusted Node.js context — browser apps, mobile clients, third-party services.

## Setup

A scaffolded project already has this route. It is a single Next.js catch-all that delegates to Nextly, and its dynamic handlers cover collections, singles, auth, dashboard, schema, email, RBAC, media, API keys, and the rest of the surface. Adding Nextly to an existing app, create it at `admin/api/[[...params]]/route.ts` in your app directory: `src/app/` as below, or the root `app/` in a project without `src/` (Next.js ignores `src/app` when a root `app` exists), where the config import is one `../` shorter:

```typescript title="src/app/admin/api/[[...params]]/route.ts"
import { createDynamicHandlers } from "nextly/runtime";
import nextlyConfig from "../../../../../nextly.config";

const handlers = createDynamicHandlers({ config: nextlyConfig });

export const { GET, POST, PUT, PATCH, DELETE, OPTIONS } = handlers;
```

The health endpoint and media-streaming endpoint can be exported at their own routes if you want to opt out of catch-all coverage. Put them in the same app directory as the catch-all: `app/api/health/route.ts` in a project with a root `app/`.

```typescript title="src/app/api/health/route.ts"
export { GET, HEAD } from "nextly/api/health";
```

Every endpoint below is served under `/admin/api/`, except the health route at `/api/health`. Keep the handler at `/admin/api`: the admin panel calls it there, and the session cookies are scoped to `/admin`, so a browser session never reaches a handler mounted anywhere else. An API key works on any mount, but the paths on this page assume the scaffold's. The global [`rateLimit`](https://nextlyhq.com/docs/configuration/nextly-config.md#ratelimit) skips requests under `/admin/api/` by default, so it applies to none of them until you change its `skip`.

## Authentication

Two authentication mechanisms are supported.

### Cookie auth (browser sessions)

When a user logs in through `/admin/api/auth/login` (or completes the `/admin/setup` wizard), the runtime sets two HTTP-only cookies:

* `nextly_session` — short-lived JWT used for every authenticated request, scoped to `/admin`.
* `nextly_refresh` — long-lived token, scoped to `/admin/api/auth` so that only the auth endpoints receive it: `/admin/api/auth/refresh` rotates it and `/admin/api/auth/logout` deletes its row. *Needs the release after 0.0.2-alpha.66.* On 0.0.2-alpha.66 it is scoped to `/admin/api/auth/refresh`, so logout does not receive it.

Browsers send these automatically. There is no header for you to set; just call `fetch` with `credentials: "include"` if your frontend lives on a different origin.

### API key (server-to-server)

Send the API key as a Bearer token:

```bash
curl https://example.com/admin/api/collections/posts/entries \
  -H "Authorization: Bearer nx_live_<random>..."
```

API keys are managed via `POST /admin/api/api-keys` (admin UI). There is **no** `X-API-Key` header — `Authorization: Bearer <key>` is the only supported form.

## Response envelopes

All responses follow the canonical Phase 4 shapes documented in [`packages/nextly/src/api/response-shapes.ts`](https://github.com/nextlyhq/nextly/blob/main/packages/nextly/src/api/response-shapes.ts). Content-Type is always `application/json` for success responses and `application/problem+json` for errors. Every response carries an `X-Request-Id` header you can quote when filing bugs.

### List

Paginated reads return `{ items, meta }`:

```json
{
  "items": [ /* T[] */ ],
  "meta": {
    "total": 42,
    "page": 1,
    "limit": 10,
    "totalPages": 5,
    "hasNext": true,
    "hasPrev": false
  }
}
```

### Read

Single-document reads return the bare document:

```json
{
  "id": "abc-123",
  "title": "Hello world",
  "status": "published",
  "createdAt": "2026-01-15T10:30:00.000Z",
  "updatedAt": "2026-01-15T10:30:00.000Z"
}
```

### Mutation

Create / update / delete-by-id return `{ message, item }`:

```json
{
  "message": "Posts updated.",
  "item": {
    "id": "abc-123",
    "title": "Hello world",
    "status": "published"
  }
}
```

### Action

Non-CRUD writes (login, forgot-password, verify-email, ...) return `{ message, ...result }` — a server-authored toast plus any operation-specific fields:

```json
{
  "message": "Logged in.",
  "user": { "id": "user-1", "email": "user@example.com" },
  "token": "eyJ..."
}
```

### Bulk

Bulk-by-id and bulk-upload responses return `{ message, items, errors }`. The HTTP status is always 200 unless the request itself is malformed; per-item failures are first-class data in `errors`:

```json
{
  "message": "Bulk delete completed.",
  "items": [ { "id": "post-1" }, { "id": "post-2" } ],
  "errors": [
    { "id": "post-3", "code": "NOT_FOUND", "message": "Resource not found." }
  ]
}
```

### Errors

Failures emit `application/problem+json` with status from `NextlyError.statusCode` and a single envelope:

```json
{
  "error": {
    "code": "NOT_FOUND",
    "message": "Resource not found.",
    "requestId": "req_nlwxzwtnlvqsu3lc"
  }
}
```

When the failure is a validation error, `error.data` carries field-level details:

```json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed.",
    "requestId": "req_xxx",
    "data": {
      "errors": [
        { "path": "email", "code": "INVALID_EMAIL", "message": "Email is invalid." }
      ]
    }
  }
}
```

For rate-limited responses (`code: "RATE_LIMITED"`, status 429) the runtime also sets `Retry-After` from `error.data.retryAfterSeconds`.

| Status | Common codes                                | When                                                                                                       |
| ------ | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `400`  | `VALIDATION_ERROR`                          | Body or query failed validation.                                                                           |
| `401`  | `AUTH_REQUIRED`, `AUTH_INVALID_CREDENTIALS` | Missing or bad credentials.                                                                                |
| `403`  | `FORBIDDEN`                                 | Authenticated but lacks permission.                                                                        |
| `403`  | `EMAIL_NOT_VERIFIED`                        | Login with the correct password for an address not yet verified. *Needs the release after 0.0.2-alpha.66.* |
| `404`  | `NOT_FOUND`                                 | Resource does not exist.                                                                                   |
| `409`  | `CONFLICT`, `DUPLICATE`                     | Concurrent edit, duplicate email, etc.                                                                     |
| `413`  | `PAYLOAD_TOO_LARGE`                         | Upload bigger than configured cap.                                                                         |
| `415`  | `UNSUPPORTED_MEDIA_TYPE`                    | Wrong `Content-Type`.                                                                                      |
| `422`  | `INVALID_INPUT`                             | Semantic validation failed.                                                                                |
| `429`  | `RATE_LIMITED`                              | Per-IP or per-key rate limit. `Retry-After` is set.                                                        |
| `500`  | `INTERNAL_ERROR`, `DATABASE_ERROR`          | Server-side failure.                                                                                       |
| `503`  | `SERVICE_UNAVAILABLE`                       | Health probe / dependency outage.                                                                          |

## Endpoint inventory

The catch-all dispatches every URL under `/admin/api/` to one of these endpoint groups; the health route is its own route at `/api/health`. Auth column shows whether a session cookie or API key is required.

| Method          | Path                                                   | Description                                                                                                                                              | Auth                                 |
| --------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `GET` / `HEAD`  | `/api/health`                                          | Liveness + DB probe.                                                                                                                                     | Public                               |
| `GET`           | `/admin/api/auth/setup-status`                         | Whether the setup wizard has run.                                                                                                                        | Public                               |
| `POST`          | `/admin/api/auth/setup`                                | Run the first-boot setup wizard (creates super admin).                                                                                                   | Public, one-shot                     |
| `GET`           | `/admin/api/auth/session`                              | Current session info from the cookie.                                                                                                                    | Cookie                               |
| `GET`           | `/admin/api/auth/csrf`                                 | CSRF token (when applicable).                                                                                                                            | Public                               |
| `GET`           | `/admin/api/auth/ui`                                   | Login page UI: provider buttons, challenge views, slots.                                                                                                 | Public                               |
| `GET`           | `/admin/api/auth/pending`                              | Which second factor an interrupted sign-in left outstanding; 204 when none is, or its attempt budget is spent. *Needs the release after 0.0.2-alpha.66.* | Public                               |
| `POST`          | `/admin/api/auth/login`                                | Email + password login. Sets cookies.                                                                                                                    | Public                               |
| `POST`          | `/admin/api/auth/challenge/resolve`                    | Answer a second factor with the pending token. Sets cookies.                                                                                             | Pending token                        |
| `POST`          | `/admin/api/auth/set-initial-password`                 | Replace an administrator-set password at first sign-in. Sets cookies.                                                                                    | Pending token                        |
| `POST`          | `/admin/api/auth/logout`                               | Delete the session's refresh token and clear cookies.                                                                                                    | Cookie                               |
| `POST`          | `/admin/api/auth/refresh`                              | Rotate the access token.                                                                                                                                 | Cookie                               |
| `POST`          | `/admin/api/auth/register`                             | Public registration.                                                                                                                                     | Public                               |
| `POST`          | `/admin/api/auth/forgot-password`                      | Send password-reset email.                                                                                                                               | Public                               |
| `POST`          | `/admin/api/auth/reset-password`                       | Consume reset token and set new password.                                                                                                                | Public                               |
| `POST`          | `/admin/api/auth/accept-invite`                        | Consume invite token and set a password.                                                                                                                 | Public                               |
| `POST`          | `/admin/api/auth/verify-email`                         | Consume email-verification token.                                                                                                                        | Public                               |
| `POST`          | `/admin/api/auth/verify-email/resend`                  | Resend the verification email.                                                                                                                           | Public                               |
| `PATCH`         | `/admin/api/auth/change-password`                      | Change current user's password.                                                                                                                          | Cookie                               |
| `GET`           | `/admin/api/me`                                        | Current user profile.                                                                                                                                    | Cookie / Bearer                      |
| `PATCH`         | `/admin/api/me`                                        | Update current user profile.                                                                                                                             | Cookie / Bearer                      |
| `GET`           | `/admin/api/me/permissions`                            | Resolved permissions for current user.                                                                                                                   | Cookie / Bearer                      |
| `GET`           | `/admin/api/users`                                     | List users.                                                                                                                                              | `read-users`                         |
| `POST`          | `/admin/api/users`                                     | Create user.                                                                                                                                             | `create-users`                       |
| `GET`           | `/admin/api/users/{id}`                                | Get user.                                                                                                                                                | `read-users`                         |
| `PATCH`         | `/admin/api/users/{id}`                                | Update user.                                                                                                                                             | `update-users`                       |
| `DELETE`        | `/admin/api/users/{id}`                                | Delete user.                                                                                                                                             | `delete-users`                       |
| `PATCH`         | `/admin/api/users/{id}/password`                       | Set password hash directly.                                                                                                                              | `update-users`                       |
| `GET`           | `/admin/api/users/{id}/roles`                          | List user roles.                                                                                                                                         | `read-roles`                         |
| `POST`          | `/admin/api/users/{id}/roles`                          | Assign role to user.                                                                                                                                     | `update-users`                       |
| `DELETE`        | `/admin/api/users/{id}/roles/{roleId}`                 | Unassign role.                                                                                                                                           | `update-users`                       |
| `GET`           | `/admin/api/roles`                                     | List roles.                                                                                                                                              | `read-roles`                         |
| `POST`          | `/admin/api/roles`                                     | Create role.                                                                                                                                             | `create-roles`                       |
| `GET`           | `/admin/api/roles/{id}`                                | Get role.                                                                                                                                                | `read-roles`                         |
| `PATCH`         | `/admin/api/roles/{id}`                                | Update role.                                                                                                                                             | `update-roles`                       |
| `DELETE`        | `/admin/api/roles/{id}`                                | Delete role.                                                                                                                                             | `delete-roles`                       |
| `GET`           | `/admin/api/roles/{id}/permissions`                    | List role permissions.                                                                                                                                   | `read-roles` or `read-permissions`   |
| `POST`          | `/admin/api/roles/{id}/permissions`                    | Add permission to role.                                                                                                                                  | `update-roles`                       |
| `PATCH`         | `/admin/api/roles/{id}/permissions`                    | Replace all role permissions.                                                                                                                            | `update-roles`                       |
| `DELETE`        | `/admin/api/roles/{id}/permissions/{permissionId}`     | Remove permission from role.                                                                                                                             | `update-roles`                       |
| `GET`           | `/admin/api/roles/{id}/children`                       | List child roles (inheritance).                                                                                                                          | `read-roles`                         |
| `POST`          | `/admin/api/roles/{id}/children`                       | Add child role.                                                                                                                                          | `update-roles`                       |
| `DELETE`        | `/admin/api/roles/{id}/children/{childId}`             | Remove child role.                                                                                                                                       | `update-roles`                       |
| `GET`           | `/admin/api/roles/{id}/parents`                        | List ancestor roles.                                                                                                                                     | `read-roles`                         |
| `GET`           | `/admin/api/permissions`                               | List permissions.                                                                                                                                        | `read-permissions` (or `read-roles`) |
| `POST`          | `/admin/api/permissions`                               | Create permission.                                                                                                                                       | `create-permissions`                 |
| `GET`           | `/admin/api/permissions/{id}`                          | Get permission.                                                                                                                                          | `read-permissions` (or `read-roles`) |
| `PATCH`         | `/admin/api/permissions/{id}`                          | Update permission.                                                                                                                                       | `update-permissions`                 |
| `DELETE`        | `/admin/api/permissions/{id}`                          | Delete permission.                                                                                                                                       | `delete-permissions`                 |
| `GET`           | `/admin/api/collections`                               | List collection definitions.                                                                                                                             | Authenticated                        |
| `POST`          | `/admin/api/collections`                               | Create collection (Schema Builder).                                                                                                                      | `manage-settings`                    |
| `GET`           | `/admin/api/collections/{slug}`                        | Get collection definition.                                                                                                                               | `read-{slug}`                        |
| `PATCH`         | `/admin/api/collections/{slug}`                        | Update collection definition.                                                                                                                            | `manage-settings`                    |
| `DELETE`        | `/admin/api/collections/{slug}`                        | Delete collection.                                                                                                                                       | `manage-settings`                    |
| `GET`           | `/admin/api/collections/schema/{slug}`                 | Get enriched schema (with component fields).                                                                                                             | `read-{slug}`                        |
| `POST`          | `/admin/api/collections/schema/{slug}/preview`         | Dry-run schema diff.                                                                                                                                     | `manage-settings`                    |
| `POST`          | `/admin/api/collections/schema/{slug}/apply`           | Apply confirmed schema changes.                                                                                                                          | `manage-settings`                    |
| `GET`           | `/admin/api/collections/{slug}/entries`                | List entries.                                                                                                                                            | `read-{slug}`                        |
| `POST`          | `/admin/api/collections/{slug}/entries`                | Create entry.                                                                                                                                            | `create-{slug}`                      |
| `GET`           | `/admin/api/collections/{slug}/entries/{id}`           | Get entry by ID.                                                                                                                                         | `read-{slug}`                        |
| `PATCH`         | `/admin/api/collections/{slug}/entries/{id}`           | Update entry.                                                                                                                                            | `update-{slug}`                      |
| `DELETE`        | `/admin/api/collections/{slug}/entries/{id}`           | Delete entry.                                                                                                                                            | `delete-{slug}`                      |
| `GET`           | `/admin/api/collections/{slug}/entries/count`          | Count entries matching a query.                                                                                                                          | `read-{slug}`                        |
| `POST`          | `/admin/api/collections/{slug}/entries/{id}/duplicate` | Duplicate entry.                                                                                                                                         | `create-{slug}`                      |
| `POST`          | `/admin/api/collections/{slug}/entries/bulk-delete`    | Bulk delete by IDs.                                                                                                                                      | `delete-{slug}`                      |
| `POST`          | `/admin/api/collections/{slug}/entries/bulk-update`    | Bulk update by IDs.                                                                                                                                      | `update-{slug}`                      |
| `PATCH`         | `/admin/api/collections/{slug}/entries`                | Bulk update by query.                                                                                                                                    | `update-{slug}`                      |
| `GET`           | `/admin/api/singles`                                   | List singles.                                                                                                                                            | Authenticated                        |
| `POST`          | `/admin/api/singles`                                   | Create single (Schema Builder).                                                                                                                          | `manage-settings`                    |
| `GET`           | `/admin/api/singles/{slug}`                            | Get single content.                                                                                                                                      | `read-{slug}`                        |
| `PATCH`         | `/admin/api/singles/{slug}`                            | Update single content.                                                                                                                                   | `update-{slug}`                      |
| `DELETE`        | `/admin/api/singles/{slug}`                            | Delete UI-created single.                                                                                                                                | `manage-settings`                    |
| `GET`           | `/admin/api/singles/{slug}/schema`                     | Get single schema.                                                                                                                                       | `read-{slug}`                        |
| `PATCH`         | `/admin/api/singles/{slug}/schema`                     | Update single schema (Schema Builder).                                                                                                                   | `manage-settings`                    |
| `POST`          | `/admin/api/singles/schema/{slug}/preview`             | Dry-run single schema diff.                                                                                                                              | `manage-settings`                    |
| `POST`          | `/admin/api/singles/schema/{slug}/apply`               | Apply single schema changes.                                                                                                                             | `manage-settings`                    |
| `GET`           | `/admin/api/field-groups`                              | List field group definitions.                                                                                                                            | Authenticated                        |
| `POST`          | `/admin/api/field-groups`                              | Create field group.                                                                                                                                      | `manage-settings`                    |
| `GET`           | `/admin/api/field-groups/{slug}`                       | Get field group.                                                                                                                                         | Authenticated                        |
| `PATCH`         | `/admin/api/field-groups/{slug}`                       | Update field group.                                                                                                                                      | `manage-settings`                    |
| `DELETE`        | `/admin/api/field-groups/{slug}`                       | Delete field group.                                                                                                                                      | `manage-settings`                    |
| `POST`          | `/admin/api/field-groups/schema/{slug}/preview`        | Dry-run field group schema diff.                                                                                                                         | `manage-settings`                    |
| `POST`          | `/admin/api/field-groups/schema/{slug}/apply`          | Apply field group schema changes.                                                                                                                        | `manage-settings`                    |
| `POST`          | `/admin/api/email/send`                                | Send a raw email.                                                                                                                                        | Authenticated                        |
| `POST`          | `/admin/api/email/send-with-template`                  | Send a templated email.                                                                                                                                  | Authenticated                        |
| `GET`           | `/admin/api/email-providers`                           | List providers.                                                                                                                                          | `read-email-providers`               |
| `POST`          | `/admin/api/email-providers`                           | Create provider.                                                                                                                                         | `create-email-providers`             |
| `GET`           | `/admin/api/email-providers/{id}`                      | Get provider.                                                                                                                                            | `read-email-providers`               |
| `PATCH`         | `/admin/api/email-providers/{id}`                      | Update provider.                                                                                                                                         | `update-email-providers`             |
| `DELETE`        | `/admin/api/email-providers/{id}`                      | Delete provider.                                                                                                                                         | `delete-email-providers`             |
| `PATCH`         | `/admin/api/email-providers/{id}/default`              | Set default provider.                                                                                                                                    | `update-email-providers`             |
| `POST`          | `/admin/api/email-providers/{id}/test`                 | Send a test email.                                                                                                                                       | `read-email-providers`               |
| `GET`           | `/admin/api/email-templates`                           | List templates.                                                                                                                                          | `read-email-templates`               |
| `POST`          | `/admin/api/email-templates`                           | Create template.                                                                                                                                         | `create-email-templates`             |
| `GET`           | `/admin/api/email-templates/layout`                    | Get shared header/footer.                                                                                                                                | `read-email-templates`               |
| `PATCH`         | `/admin/api/email-templates/layout`                    | Update shared header/footer.                                                                                                                             | `update-email-templates`             |
| `GET`           | `/admin/api/email-templates/{id}`                      | Get template.                                                                                                                                            | `read-email-templates`               |
| `PATCH`         | `/admin/api/email-templates/{id}`                      | Update template.                                                                                                                                         | `update-email-templates`             |
| `DELETE`        | `/admin/api/email-templates/{id}`                      | Delete template.                                                                                                                                         | `delete-email-templates`             |
| `POST`          | `/admin/api/email-templates/{id}/preview`              | Render template with sample data.                                                                                                                        | `read-email-templates`               |
| `GET`           | `/admin/api/user-fields`                               | List user field definitions.                                                                                                                             | `read-settings`                      |
| `POST`          | `/admin/api/user-fields`                               | Create user field.                                                                                                                                       | `create-settings`                    |
| `PATCH`         | `/admin/api/user-fields/reorder`                       | Reorder user fields.                                                                                                                                     | `update-settings`                    |
| `GET`           | `/admin/api/user-fields/{id}`                          | Get user field.                                                                                                                                          | `read-settings`                      |
| `PATCH`         | `/admin/api/user-fields/{id}`                          | Update user field.                                                                                                                                       | `update-settings`                    |
| `DELETE`        | `/admin/api/user-fields/{id}`                          | Delete user field.                                                                                                                                       | `delete-settings`                    |
| `GET`           | `/admin/api/api-keys`                                  | List API keys for current user.                                                                                                                          | Cookie (session-only)                |
| `POST`          | `/admin/api/api-keys`                                  | Create API key.                                                                                                                                          | Cookie (session-only)                |
| `GET`           | `/admin/api/api-keys/{id}`                             | Get API key metadata.                                                                                                                                    | Cookie (session-only)                |
| `PATCH`         | `/admin/api/api-keys/{id}`                             | Update name/description.                                                                                                                                 | Cookie (session-only)                |
| `DELETE`        | `/admin/api/api-keys/{id}`                             | Revoke API key.                                                                                                                                          | Cookie (session-only)                |
| `GET`           | `/admin/api/dashboard/stats`                           | Admin dashboard stats.                                                                                                                                   | Cookie                               |
| `GET`           | `/admin/api/dashboard/recent-entries`                  | Recent activity.                                                                                                                                         | Cookie                               |
| `GET`           | `/admin/api/dashboard/activity`                        | Activity feed.                                                                                                                                           | Cookie                               |
| `GET`           | `/admin/api/schema/journal`                            | Schema migration journal (super-admin only).                                                                                                             | Cookie + super admin                 |
| `GET` / `PATCH` | `/admin/api/general-settings`                          | Project-wide settings.                                                                                                                                   | `manage-settings`                    |
| `GET`           | `/admin/api/image-sizes`                               | List image-size presets.                                                                                                                                 | Cookie                               |
| `POST`          | `/admin/api/image-sizes`                               | Create image-size preset.                                                                                                                                | `manage-settings`                    |
| `PATCH`         | `/admin/api/image-sizes/{id}`                          | Update preset.                                                                                                                                           | `manage-settings`                    |
| `DELETE`        | `/admin/api/image-sizes/{id}`                          | Delete preset.                                                                                                                                           | `manage-settings`                    |

The slug-based permissions use the pattern `{action}-{slug}` so a `posts` collection requires `read-posts`, `create-posts`, etc.

A plugin may serve its own endpoints, and they are not listed here. `/admin/api/forms`, `/admin/api/forms/{slug}` and `/admin/api/forms/{slug}/submit` are served by [`@nextlyhq/plugin-form-builder`](https://nextlyhq.com/docs/plugins/form-builder.md) at those exact addresses, and answer only when it is installed. They were once built in, which never worked without the plugin either: the collections they read are declared by it.

## Per-section detail

The endpoint inventory above is the complete list. The sections below cover the request/response specifics for the groups with non-trivial body shapes.

### Health

```bash
curl -i http://localhost:3000/api/health
```

```http
HTTP/1.1 200 OK
content-type: application/json
cache-control: public, max-age=60, stale-while-revalidate=30
x-request-id: req_xxx

{
  "ok": true,
  "version": "0.0.142",
  "uptime": 123,
  "timestamp": "2026-05-05T09:00:00.000Z",
  "database": { "ok": true, "dialect": "postgresql", "latency": 4 }
}
```

When the database probe fails the runtime emits the canonical error envelope at status 503:

```json
{
  "error": {
    "code": "SERVICE_UNAVAILABLE",
    "message": "Service unavailable. Please try again later.",
    "requestId": "req_xxx"
  }
}
```

`HEAD /api/health` returns the same status, headers, and `X-Request-Id` with no body — useful for monitors that only need the status code.

### Auth

```bash
# Login
curl -i -X POST http://localhost:3000/admin/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","password":"secure-password"}'
```

A successful login responds 200 with the action envelope `{ message, user, token, exp }` and sets the `nextly_session` and `nextly_refresh` cookies on the response. Browsers handle these automatically; for API clients you can ignore them and use the returned `token` as a Bearer token instead.

```bash
# Register
curl -X POST http://localhost:3000/admin/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"new@example.com","password":"secure-password","name":"New User"}'
```

```bash
# Forgot password
curl -X POST http://localhost:3000/admin/api/auth/forgot-password \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com"}'
```

The `forgot-password` endpoint always returns 200 to avoid leaking which addresses exist. The reset email contains the token; consume it with `POST /admin/api/auth/reset-password`.

```bash
# Reset password
curl -X POST http://localhost:3000/admin/api/auth/reset-password \
  -H "Content-Type: application/json" \
  -d '{"token":"<token-from-email>","password":"new-secure-password"}'
```

```bash
# Change password (authenticated)
curl -X PATCH http://localhost:3000/admin/api/auth/change-password \
  -H "Content-Type: application/json" \
  -b "nextly_session=..." \
  -d '{"currentPassword":"old","newPassword":"new-secure"}'
```

```bash
# Verify email
curl -X POST http://localhost:3000/admin/api/auth/verify-email \
  -H "Content-Type: application/json" \
  -d '{"token":"<token-from-email>"}'
```

The setup wizard is a one-shot path that runs only when no super admin exists:

```bash
curl http://localhost:3000/admin/api/auth/setup-status
# {"ok":true,"completed":false}

curl -X POST http://localhost:3000/admin/api/auth/setup \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@example.com","name":"Admin","password":"strong-password"}'
```

Re-running setup after the first super admin exists returns a 409.

### Collection entries

List:

```bash
curl "http://localhost:3000/admin/api/collections/posts/entries?limit=10&page=1&sort=-createdAt&where[status][equals]=published"
```

```json
{
  "items": [
    { "id": "abc-123", "title": "Hello world", "status": "published" }
  ],
  "meta": {
    "total": 42,
    "page": 1,
    "limit": 10,
    "totalPages": 5,
    "hasNext": true,
    "hasPrev": false
  }
}
```

Get by ID:

```bash
curl "http://localhost:3000/admin/api/collections/posts/entries/abc-123?depth=2"
```

```json
{
  "id": "abc-123",
  "title": "Hello world",
  "author": { "id": "user-1", "name": "John Doe" },
  "createdAt": "2026-01-15T10:30:00.000Z",
  "updatedAt": "2026-01-15T10:30:00.000Z"
}
```

Create:

```bash
curl -X POST "http://localhost:3000/admin/api/collections/posts/entries" \
  -H "Authorization: Bearer nx_live_..." \
  -H "Content-Type: application/json" \
  -d '{"title":"New Post","content":"Hello!","status":"draft"}'
```

```json
{
  "message": "Posts created.",
  "item": { "id": "...", "title": "New Post", "status": "draft" }
}
```

Update (PATCH a single field set; everything not in the body is left alone):

```bash
curl -X PATCH "http://localhost:3000/admin/api/collections/posts/entries/abc-123" \
  -H "Authorization: Bearer nx_live_..." \
  -H "Content-Type: application/json" \
  -d '{"status":"published"}'
```

Delete:

```bash
curl -X DELETE "http://localhost:3000/admin/api/collections/posts/entries/abc-123" \
  -H "Authorization: Bearer nx_live_..."
```

Count:

```bash
curl "http://localhost:3000/admin/api/collections/posts/entries/count?where[status][equals]=published"
# {"total": 42}
```

Duplicate:

```bash
curl -X POST "http://localhost:3000/admin/api/collections/posts/entries/abc-123/duplicate" \
  -H "Authorization: Bearer nx_live_..."
```

Bulk delete (status 200, partial success in body):

```bash
curl -X POST "http://localhost:3000/admin/api/collections/posts/entries/bulk-delete" \
  -H "Authorization: Bearer nx_live_..." \
  -H "Content-Type: application/json" \
  -d '{"ids":["post-1","post-2","post-3"]}'
```

```json
{
  "message": "Bulk delete completed.",
  "items": [{ "id": "post-1" }, { "id": "post-2" }],
  "errors": [
    { "id": "post-3", "code": "NOT_FOUND", "message": "Resource not found." }
  ]
}
```

Bulk update by IDs:

```bash
curl -X POST "http://localhost:3000/admin/api/collections/posts/entries/bulk-update" \
  -H "Authorization: Bearer nx_live_..." \
  -H "Content-Type: application/json" \
  -d '{"ids":["post-1","post-2"],"data":{"status":"archived"}}'
```

Bulk update by query (PATCH on the collection root with a `where` clause in the body):

```bash
curl -X PATCH "http://localhost:3000/admin/api/collections/posts/entries" \
  -H "Authorization: Bearer nx_live_..." \
  -H "Content-Type: application/json" \
  -d '{"where":{"status":{"equals":"draft"}},"data":{"status":"archived"}}'
```

### Singles

List metadata for every registered single:

```bash
curl http://localhost:3000/admin/api/singles
```

Get the actual content:

```bash
curl http://localhost:3000/admin/api/singles/site-settings
```

Update content:

```bash
curl -X PATCH http://localhost:3000/admin/api/singles/site-settings \
  -H "Authorization: Bearer nx_live_..." \
  -H "Content-Type: application/json" \
  -d '{"siteName":"My New Site","maintenanceMode":false}'
```

### Email send

Both endpoints accept an authenticated request (cookie or API key). The body matches the Direct API arguments documented in [Direct API → Email](https://nextlyhq.com/docs/api-reference/direct-api.md#email).

```bash
curl -X POST http://localhost:3000/admin/api/email/send \
  -H "Authorization: Bearer nx_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "user@example.com",
    "subject": "Hello",
    "html": "<p>Welcome to Nextly</p>"
  }'

curl -X POST http://localhost:3000/admin/api/email/send-with-template \
  -H "Authorization: Bearer nx_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "user@example.com",
    "template": "welcome-email",
    "variables": { "name": "Alice" }
  }'
```

### Media

The media collection is just a normal collection (`/admin/api/collections/media/entries`) with extra plumbing for binary uploads. The `POST /admin/api/collections/media/entries` endpoint accepts `multipart/form-data` with the file under `file` and any other fields alongside.

Image-size presets are managed under `/admin/api/image-sizes`. The `regeneration-status` and `regenerate` sub-routes return a "coming soon" placeholder today (see [`packages/nextly/src/routeHandler.ts`](https://github.com/nextlyhq/nextly/blob/main/packages/nextly/src/routeHandler.ts)).

### API keys

`/admin/api/api-keys/*` is session-only — these endpoints **do not** accept Bearer authentication, so an attacker with a stolen key cannot mint new keys with it. Manage keys from the admin UI or via cookie-authenticated requests.

```bash
curl http://localhost:3000/admin/api/api-keys -b "nextly_session=..."
```

`POST /admin/api/api-keys` returns the new key value once at creation; it is hashed on the server and never displayed again.

## Query parameters

These parameters are accepted by every list endpoint that delegates to the collection / single dispatcher.

### status

For Draft/Published collections and singles, `?status=` controls which lifecycle states a read returns.

| Value       | Returns                                                                      |
| ----------- | ---------------------------------------------------------------------------- |
| *(omitted)* | Published only — the safe default. Drafts are never returned unless you ask. |
| `published` | Published only (explicit).                                                   |
| `draft`     | Drafts only.                                                                 |
| `all`       | Every entry regardless of status.                                            |

```
?status=all
?status=draft
```

An **unrecognized** value (a typo like `?status=pubished`) is rejected with `400 VALIDATION_ERROR` rather than silently returning everything, so a bad filter can never widen a read. Trusted server-side calls through the Direct API (which bypass access checks) see every status by default; over HTTP the default is always published-only. The admin panel requests `?status=all` explicitly so editors still see their drafts.

### where

Filter rows using bracket notation:

```
?where[status][equals]=published
?where[price][greater_than]=100
?where[title][contains]=hello
?where[category][in]=news,featured
```

Operators (verified against `packages/nextly/src/domains/collections/query/query-operators.ts`):

| Operator             | Description                                       | Query string example                   |
| -------------------- | ------------------------------------------------- | -------------------------------------- |
| `equals`             | Exact match.                                      | `where[status][equals]=published`      |
| `not_equals`         | Not equal.                                        | `where[status][not_equals]=draft`      |
| `greater_than`       | `>`                                               | `where[price][greater_than]=100`       |
| `greater_than_equal` | `>=`                                              | `where[price][greater_than_equal]=100` |
| `less_than`          | `<`                                               | `where[price][less_than]=50`           |
| `less_than_equal`    | `<=`                                              | `where[price][less_than_equal]=50`     |
| `like`               | SQL `LIKE` (case-sensitive).                      | `where[title][like]=%hello%`           |
| `contains`           | Case-insensitive substring (`ILIKE`).             | `where[title][contains]=hello`         |
| `search`             | Alias for `contains`.                             | `where[title][search]=hello`           |
| `in`                 | Value in comma-separated list.                    | `where[status][in]=draft,published`    |
| `not_in`             | Value not in comma-separated list.                | `where[status][not_in]=archived`       |
| `exists`             | Field is non-null when `true`, null when `false`. | `where[image][exists]=true`            |

For nested AND / OR you can either pass URL-encoded JSON via `where=...`, or use the index syntax:

```
?where[and][0][status][equals]=published&where[and][1][category][equals]=news
```

### sort

Field name; prefix with `-` for descending.

```
?sort=-createdAt
?sort=title
```

### Pagination

```
?limit=20&page=2
```

Maximum `limit` is 500 (anything higher is clamped). The response always includes the `meta` envelope with `total`, `page`, `limit`, `totalPages`, `hasNext`, and `hasPrev`.

### depth

Relationship population depth. `0` returns only IDs; `1+` populates referenced documents.

```
?depth=0   # IDs only
?depth=1   # populate direct relationships
?depth=2   # populate one level deeper
```

### select

Whitelist of fields to include. Two spellings, both accepted:

```
?select=id,title,publishedAt          # comma-separated
?select={"id":true,"title":true}      # JSON object (URL-encode it)
```

The `id` and the system timestamps come back whether or not they are named.

A `select` the API cannot read is a `400`, rather than a response carrying every
field — a projection that is silently discarded looks exactly like one that was
never asked for. Naming no fields at all is unreadable for the same reason; omit
the parameter to get every field.

### richTextFormat

```
?richTextFormat=html       # rendered HTML
?richTextFormat=json       # Lexical JSON (default)
?richTextFormat=both       # { json, html }
```

## Pagination

Page-based with the `meta` envelope. Use `meta.hasNext` to drive a "Next page" UI; do not assume a fixed `totalPages` because the underlying count can change between requests.

```typescript
async function fetchPosts(page = 1) {
  const params = new URLSearchParams({
    limit: "10",
    page: String(page),
    sort: "-createdAt",
    "where[status][equals]": "published",
    depth: "1",
  });

  const response = await fetch(`/admin/api/collections/posts/entries?${params}`, {
    credentials: "include",
  });
  const json = await response.json();
  return json; // { items, meta }
}
```

## Security

The route handler ships with built-in defences:

* **Rate limiting** on auth write endpoints (per IP) and global per-IP / per-key limits configured in `nextly.config.ts`.
* **CORS** controlled by `NEXTLY_ALLOWED_ORIGINS` and the `security` config.
* **Cache headers** (`no-store`) on every `/admin/api/auth/*` response to keep tokens out of caches.
* **Super-admin protection** so non-super-admins cannot grant the super-admin role.
* **Trusted proxy / forwarded-for** filtering when `trustProxy` is on.

See [Configuration](https://nextlyhq.com/docs/configuration/nextly-config.md) and [Authentication](https://nextlyhq.com/docs/guides/authentication.md) for the configurable surface.

## Next steps

* [Direct API](https://nextlyhq.com/docs/api-reference/direct-api.md) — server-side equivalent with no HTTP overhead.
* [Authentication](https://nextlyhq.com/docs/guides/authentication.md) — RBAC, API keys, and cookie configuration.
* [Collections](https://nextlyhq.com/docs/configuration/collections.md) — define the content types this API serves.
* [Configuration](https://nextlyhq.com/docs/configuration/nextly-config.md) — security, rate-limit, and storage configuration.
