---
title: Read text from the command line
description: Submit text to Telereader with curl and get back an openable URL — no SDK, no install.
category: api
order: 1
last_verified_at: 2026-06-21
status: published
---

# Read text from the command line

Telereader has a small HTTP API for turning text into a narrated
reading. Everything works from `curl` — no SDK or CLI required.
You onboard once, then every submit returns a URL you can open and
hear. A free caller is **never** dead-ended: you always get back an
openable URL.

Base URL: `https://telereader.ai`

## Step 1 — onboard once

The API uses the OAuth device grant (RFC 8628): the tool asks for a
code, a human approves it in their browser, and the tool gets a
token. You do this once and reuse the token.

```bash
# (a) ask for a code
curl -s -X POST https://telereader.ai/api/auth/device/code \
  -H 'content-type: application/json' -d '{"client_id":"tlread"}'
# → {"user_code":"WXYZ-1234",
#    "verification_uri_complete":"https://telereader.ai/device?user_code=WXYZ-1234",
#    "device_code":"…","interval":5,"expires_in":900}
```

Open the `verification_uri_complete` in a browser, sign in if asked,
and approve. Meanwhile, poll for the token:

```bash
# (b) poll every `interval` seconds until approved
curl -s -X POST https://telereader.ai/api/auth/device/token \
  -H 'content-type: application/json' \
  -d '{"grant_type":"urn:ietf:params:oauth:grant-type:device_code","device_code":"<device_code>","client_id":"tlread"}'
# pending  → {"error":"authorization_pending"}   (keep polling)
# approved → {"access_token":"<TOKEN>","token_type":"Bearer",...}
```

Save `<TOKEN>`. Reuse it for every future call.

## Step 2 — submit text, get an openable URL

```bash
curl -s -X POST https://telereader.ai/api/v1/readings \
  -H "Authorization: Bearer <TOKEN>" -H 'content-type: application/json' \
  -d '{"source":{"kind":"markdown","body":"# Title\n\nThe text to read aloud."}}'
```

Returns `202` immediately with a `readUrl`. Open it and it plays —
text on screen, in sync with the narration.

- **Free / over-cap:** `{"mode":"browser","readUrl":"…"}` — the URL
  generates the audio in the browser. Hand it to the human or
  `open` it.
- **Paid + headroom:** `{"mode":"server","readingId":"…","readUrl":"…","pollUrl":"…"}`
  — the audio renders on our servers. **Open `readUrl` immediately** — it
  streams as it generates, so hand it over right away; don't wait for
  generation to finish. Polling `pollUrl` for `status` is optional (only if
  you want to show progress).

A free caller is never returned a bare `402` — you get a browser
`readUrl` instead.

## The request body

`source` is the only required field:

- `{"kind":"text","body":"…"}` or `{"kind":"markdown","body":"…"}`
  (≤ 1,000,000 characters)
- `{"kind":"url","url":"https://…"}` or `{"kind":"upload",…}` —
  Telereader fetches + extracts + cleans the page/document for you, so you
  don't have to. Import is a paid feature: free callers get `402`
  (`paid_feature`) — fetch the text yourself and submit it directly instead.

Optional: `voice`, `title`, `ephemeral`, `idempotencyKey`.
Re-submitting the same content with the same key is idempotent (one
reading). You can also pass the key as a header:
`-H "Idempotency-Key: <key>"`.

## Errors

Every error is `{ "error": <code>, "message": … }`:

| HTTP | code | what to do |
|---|---|---|
| 401 | `unauthorized` | onboard (Step 1) and send the Bearer token |
| 400 | `validation_failed` | fix the body (carries `zodError`) |
| 429 | `rate_limited` | wait `Retry-After` seconds, then retry |
| 422 | `extraction_failed` | url/upload import failed — submit text instead |
| 5xx | `internal_error` | our side — grab the trace id and offer to report it (below) |

Every response carries a trace id — the **`x-trace-id`** response header, also
mirrored into the JSON body as **`traceId`**. On a `5xx`, quote it to the user
and offer to file a bug: `POST /api/auth`-authenticated `multipart/form-data` to
`/api/bug-report` with a `note` like `"[trace: <x-trace-id>] <what happened>"`.
(An agent should ask first: *"Telereader had a problem — I have a request id,
want me to report it?"*)

## Generate a client — don't hand-roll

The full machine-readable spec lives at
[`/api/v1/openapi.json`](https://telereader.ai/api/v1/openapi.json)
— a complete OpenAPI 3.0 document (onboard + submit + poll +
delete). Point your generator at it rather than hand-writing a
client.

If you're wiring up an AI agent rather than a script, see
[Use Telereader from an agent](/docs/api/agents).
