> ## Documentation Index
> Fetch the complete documentation index at: https://docs.publica.la/llms.txt
> Use this file to discover all available pages before exploring further.

# Lists API Overview

Read the reading lists of any user of your store, and manage the publications they hold, from your own platform.

This is an administrator API: every endpoint operates on **another user's** data, identified by the target user parameters described below. There is no self-service mode.

## Features

- **Read any list of any user of your store**, favorites and named lists, private ones included
- **Read the publications a list holds**, with the publica.la id, the ISBN and the reader URL of each one
- **Add and remove publications in any list of a user**, favorites included, addressed by internal id or by ISBN
- **Idempotent writes**: replaying a call never undoes what the previous one did
- **Cursor-based pagination** on every collection

---

## Authentication

| Header           | Example         | Description                                                          |
| ---------------- | --------------- | -------------------------------------------------------------------- |
| **X-User-Token** | `api-abc123...` | API token generated in your dashboard. **Header-only for security.** |

All requests **must** be performed over HTTPS. The token is not accepted as a query parameter or in the request body.

```bash
curl -X GET "https://yourstore.publica.la/api/v3/lists?user_external_id=student-1042" \
  -H "X-User-Token: api-abc123..." \
  -H "Accept: application/json"
```

### The token must belong to an administrator

Unlike the rest of the v3 API, where the token itself is the authorization, the Lists API also checks the role of the user who owns the token. The token must belong to a **store administrator**, because these endpoints read and write other users' data.

A token that belongs to a regular user is rejected on **every** endpoint of this resource:

```json
{
  "message": "This token cannot operate on other users' lists."
}
```

The store also needs the integrations API enabled. If it is not, every endpoint of this resource answers:

```json
{
  "message": "This store cannot access the integrations API."
}
```

Both responses use status `403`. Contact support if your store needs the integrations API enabled.

---

## Identifying the target user

**Every endpoint** requires **exactly one** of these parameters. Send them in the query string on `GET` and `DELETE`, and in the JSON body on `POST`.

| Parameter          | Type   | Description                                                                                         |
| ------------------ | ------ | --------------------------------------------------------------------------------------------------- |
| `user_external_id` | string | Your own identifier for the user, the one you send as `external_id` when the account is provisioned |
| `user_email`       | string | Email address of the user in your store                                                             |

- Sending **neither** returns `422`.
- Sending **both** returns `422`.
- A user who does not exist in the store, or whose account was deactivated, returns `404` with `The specified user does not exist in this store.`

:::info The account is never created
This API never provisions the user it was pointed at. An unknown identifier is an error, not an invitation to create the account. Use the [Users API](../../rest-api/users.md) or the [Auth Token integration](../../auth-integrations/auth-token.md) to create accounts.
:::

The user is required even when a list is addressed by its id: a list is only reachable through the user it belongs to, so an id that is not one of that user's lists is a `404`.

---

## Addressing a list {#addressing-a-list}

Every endpoint under `/api/v3/lists/{list}` accepts two forms of `{list}`, both resolved among the target user's lists:

| Form    | Example                                | Description                                                  |
| ------- | -------------------------------------- | ------------------------------------------------------------ |
| Slug    | `favorites`                            | The user's default list of that kind. No prior lookup needed |
| List id | `9f2d4b1e-6c3a-4f58-9b77-1d0a2e5c8b34` | Any list of the user, as returned by the list index          |

Default lists have a fixed slug. The only one exposed today is `favorites`. Custom lists carry the name the user gave them, so they are addressed by `id` only.

```bash
# The favorites of a user, without knowing the list id
GET /api/v3/lists/favorites/items?user_external_id=student-1042

# A custom list of the same user, by id
GET /api/v3/lists/9f2d4b1e-6c3a-4f58-9b77-1d0a2e5c8b34/items?user_external_id=student-1042
```

An `id` that is not a list of the target user, a slug other than `favorites`, or a reserved storefront collection returns `404` with `Not found`.

---

## Identifying the publication

The item write endpoints address a publication with `content_id` plus an optional `id_type`.

| Parameter    | Type   | Default    | Description                                                                                                                     |
| ------------ | ------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `content_id` | string | (required) | The publication identifier, read according to `id_type`                                                                         |
| `id_type`    | string | `internal` | `internal` reads `content_id` as the publica.la id; `external` reads it as one of the publication identifiers (ISBN among them) |

- Any other value of `id_type` returns `422`.
- The two modes are exclusive: with `id_type=external`, a publica.la id is not accepted and returns `404`.
- A publication that does not exist, belongs to another store, or is no longer available in your store, returns `404` with `The specified publication does not exist in this store.`

Only publications are addressable. Subscription plans, which a user can also add to a list from the storefront, are outside the scope of this API.

---

## API Endpoints

| Operation           | Method   | Endpoint                                  | Description                                                                |
| ------------------- | -------- | ----------------------------------------- | -------------------------------------------------------------------------- |
| List a user's lists | `GET`    | `/api/v3/lists`                           | Every list the store shows the target user, filterable by type, id or name |
| Get list items      | `GET`    | `/api/v3/lists/{list}/items`              | The publications a list holds                                              |
| Add an item         | `POST`   | `/api/v3/lists/{list}/items`              | Add a publication to a list of the user                                    |
| Remove an item      | `DELETE` | `/api/v3/lists/{list}/items/{content_id}` | Remove a publication from a list of the user                               |

`{list}` is a slug or a list id, see [Addressing a list](#addressing-a-list). Every endpoint takes the target user parameters.

---

## List management is not available through the API

This API reads lists and writes their contents. It does not manage lists themselves. The following operations exist only in the storefront, where the owner of the list performs them, and have **no endpoint** in the API:

| Operation                                     | Available in the API |
| --------------------------------------------- | -------------------- |
| Create a named list                           | No                   |
| Rename a list                                 | No                   |
| Change a list between public and private      | No                   |
| Delete a list                                 | No                   |
| Add, remove or change collaborators on a list | No                   |

The favorites list is the exception: it is created on first use by `POST /api/v3/lists/favorites/items`, so no create operation is needed for it.

---

## List types

| `type`      | `name`                 | Addressable by                    | Description                                                        |
| ----------- | ---------------------- | --------------------------------- | ------------------------------------------------------------------ |
| `favorites` | `favorites`            | The `favorites` slug, or its `id` | The single favorites list of the user, created on first use        |
| `custom`    | The name the user gave | Its `id` only                     | A named list the user created in the storefront, public or private |

The shopping cart and the save-for-later list are reserved storefront collections and are **not** part of this API. They never appear in the list index, and addressing one by its id returns `404`.

Private lists are visible to an administrator token: reading them is the purpose of this API.

---

## Cursor pagination

Every collection endpoint pages by cursor:

```json
{
  "data": [],
  "links": {
    "next": "https://yourstore.publica.la/api/v3/lists?user_external_id=student-1042&cursor=eyJsaXN0cy5pZCI6MTQ...",
    "prev": null
  },
  "meta": {
    "has_more": true
  }
}
```

| Parameter  | Type    | Default | Description                           |
| ---------- | ------- | ------- | ------------------------------------- |
| `per_page` | integer | `100`   | Items per page, between `1` and `500` |
| `cursor`   | string  | -       | Cursor token, taken from `links.next` |

### Pagination workflow

1. Make the initial request without `cursor`.
2. Follow `links.next` as-is: it already carries your other parameters.
3. Continue until `meta.has_more` is `false`.

:::tip Best Practice
Use `meta.has_more` to detect the end of the results. These responses carry no total count.
:::

---

## Common response format

### List object

```json
{
  "data": {
    "id": "9f2d4b1e-6c3a-4f58-9b77-1d0a2e5c8b34",
    "name": "favorites",
    "type": "favorites",
    "private": false,
    "role": "owner"
  }
}
```

| Field     | Type    | Description                                                              |
| --------- | ------- | ------------------------------------------------------------------------ |
| `id`      | string  | Stable id of the list, used in the list endpoints                        |
| `name`    | string  | `favorites` for the favorites list, the user's own name for a named list |
| `type`    | string  | `favorites` or `custom`                                                  |
| `private` | boolean | `true` when the list is not visible to other users in the storefront     |
| `role`    | string  | The target user's role on the list: `owner`, `editor` or `viewer`        |

Lists carry no timestamps of their own, so no creation date is exposed.

A user's lists include the ones they were invited to as a collaborator. `role` tells the two apart: `owner` for the lists the user created, `editor` or `viewer` for lists someone else shared with them. The favorites list is always `owner`.

### List item object

```json
{
  "data": {
    "content_id": "468166",
    "isbn": "9781234567890",
    "title": "Conversations with Donald Hall",
    "reader_url": "https://yourstore.publica.la/reader/conversations-with-donald-hall",
    "added_at": "2026-08-27T12:00:00.000000Z"
  }
}
```

| Field        | Type           | Description                                                                                                                              |
| ------------ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `content_id` | string         | publica.la id of the publication                                                                                                         |
| `isbn`       | string or null | Primary identifier of the publication, `null` when it has none                                                                           |
| `title`      | string         | Title of the publication                                                                                                                 |
| `reader_url` | string         | URL that opens the publication in the reader                                                                                             |
| `added_at`   | string or null | ISO 8601 timestamp of when the publication was added to the list, `null` for publications added before the platform started recording it |

The items endpoints return items in a stable order, the same order on every request, so a cursor pages through a list without repeating or skipping an item. That order is not chronological.

---

## Error handling

| Status | Message                                                   | Cause                                                                                                                             |
| ------ | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | `Unauthenticated.`                                        | Missing or invalid `X-User-Token`                                                                                                 |
| `403`  | `This store cannot access the integrations API.`          | The store does not have the integrations API enabled                                                                              |
| `403`  | `This token cannot operate on other users' lists.`        | The token does not belong to a store administrator                                                                                |
| `404`  | `The specified user does not exist in this store.`        | No user matches `user_external_id` or `user_email`, or the account was deactivated                                                |
| `404`  | `The specified publication does not exist in this store.` | No publication matches `content_id` under the given `id_type`                                                                     |
| `404`  | `Not found`                                               | A list id that is not one of the target user's lists, an unknown slug, or a reserved storefront collection (cart, save for later) |
| `422`  | First validation error                                    | Missing, duplicated or invalid parameters                                                                                         |
| `429`  | `Too Many Requests`                                       | Rate limit or daily read quota exceeded                                                                                           |

### Authentication error (401)

```json
{
  "message": "Unauthenticated."
}
```

### Validation error (422)

The `message` repeats the first error; `errors` carries all of them, keyed by parameter.

```json
{
  "message": "The user external id field is required when user email is not present.",
  "errors": {
    "user_external_id": [
      "The user external id field is required when user email is not present."
    ],
    "user_email": [
      "The user email field is required when user external id is not present."
    ]
  }
}
```

Sending both identifiers at once:

```json
{
  "message": "The user external id field prohibits user email from being present.",
  "errors": {
    "user_external_id": [
      "The user external id field prohibits user email from being present."
    ]
  }
}
```

### Not found (404)

```json
{
  "message": "The specified user does not exist in this store."
}
```

---

## Request logging

Every call to this resource is recorded in your store's API log, with the token that made the call, the endpoint and method, the response status, and the parameters sent (the target user among them). Logs are retained for 6 months.

---

## Rate limiting

Two limits apply: a per-token burst of **60 requests per minute** and a **daily read quota** of 2,000 reads per token plus 5,000 per store (`GET` and `HEAD` only; writes are not counted). Exceeding either returns `429 Too Many Requests` with a `Retry-After` header.

Reading a user's favorites on every page view of your own platform exhausts the daily quota quickly. Read the list once per session and cache it on your side. See the [API Overview](../../rest-api/overview.md#rate-limiting-and-flood-protection) for full details.

---

## Quick start examples

### Read a user's lists

```bash
curl -X GET "https://yourstore.publica.la/api/v3/lists?user_external_id=student-1042" \
  -H "X-User-Token: your-api-token" \
  -H "Accept: application/json"
```

### Add a publication to a user's favorites by ISBN

```bash
curl -X POST "https://yourstore.publica.la/api/v3/lists/favorites/items" \
  -H "X-User-Token: your-api-token" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "user_external_id": "student-1042",
    "content_id": "9781234567890",
    "id_type": "external"
  }'
```

---

## Next steps

- **[List Lists](./lists-v3-list.md)** - Read the lists of a user, filterable by type, id or name
- **[Get List Items](./lists-v3-get-items.md)** - Read the publications a list holds
- **[Manage List Items](./lists-v3-items.md)** - Add and remove publications in a user's favorites

---

## See also

- [API Authentication](../../rest-api/overview.md)
- [Content API](../content/content-v3-overview.md)
- [Orders API](../orders/orders-v3-overview.md)
