# Collections API

Source: https://docs.qiick.io/externa/collections-api

Collection routes are defined explicitly in `routes/collections.php` (not a Laravel `Route::resource`). Every route requires **`auth` + `verified`**. Most actions also use Spatie **`permission:`** middleware; bulk delete/restore/force-delete authorize in the FormRequest. Super-admin bypasses via `Gate::before`.

> Collection schema and item CRUD are gated server-side with `PermissionEnum` (`can-show-collections`, `can-create-collections`, `can-edit-collections`, `can-delete-collections`, restore/force-delete). The React UI (`useCan`) and AI tools share the same names. Item read/write also applies **`CollectionPermissionEnforcer`** (role `collection_permissions` field ACL / `item_filter`). See [Effective permissions](https://docs.qiick.io/externa/permissions).

> **Headless Public CMS API**
> For external websites and integrations, use **`/api/v1/collections...`** (not these session routes). Auth is anonymous `public` role or Bearer API keys, gated by the Collection access matrix. See [Public CMS API](https://docs.qiick.io/externa/public-cms-api) and [GraphQL](https://docs.qiick.io/externa/graphql). Runnable companion: [externa-bruno](https://github.com/qiick-io/externa-bruno) (PublicApi + GraphQL). Session pack helpers also live under Bruno `Admin/` (not public API).

Relevant permission names:

- `can-show-collections`
- `can-create-collections`
- `can-edit-collections`
- `can-delete-collections`
- `can-restore-collections`
- `can-force-delete-collections`

## Collections

| Method   | Path                                          | Route name                      | Permission / notes                                                                 |
| -------- | --------------------------------------------- | ------------------------------- | ---------------------------------------------------------------------------------- |
| `GET`    | `/collections`                                | `collections.index`             | `can-show-collections` (`search`, `sort`, `direction`, `trashed`)                  |
| `POST`   | `/collections`                                | `collections.store`             | `can-create-collections`                                                           |
| `POST`   | `/collections/bulk`                           | `collections.bulk`              | FormRequest: `delete` → delete; `restore` → restore; `force_delete` → force-delete |
| `POST`   | `/collections/packs/{pack}`                   | `collections.packs.apply`       | `can-create-collections` (collection pack; auto-create deps)                       |
| `GET`    | `/collections/{collection}`                   | `collections.show`              | `can-show-collections`                                                             |
| `PUT`    | `/collections/{collection}`                   | `collections.update`            | `can-edit-collections` (**PUT only**)                                              |
| `DELETE` | `/collections/{collection}`                   | `collections.destroy`           | `can-delete-collections` (soft delete)                                             |
| `POST`   | `/collections/{collection}/restore`           | `collections.restore`           | `can-restore-collections`                                                          |
| `DELETE` | `/collections/{collection}/force`             | `collections.force-delete`      | `can-force-delete-collections`                                                     |
| `PUT`    | `/collections/{collection}/singleton-content` | `collections.singleton-content` | `can-edit-collections`                                                             |

Controller: `ContentCollectionController`. Bulk body: `{ action, ids }` with `action` ∈ `delete` \| `restore` \| `force_delete`.

## Fields

| Method   | Path                                                              | Route name                                  | Permission             |
| -------- | ----------------------------------------------------------------- | ------------------------------------------- | ---------------------- |
| `GET`    | `/collections/{collection}/fields`                                | `collections.fields.index`                  | `can-show-collections` |
| `POST`   | `/collections/{collection}/fields`                                | `collections.fields.store`                  | `can-edit-collections` |
| `POST`   | `/collections/{collection}/field-packs/{pack}`                    | `collections.field-packs.apply`             | `can-edit-collections` |
| `PATCH`  | `/collections/{collection}/fields/{field}`                        | `collections.fields.update`                 | `can-edit-collections` |
| `DELETE` | `/collections/{collection}/fields/{field}`                        | `collections.fields.destroy`                | `can-edit-collections` |
| `POST`   | `/collections/{collection}/fields/reorder`                        | `collections.fields.reorder`                | `can-edit-collections` |
| `POST`   | `/collections/{collection}/fields/{field}/duplicate`              | `collections.fields.duplicate`              | `can-edit-collections` |
| `POST`   | `/collections/{collection}/fields/{field}/toggle-form-visibility` | `collections.fields.toggle-form-visibility` | `can-edit-collections` |
| `POST`   | `/collections/{collection}/fields/{field}/layout-width`           | `collections.fields.update-layout-width`    | `can-edit-collections` |
| `PUT`    | `/collections/{collection}/form-layout`                           | `collections.form-layout.update`            | `can-edit-collections` |

Controller: `FieldController`. Types: [Field types](https://docs.qiick.io/externa/field-types). Packs: [Field types — Field packs](https://docs.qiick.io/externa/field-types#field-packs). Locales for labels: project **content locales** ([Project settings](https://docs.qiick.io/externa/project-settings)).

## Items

Helpers registered before `{item}` so `new` / `options` / `export` are not captured as IDs:

| Method   | Path                                             | Route name                              | Permission                                         |
| -------- | ------------------------------------------------ | --------------------------------------- | -------------------------------------------------- |
| `GET`    | `/collections/{collection}/items/new`            | `collections.items.new`                 | `can-create-collections`                           |
| `GET`    | `/collections/{collection}/items/options`        | `collections.items.field-options`       | `can-show-collections`                             |
| `GET`    | `/collections/{collection}/items/export`         | `collections.items.export`              | `can-show-collections` (`?format=csv\|json`)       |
| `PUT`    | `/collections/{collection}/list-columns`         | `collections.items.list-columns.update` | `can-edit-collections`                             |
| `POST`   | `/collections/{collection}/items/{item}/restore` | `collections.items.restore`             | `can-restore-collections`                          |
| `DELETE` | `/collections/{collection}/items/{item}/force`   | `collections.items.force-delete`        | `can-force-delete-collections`                     |
| `GET`    | `/collections/{collection}/items`                | `collections.items.index`               | `can-show-collections`                             |
| `POST`   | `/collections/{collection}/items`                | `collections.items.store`               | `can-create-collections`                           |
| `POST`   | `/collections/{collection}/items/bulk`           | `collections.items.bulk`                | FormRequest per `action` (same as collection bulk) |
| `GET`    | `/collections/{collection}/items/{item}`         | `collections.items.show`                | `can-show-collections`                             |
| `PUT`    | `/collections/{collection}/items/{item}`         | `collections.items.update`              | `can-edit-collections` (**PUT only**)              |
| `DELETE` | `/collections/{collection}/items/{item}`         | `collections.items.destroy`             | `can-delete-collections`                           |

Controller: `ItemController`. Data model: [Collections data model](https://docs.qiick.io/externa/collections-data-model). Product UX (filters, History, export): [Collection items](https://docs.qiick.io/externa/collection-items).

Item bulk also runs `CollectionPermissionEnforcer::assertItemWritable` for delete/force-delete (and skips restore). IDs from another collection are rejected.

## Item revisions

| Method | Path                                                                  | Route name                            | Permission             |
| ------ | --------------------------------------------------------------------- | ------------------------------------- | ---------------------- |
| `GET`  | `/collections/{collection}/items/{item}/revisions`                    | `collections.items.revisions.index`   | `can-show-collections` |
| `POST` | `/collections/{collection}/items/{item}/revisions/{revision}/restore` | `collections.items.revisions.restore` | `can-edit-collections` |
| `GET`  | `/collections/{collection}/items/{item}/preview-as-role`              | `collections.items.preview-as-role`   | `can-show-collections` |

Snapshots are written on every `CollectionItemValuesWriter::sync` (create/update/restore). Soft-deleted items keep history. Preview-as-role is JSON (`role_id` or `as_public=1`) — see [Collection items](https://docs.qiick.io/externa/collection-items#preview-as-role-public).

## Imports via AI / webhook

Bulk import does **not** live on these REST routes. Use:

- Authenticated AI tools (`ImportCollectionCsv`, `ImportRemoteJson`, …) under [AI API](https://docs.qiick.io/externa/ai-api)
- HMAC webhook `POST /ai/webhooks/collection-import`

Import job status: `GET /ai/import-jobs/{jobId}` (requires `can-use-ai` **and** `can-create-collections`).

## Authorization

Verified users without the matching `can-*-collections` permission receive **403**. Pest: `tests/Feature/Collections/CollectionAuthorizationTest.php`, `tests/Feature/Collections/BulkActionsTest.php`.

## Advanced list filters

Admin items index: **Search title** + **Filters** popover (AND multi-field) write this dialect into the URL. Same operators for Public API + GraphQL `filter`:

- `?filter[title]=hello` — contains (LIKE), backward compatible
- `?filter[status][_eq]=published`
- `?filter[status][_neq]=draft`
- `?filter[tags][_in]=a,b`
- `?filter[body][_null]=1` / `[_nnull]=1`
- `?filter[price][_gte]=10` / `[_lte]` / `[_gt]` / `[_lt]`
- `?filter[title][_contains]=hello`

Public REST and GraphQL also accept system columns (`id`, `created_at`, `updated_at`, `published_at`) and depth-1 relational keys such as `filter[author.name][_eq]=Ada`, plus a `sort` parameter (externa-core 1.4.0) — [Public CMS API — Advanced list filters](https://docs.qiick.io/externa/public-cms-api#advanced-list-filters).

See also [Public CMS API](https://docs.qiick.io/externa/public-cms-api#advanced-list-filters) and [GraphQL](https://docs.qiick.io/externa/graphql).
