---
title: Vooks Public Catalog API
description: Read the Vooks children's library and its published pricing. No API key required.
canonical: https://www.vooks.com/developers
last-updated: 2026-11-03
---

# Vooks Public Catalog API

Vooks is an ad-free streaming library of animated read-along storybooks, audiobooks and
ebooks for children roughly ages 2 to 8. This API exposes the published catalog and the
published subscription prices so an agent can answer "what should a five-year-old watch"
or "what does Vooks cost" without scraping a page.

- Base URL: `https://www.vooks.com/api/v1`
- OpenAPI 3.1: [https://www.vooks.com/openapi.json](https://www.vooks.com/openapi.json)
  ([YAML](https://www.vooks.com/openapi.yaml))
- API catalog (RFC 9727): [https://www.vooks.com/.well-known/api-catalog](https://www.vooks.com/.well-known/api-catalog)
- Support: support@vooks.com

## Authentication

Reads need no credential. There is no key to request, no form to fill in, and no quota
to apply for.

```
curl https://www.vooks.com/api/v1/titles?age=5&limit=3
```

The one exception is the bulk catalog export, which needs an OAuth 2.0 bearer token with
the `catalog:export` scope. Everything an agent needs to answer a question about Vooks is
open.

A token also raises the rate limit from 600 requests a minute to 3,000. Getting one takes
a single call:

```
curl -X POST https://www.vooks.com/agent/identity \
  -H 'content-type: application/json' \
  -d '{"identity_type":"anonymous","client_name":"My agent"}'
```

Named integrations that want `catalog:export` register a client instead. The full
walkthrough, including the authorization-code flow with PKCE, is at
[auth.md](https://www.vooks.com/auth.md). Discovery documents:
[authorization server](https://www.vooks.com/.well-known/oauth-authorization-server) (RFC
8414) and
[protected resource](https://www.vooks.com/.well-known/oauth-protected-resource) (RFC
9728).

Reads need no token, so no scope gates access to them. A token raises the rate limit
from 600 to 3,000 requests a minute, and one scope gates one operation.

| Scope | What it is for |
|---|---|
| `catalog:read` | Names the read surface on a token. Searching titles, fetching one and listing topics all work without a token. |
| `pricing:read` | The same, for the published plans and trial terms. |
| `catalog:export` | Required, and the only scope that is. It gates `POST /exports`, the one operation that writes. Reading an export back (`GET /exports/{id}`, `GET /jobs/{id}`) needs no token. |

## Endpoints

| Method | Path | What it does |
|---|---|---|
| GET | `/` | Describes the API, its policies and its endpoints |
| GET | `/titles` | Search and filter the library |
| GET | `/titles/{slug}` | One title, including the long description |
| GET | `/topics` | The topic facets `/titles?topic=` accepts |
| GET | `/plans` | Published subscription plans and trial terms |
| POST | `/batch` | Up to 20 reads in one round trip |
| POST | `/exports` | Start an asynchronous catalog export |
| GET | `/jobs/{id}` | Poll an export job |
| GET | `/exports/{id}` | Download a finished export |

### Filtering titles

`/titles` accepts `q`, `age`, `topic`, `language`, `audio` and `sort`. Filters apply
before pagination, so `pagination.total` is the true count for the query.

```
curl "https://www.vooks.com/api/v1/titles?age=4&topic=animals&limit=2"
```

```json
{
  "data": [
    {
      "slug": "a-house-for-hermit-crab",
      "title": "A House for Hermit Crab",
      "age_range": { "min": 4, "max": 8 },
      "topics": ["animals", "fiction", "ocean"],
      "url": "https://watch.vooks.com/watch/a-house-for-hermit-crab"
    }
  ],
  "pagination": { "limit": 2, "total": 61, "has_more": true, "next_cursor": "bzoy" }
}
```

`age` is a child's age in whole years and matches a title whose published range contains
it, so `age=5` returns a 4-8 title but not a 6-9 one.

## Pagination

Every list endpoint returns a `pagination` object. Follow `next_cursor` until `has_more`
is false. Cursors are opaque: pass back exactly what you were given.

```
curl "https://www.vooks.com/api/v1/titles?limit=50"
curl "https://www.vooks.com/api/v1/titles?limit=50&cursor=bzo1MA"
```

## Errors

Every non-2xx response is an RFC 9457 problem document served as
`application/problem+json`. Branch on `code`, which is stable; `title` and `detail` are
written for humans.

```json
{
  "type": "https://www.vooks.com/developers#error-invalid_parameter",
  "title": "Invalid parameter",
  "status": 400,
  "detail": "Request rejected: limit must be an integer between 1 and 100.",
  "code": "invalid_parameter",
  "errors": [{ "parameter": "limit", "detail": "must be an integer between 1 and 100" }]
}
```

| `code` | Status | Means |
|---|---|---|
| `invalid_parameter` | 400 | A parameter was rejected. `errors[].parameter` names it |
| `idempotency_key_required` | 400 | A write arrived without an `Idempotency-Key` |
| `unauthorized` | 401 | A write arrived without a token. `WWW-Authenticate` says where to get one |
| `insufficient_scope` | 403 | The token is valid but lacks `catalog:export` |
| `not_found` | 404 | No such endpoint, title or job |
| `method_not_allowed` | 405 | Wrong verb. The `Allow` header lists the right ones |
| `rate_limited` | 429 | Slow down. `Retry-After` says how long |
| `internal_error` | 500 | Something broke on our side. Retry, then tell support@vooks.com |
| `upstream_error` | 502 | The catalog service was unreachable. Retry |

## Rate limit

600 requests per minute per client. Every response, including errors, carries both the
RFC structured-field headers and the older numeric ones:

```
RateLimit-Policy: "default";q=600;w=60
RateLimit: "default";r=598;t=60
RateLimit-Limit: 600
RateLimit-Remaining: 598
RateLimit-Reset: 60
```

A 429 adds `Retry-After`. The limit is enforced per Cloudflare edge location rather than
globally, so the effective ceiling for a geographically spread client is higher than 600.
Treat 600 as the figure to design against.

## Versioning

The major version is in the path. Within `v1`, fields are only ever added, never removed
or retyped, so parse defensively and ignore what you do not recognise.

### Deprecation policy

This is the policy, not an aspiration, and it is linked from every API response as
`Link: <https://www.vooks.com/developers#versioning>; rel="deprecation"`.

- A breaking change ships as a new major version at `/api/v2`. The old version is never
  changed in place.
- When `/api/v2` ships, `/api/v1` begins returning a `Deprecation` header (RFC 9745)
  giving the date it was deprecated, and a `Sunset` header (RFC 8594) giving the date it
  stops serving.
- `/api/v1` keeps serving for **at least 180 days** after that `Deprecation` header first
  appears. We will not shorten that window.
- Watch for `Sunset` on any response. If it is absent, the version you are calling has no
  retirement date.

No version has been deprecated to date, so no response carries `Deprecation` or `Sunset`
today. Their absence is the signal that `/api/v1` is current.

## Idempotency

`POST /exports` requires an `Idempotency-Key` header. Repeating a request with the same
key and the same filters returns the same job rather than starting a second one. Nothing
is stored and nothing expires: see the note below for how that works.

```
curl -X POST https://www.vooks.com/api/v1/exports \
  -H 'authorization: Bearer <token with catalog:export>' \
  -H 'content-type: application/json' \
  -H 'idempotency-key: 2026-11-03-nightly-sync' \
  -d '{"resource":"titles","filters":{"language":"spanish"}}'
```

```json
{ "id": "6f1c...", "status": "succeeded", "result_url": "https://www.vooks.com/api/v1/exports/6f1c..." }
```

The response is `202 Accepted` with a `Location` header pointing at `/jobs/{id}`. Poll it
until `status` is `succeeded`, then fetch `result_url`. The filter runs in memory, so the
job is already finished when the 202 arrives.

Keys are 8 to 200 characters and are scoped to your client, so your `nightly` is not
anyone else's.

The job id is derived from your key **and** your filters, so the same request always
produces the same id. A retry is therefore safe: it cannot produce a second job. The
other side of that is worth knowing: reusing one key with different filters is not a
conflict, it simply addresses a different job. Nothing is stored, so there is no record
of what a key was first used for. If you want one job per key, keep the filters fixed.

## Client libraries and skills

- **npm**: [@vooks/catalog](https://www.npmjs.com/package/@vooks/catalog), a typed
  client and a `vooks` CLI with no runtime dependencies.
  `npx @vooks/catalog search dinosaurs --age 4`
- **PyPI**: [vooks-catalog](https://pypi.org/project/vooks-catalog/), the same surface in
  Python, no runtime dependencies. `pip install vooks-catalog`
- **MCP Registry**: `com.vooks/catalog` and `com.vooks/docs`, both Streamable HTTP.
- **Agent skills**: [github.com/vooks/agent-skills](https://github.com/vooks/agent-skills),
  published as an Agent Plugin.

## Sandbox

The sandbox base URL is <https://www.vooks.com/api/v1/sandbox>. It is listed as a server
in [openapi.json](https://www.vooks.com/openapi.json), so a generated client can switch
to it without editing code. It serves four fixed titles with production response
shapes. Nothing there touches the live library and no credential is needed, so an
integration can be built and tested end to end before it is pointed at real data.

```
curl https://www.vooks.com/api/v1/sandbox/titles
```

Every sandbox response carries `"sandbox": true`, so sandbox data can never be mistaken
for live data.

## Data freshness

Search and filtering are served from an index rebuilt from the live catalog; the date it
was built is in `catalog_generated_at` on every list response. `GET /titles/{slug}` reads
the live catalog directly, so a single title is never a snapshot.

## What this API does not expose

No personal data, no account data, and no playback credentials. Video playback
identifiers, stream URLs and per-user state (favourites, watch progress) are deliberately
outside the public surface. Watching a title requires a Vooks subscription at
[watch.vooks.com](https://watch.vooks.com).
