One endpoint, two lanes — the **same** path decides by whether a key is present.
```endpoint
POST /summarize
```
## Keyed lane
### Request body
| Field | Type | Rules |
|---|---|---|
| `pasted_sources` | string[] | Optional. 1–20 entries; each 1–500 characters; duplicates are dropped server-side |
| `include_account_feeds` | boolean | Optional. `true` also pulls from the account's connected [feeds](/docs/feeds); absent or `false` does not |
At least one input source is required — a non-empty `pasted_sources` **or** `include_account_feeds: true` (`400 summarize.no_input_sources` otherwise).
### Request
```tabs
~~~curl
curl -X POST https://api.daily-tube.com/v1/summarize \
-H "Authorization: Bearer dtk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"pasted_sources":["https://www.youtube.com/watch?v=…","https://www.reddit.com/r/space/…"],"include_account_feeds":true}'
~~~
~~~javascript
const res = await fetch("https://api.daily-tube.com/v1/summarize", {
method: "POST",
headers: { Authorization: "Bearer dtk_YOUR_KEY", "Content-Type": "application/json" },
body: JSON.stringify({
pasted_sources: ["https://www.youtube.com/watch?v=…"],
include_account_feeds: true,
}),
});
~~~
~~~python
import urllib.request, json
body = json.dumps({
"pasted_sources": ["https://www.youtube.com/watch?v=…"],
"include_account_feeds": True,
}).encode()
req = urllib.request.Request(
"https://api.daily-tube.com/v1/summarize",
data=body,
headers={"Authorization": "Bearer dtk_YOUR_KEY", "Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as r:
print(r.status)
~~~
```
### Response
```responses
~~~202 Accepted
{
"data": { "message": "Triggered successfully", "summary_id": "7f3a…91", "state": "in_progress" },
"request_id": "5f0c…"
}
~~~
```
`summary_id` is a normal briefing id — poll `GET /v1/summaries/{id}` (the `in_progress` projection carries `progress.overall_percent`) or watch it appear in [Summaries](/docs/summaries).
> **One manual run at a time.** A second trigger while a manual-family run is still in flight returns `409 api.run_in_progress`, with the live run's id in the message. The daily automatic run is unaffected — only a live manual run blocks a new manual trigger.
## Keyless lane (free tier)
Calling **without** an `Authorization` header runs the free tier — no account, no key:
| Field | Type | Rules |
|---|---|---|
| `pasted_sources` | string[] | **Required, exactly one entry** — the single source to summarize |
- `include_account_feeds` cannot appear without a key — `401 auth.unauthenticated`.
- Subject to the free tier's per-visitor daily cap and global window — `429 rate_limit.free_tier_daily` with an exact `Retry-After` when exhausted.
### Request
```tabs
~~~curl
curl -X POST https://api.daily-tube.com/v1/summarize \
-H "Content-Type: application/json" \
-d '{"pasted_sources":["https://www.youtube.com/watch?v=…"]}'
~~~
```
### Response
```responses
~~~202 Accepted
{
"data": { "request_id": "4d8b91c2a7", "state": "in_progress" },
"request_id": "5f0c…"
}
~~~
```
The free run's content and state are readable through `GET /v1/summaries/{request_id}` — the run id is a bare 10-hex string (the keyless-run id kind) — no side effects, no delivery.
## Errors
Every error returns the same body — `code` is the stable machine string, `message` is human: The full registry of every code lives on [Errors](/docs/errors).
```responses
~~~409 Conflict
{
"error": { "code": "api.run_in_progress", "message": "A run is already in progress (summary_id: 7f3a…91)" },
"request_id": "5f0c…"
}
~~~
```
| Code | Status | When |
|---|---|---|
| `summarize.no_input_sources` | 400 | Keyed call with no inputs |
| `validation.invalid_input` | 400 | >20 sources, >500-char entry, keyless ≠ exactly 1 source |
| `auth.unauthenticated` | 401 | `include_account_feeds` present, no key |
| `api.run_in_progress` | 409 | Previous manual run still in flight |
| `rate_limit.free_tier_daily` | 429 | Free-tier caps (keyless) |
| `rate_limit.ip_per_min` | 429 | Anonymous 30/min/IP — see [Rate limits](/docs/rate-limits) |
Summarize
One endpoint, two lanes — the same path decides by whether a key is present.
POST /summarize
Keyed lane
Request body
| Field | Type | Rules |
|---|---|---|
| pasted_sources | string[] | Optional. 1–20 entries; each 1–500 characters; duplicates are dropped server-side |
| include_account_feeds | boolean | Optional. true also pulls from the account's connected feeds (/docs/feeds); absent or false does not |
At least one input source is required — a non-empty pasted_sources or include_account_feeds: true (400 summarize.no_input_sources otherwise).
Request
~~~curl
curl -X POST https://api.daily-tube.com/v1/summarize \
-H "Authorization: Bearer dtk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"pasted_sources":["https://www.youtube.com/watch?v=…","https://www.reddit.com/r/space/…"],"include_account_feeds":true}'
~~~
~~~javascript
const res = await fetch("https://api.daily-tube.com/v1/summarize", {
method: "POST",
headers: { Authorization: "Bearer dtk_YOUR_KEY", "Content-Type": "application/json" },
body: JSON.stringify({
pasted_sources: ["https://www.youtube.com/watch?v=…"],
include_account_feeds: true,
}),
});
~~~
~~~python
import urllib.request, json
body = json.dumps({
"pasted_sources": ["https://www.youtube.com/watch?v=…"],
"include_account_feeds": True,
}).encode()
req = urllib.request.Request(
"https://api.daily-tube.com/v1/summarize",
data=body,
headers={"Authorization": "Bearer dtk_YOUR_KEY", "Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as r:
print(r.status)
~~~
Response
~~~202 Accepted
{
"data": { "message": "Triggered successfully", "summary_id": "7f3a…91", "state": "in_progress" },
"request_id": "5f0c…"
}
~~~
summary_id is a normal briefing id — poll GET /v1/summaries/{id} (the in_progress projection carries progress.overall_percent) or watch it appear in Summaries (/docs/summaries).
One manual run at a time. A second trigger while a manual-family run is still in flight returns 409 api.run_in_progress, with the live run's id in the message. The daily automatic run is unaffected — only a live manual run blocks a new manual trigger.
Keyless lane (free tier)
Calling without an Authorization header runs the free tier — no account, no key:
| Field | Type | Rules |
|---|---|---|
| pasted_sources | string[] | Required, exactly one entry — the single source to summarize |
• include_account_feeds cannot appear without a key — 401 auth.unauthenticated.
• Subject to the free tier's per-visitor daily cap and global window — 429 rate_limit.free_tier_daily with an exact Retry-After when exhausted.
Request
~~~curl
curl -X POST https://api.daily-tube.com/v1/summarize \
-H "Content-Type: application/json" \
-d '{"pasted_sources":["https://www.youtube.com/watch?v=…"]}'
~~~
Response
~~~202 Accepted
{
"data": { "request_id": "4d8b91c2a7", "state": "in_progress" },
"request_id": "5f0c…"
}
~~~
The free run's content and state are readable through GET /v1/summaries/{request_id} — the run id is a bare 10-hex string (the keyless-run id kind) — no side effects, no delivery.
Errors
Every error returns the same body — code is the stable machine string, message is human: The full registry of every code lives on Errors (/docs/errors).
~~~409 Conflict
{
"error": { "code": "api.run_in_progress", "message": "A run is already in progress (summary_id: 7f3a…91)" },
"request_id": "5f0c…"
}
~~~
| Code | Status | When |
|---|---|---|
| summarize.no_input_sources | 400 | Keyed call with no inputs |
| validation.invalid_input | 400 | >20 sources, >500-char entry, keyless ≠ exactly 1 source |
| auth.unauthenticated | 401 | include_account_feeds present, no key |
| api.run_in_progress | 409 | Previous manual run still in flight |
| rate_limit.free_tier_daily | 429 | Free-tier caps (keyless) |
| rate_limit.ip_per_min | 429 | Anonymous 30/min/IP — see Rate limits (/docs/rate-limits) |
summary_id is a normal briefing id — poll GET /v1/summaries/{id} (the in_progress projection carries progress.overall_percent) or watch it appear in Summaries.
One manual run at a time. A second trigger while a manual-family run is still in flight returns 409 api.run_in_progress, with the live run's id in the message. The daily automatic run is unaffected — only a live manual run blocks a new manual trigger.
Keyless lane (free tier)
Calling without an Authorization header runs the free tier — no account, no key:
Field
Type
Rules
pasted_sources
string[]
Required, exactly one entry — the single source to summarize
include_account_feeds cannot appear without a key — 401 auth.unauthenticated.
Subject to the free tier's per-visitor daily cap and global window — 429 rate_limit.free_tier_daily with an exact Retry-After when exhausted.
The free run's content and state are readable through GET /v1/summaries/{request_id} — the run id is a bare 10-hex string (the keyless-run id kind) — no side effects, no delivery.
Errors
Every error returns the same body — code is the stable machine string, message is human: The full registry of every code lives on Errors.
{
"error": { "code": "api.run_in_progress", "message": "A run is already in progress (summary_id: 7f3a…91)" },
"request_id": "5f0c…"
}
{ "error": { "code": "api.run_in_progress", "message": "A run is already in progress (summary_id: 7f3a…91)" }, "request_id": "5f0c…" }