---
title: "Create a list"
method: POST
path: "/v1/lists"
tags: ["Lists"]
---

# Create a list

`POST /v1/lists`

## Request

- Base URL: `https://api.trayo.ai`
- URL: `https://api.trayo.ai/v1/lists`
- Auth: HTTP bearer

**Required permissions:** `people:write`.

**A list is where the set you are working lives between calls.** Everything else in this API is a record the workspace holds; a list is YOUR grouping of those records — the 25 accounts for this week, the 40 people going to a campaign. Create it, add members with `POST /v1/lists/{id}/members`, and hand the list (not your session) to whatever acts on it.

**Do not mark a working set any other way.** Overloading `starred`, writing a marker into a person's `title`, or keeping the set only in your own context are all things previous callers did before this resource existed, and all three lose the set or corrupt a record. This is the intended place.

The response has `memberCount: 0` — members are a second call. Lists are scoped to the USER your key acts as, matching the app: you see the lists this key created, and they appear in the Trayo app under that user. Another user's list in the same workspace is indistinguishable from a non-existent one.

## Request body

- CreateListDto
  - `name` string, required: Human-readable name, shown in the Trayo app. Not unique — two lists may share a name, and the `id` is the identity.

## Response `201`

List created, empty.

- ListDto
  - `id` string, uuid, required
  - `name` string, required
  - `origin` 'saved' | 'import' | 'play', required: `saved` for anything created here. `import` and `play` are app-created lists.
  - `memberCount` integer, required: Live, non-excluded members of both kinds.
  - `composition` object, required: `memberCount`, split by kind.
    - `accounts` integer, required: Whole-account entries.
    - `people` integer, required: Pinned people.
  - `lastOutreachAt` string, date-time, nullable, required: When the app last launched outreach from this list. Null until it has.
  - `createdAt` string, date-time, required
  - `updatedAt` string, date-time, required

## Other responses

- `400`: `validation_failed` (`retry: fix_input`) — The request did not pass input validation: a missing or malformed body field, a bad `limit`, a path parameter that is not a UUID, a `signalKey` that already exists or is reserved. `message` lists every failed constraint; `param` names the first offending field when one can be identified.
- `401`: Missing, malformed, or incorrect bearer token. Every way authentication can fail returns the identical body, so do not try to infer which one it was, and do not repeat the call with the same key. `unauthorized` (`retry: do_not_retry`) — Missing, malformed, or incorrect bearer token. Identical body for every cause.
- `403`: The key grant and its owner's current workspace role must both include every scope required by the operation. `insufficient_scope` (`retry: do_not_retry`) — Scope denial details distinguish missing_key_scope (use a key granting every missing scope), owner_permission_limit (the owner needs the missing workspace permissions), and invalid_key_scope (replace the key with supported scopes). Membership and undeclared-route denials remain generic.
- `429`: Every route is rate limited per workspace across REST, MCP, all keys and OAuth users, `GET /v1/whoami` included, so a balance poll competes with the work it is watching. For `rate_limited`, wait `Retry-After` seconds (also `details.retryAfterSeconds`), then send the same request once; do not retry sooner. `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` describe the workspace's bucket, and they come back on successful responses too: read them and slow down BEFORE you are refused. A burst from one network can also be refused before the key is checked; that 429 carries `Retry-After` and no `RateLimit-*` headers. `rate_limited` (`retry: retry_later`) — The workspace or source network exhausted its request budget for one bucket. `details.retryAfterSeconds` (and `Retry-After` on REST) is the wait: wait that long, then retry the same request once. Do not poll or retry sooner; an early retry is refused again. If you cannot wait, stop and tell the user when to retry; do not fill the wait with other calls. `details.limit` and `details.bucket` name the exhausted budget. When present, RateLimit-* headers describe the active budget and reset.
- `500`: Something failed on our side. Nothing about the request needs changing. `internal_error` (`retry: retry_later`) — Something failed on our side. `message` never describes it — quote `requestId` when reporting. Retry with exponential backoff and GIVE UP after a few attempts: `retry_later` here does not mean the failure is known to be transient, only that nothing about your request needs changing. A request that fails this way repeatedly will not start working.

---

[API](https://skmtc.dev/trayo/apis/trayo-api.md) · [All operations](https://skmtc.dev/trayo/apis/trayo-api/llms.txt) · [OpenAPI document](https://skmtc.dev/trayo/apis/trayo-api/revisions/0af452dce6de?raw)
