# People and Uploaders

Every asset in Playbook records who added it. This guide covers how to look a person up, how to filter assets by uploader, and what happens when a name is ambiguous.

The short version: **people are addressed by token, and you get a token by searching for them.** A person's name typed into a search query will not find their uploads — it searches the assets' own titles, tags and descriptions.

## Prerequisites[​](#prerequisites "Direct link to Prerequisites")

1. **Access token**: a Playbook API token, sent as a Bearer token.
2. **Organization slug** (`slug`): identifier for your workspace (e.g. `coolclient-ltd`).

## Step 1 — Find the person[​](#step-1--find-the-person "Direct link to Step 1 — Find the person")

```
GET /v1/{slug}/users

Authorization: Bearer YOUR_TOKEN
```

| Parameter | Type    | Required | Description                                                                                                 |
| --------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------- |
| query     | string  | no       | Find members by name or email. Fuzzy match — partial and misspelled names work. Ignored below 2 characters. |
| page      | integer | no       | Page number (default: 1).                                                                                   |
| per\_page | integer | no       | Results per page (default: 20).                                                                             |

Omit `query` to list everyone. The listing only ever contains people you are allowed to see, and `email` is present only for those you have permission to view.

```
curl "https://api.playbook.com/v1/coolclient-ltd/users?query=quackenbsh" \

  -H "Authorization: Bearer YOUR_TOKEN" | jq
```

Note the deliberate misspelling — the match is trigram-based, so it still finds the right person:

```
{

  "data": [

    {

      "id": 42,

      "token": "asg_9f2c8a1b",

      "name": "Zephyrine Quackenbush",

      "email": "zephyrine@example.com",

      "given_name": "Zephyrine",

      "family_name": "Quackenbush"

    }

  ],

  "pagy": { "current_page": 1, "total_pages": 1, "total_count": 1 }

}
```

`token` is the value every uploader filter takes.

## Step 2 — Filter assets by uploader[​](#step-2--filter-assets-by-uploader "Direct link to Step 2 — Filter assets by uploader")

Three endpoints accept an uploader filter. Which one you want depends on the question.

| Endpoint                   | Parameter                  | Use it when                                                  |
| -------------------------- | -------------------------- | ------------------------------------------------------------ |
| `GET /v1/{slug}/assets`    | `uploaded_by` (one person) | You want a **complete** list of everything one person added. |
| `GET /v1/{slug}/search`    | `filters[uploaded_by][]`   | You are also searching by keyword, tag, or media type.       |
| `GET /v1/{slug}/ai_search` | `filters[uploaded_by][]`   | You are also searching by visual content, or by date range.  |

`/assets` walks the database directly with stable cursor pagination, so it returns everything without a result ceiling. The two search endpoints rank by relevance and are capped, but let you combine the uploader with the rest of their filters and accept several people at once.

```
# Everything one person ever uploaded, complete

curl "https://api.playbook.com/v1/coolclient-ltd/assets?uploaded_by=asg_9f2c8a1b&cursor=true" \

  -H "Authorization: Bearer YOUR_TOKEN" | jq



# What two people uploaded in the last week

curl --globoff "https://api.playbook.com/v1/coolclient-ltd/ai_search?query=&filters[uploaded_by][]=asg_9f2c8a1b&filters[uploaded_by][]=asg_1d4e&filters[date_from]=2026-08-13" \

  -H "Authorization: Bearer YOUR_TOKEN" | jq
```

### Dates default to the upload date when you filter by uploader[​](#dates-default-to-the-upload-date-when-you-filter-by-uploader "Direct link to Dates default to the upload date when you filter by uploader")

`filters[date_field]` selects which date a range applies to: `original` is when the file itself was created (its EXIF date, where available), `uploaded` is when it landed in Playbook. It normally defaults to `original`.

**When `filters[uploaded_by]` is present it defaults to `uploaded` instead**, because "what did this person upload last week" is almost never a question about when a photo was taken. Pass `filters[date_field]=original` explicitly if you do want the file's own date.

## Passing a name instead of a token[​](#passing-a-name-instead-of-a-token "Direct link to Passing a name instead of a token")

Every uploader filter also accepts a plain name, resolved for you:

* **Exactly one member matches** — the request succeeds. All three endpoints report the substitution back under `meta.resolved_uploaded_by` so you can see who was picked.
* **Several members match** — the request fails with `422` and lists the candidates. Nothing is guessed.
* **Nobody matches** — the request fails with `422`.

```
{

  "errors": [

    {

      "message": "\"Quackenbush\" matches 2 workspace members. Retry with one of the tokens listed in extensions.candidates.",

      "extensions": {

        "code": "ambiguous_uploader",

        "input": "Quackenbush",

        "candidates": [

          { "token": "asg_9f2c8a1b", "name": "Zephyrine Quackenbush" },

          { "token": "asg_5b71e3d0", "name": "Bartholomew Quackenbush" }

        ]

      }

    }

  ]

}
```

Prefer resolving the person yourself first. The fallback exists so that a near miss fails loudly instead of returning an empty list, which would read as "this person uploaded nothing".

At most **20** people can be passed in one request (`too_many_uploaders`), since every value that is not already a token costs its own lookup.

### Dates are validated, not silently ignored[​](#dates-are-validated-not-silently-ignored "Direct link to Dates are validated, not silently ignored")

`filters[date_field]` must be exactly `original` or `uploaded`, and `filters[date_from]` / `filters[date_to]` must be an ISO 8601 date or datetime. A misspelled field name or an unparseable date returns `422` (`invalid_date_field` / `invalid_date`) rather than quietly filtering on the wrong column or dropping the range.

## Reading the uploader off an asset[​](#reading-the-uploader-off-an-asset "Direct link to Reading the uploader off an asset")

Every asset payload carries an `uploaded_by` stamp:

```
{

  "token": "hero-jpg",

  "title": "Hero Banner",

  "uploaded_by": { "token": "asg_9f2c8a1b", "name": "Zephyrine Quackenbush" }

}
```

Two cases where it is `null` or unfilterable:

* **The uploader was deleted.** Their assets survive, but the link to them does not, so `uploaded_by` is `null` and those assets cannot be found by uploader.
* **The uploader was anonymous** — someone adding files through a public upload board, or an integration such as the Slack or Discord app. These are *reported* on the asset so you can see where a file came from, but they are **not** valid filter values; a request that passes one fails with `422 unknown_uploader`.

## Using this from an AI agent[​](#using-this-from-an-ai-agent "Direct link to Using this from an AI agent")

Through the Playbook MCP server the same two steps are `list_members` followed by `ai_search`, `search_assets`, or `list_assets` with `uploaded_by`. Call `list_members` first whenever a request names a person — passing the name to a search tool's `query` will return confident, unrelated results.
