Reads come from the plan catalog cache (never Stripe directly); the upgrade's gates are computed server-side. One `plan` object shape appears everywhere:
```responses
~~~Plan object
{
"id": "price_…",
"name": "Beta",
"price": "9.99",
"interval": "month",
"automatic_summaries_enabled": true,
"chatbot_enabled": true,
"api_enabled": true,
"limits": {
"daily_summary_count": 2,
"max_manual_summary_pasted_sources_count": 20,
"max_feed_count": 25,
"max_items_per_feed_count": 3,
"delivery_channel_max": 3,
"manual_selected_total_max": 20,
"chatbot_daily_message_count": 30
},
"platforms": ["youtube", "reddit"],
"channels": ["email", "telegram", "webhook"]
}
~~~
```
`limits` carries the seven numeric quotas; `platforms` and `channels` are the plan's allowed feeds and delivery channels.
## Subscription details
```endpoint
GET /subscription/details
```
```responses
~~~200 OK
{
"data": {
"plan": { "id": "price_…", "name": "Beta", "price": "9.99", "interval": "month", "automatic_summaries_enabled": true, "chatbot_enabled": true, "api_enabled": true, "limits": { "…": "see the plan object above" }, "platforms": ["youtube"], "channels": ["email"] },
"status": "active",
"current_period_end": "2026-11-25T00:00:00Z",
"cancel_at_period_end": false
},
"request_id": "5f0c…"
}
~~~
```
`plan.id` is a Stripe **price** id. Payment details, card state, and billing internals are deliberately absent — the API never exposes them.
## Plans
```endpoint
GET /subscription/plans
```
The live catalog, ranked low → high — the same data the pricing page renders:
```responses
~~~200 OK
{
"data": {
"items": [ { "id": "price_…", "name": "Beta", "price": "9.99", "interval": "month", "api_enabled": true, "…": "…" } ]
},
"request_id": "5f0c…"
}
~~~
```
## Upgrade
```endpoint
POST /subscription/upgrade
```
### Request body
| Field | Type | Rules |
|---|---|---|
| `plan_id` | string | **Required.** A price id from the catalog |
### Request
```tabs
~~~curl
curl -X POST https://api.daily-tube.com/v1/subscription/upgrade \
-H "Authorization: Bearer dtk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"plan_id":"price_…"}'
~~~
```
### Response
`200` with the new subscription block (same shape as `details`). Never cache a plan name client-side — re-fetch `details` and read from it; the catalog changes.
Four server-side gates run in order:
| Gate | Failure |
|---|---|
| The plan exists in the catalog | `400 subscription.unknown_plan` |
| Same plan group as yours | `409 subscription.cross_group_not_allowed` |
| Strictly higher rank | `409 subscription.not_an_upgrade` |
| A card on file for paid targets | `409 subscription.card_required` |
## 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": "subscription.not_an_upgrade", "message": "Target plan is not an upgrade over the current plan" },
"request_id": "5f0c…"
}
~~~
```
| Code | Status | When |
|---|---|---|
| `subscription.unknown_plan` | 400 | `plan_id` not in the catalog |
| `subscription.card_required` | 409 | No card on file for a paid upgrade |
| `subscription.cross_group_not_allowed` | 409 | Target group differs |
| `subscription.not_an_upgrade` | 409 | Same or lower rank |
| `validation.invalid_input` | 400 | Missing or empty `plan_id` |
> Every keyed endpoint on this page shares the per-key budget — [Rate limits](/docs/rate-limits).
Subscription
Reads come from the plan catalog cache (never Stripe directly); the upgrade's gates are computed server-side. One plan object shape appears everywhere:
~~~Plan object
{
"id": "price_…",
"name": "Beta",
"price": "9.99",
"interval": "month",
"automatic_summaries_enabled": true,
"chatbot_enabled": true,
"api_enabled": true,
"limits": {
"daily_summary_count": 2,
"max_manual_summary_pasted_sources_count": 20,
"max_feed_count": 25,
"max_items_per_feed_count": 3,
"delivery_channel_max": 3,
"manual_selected_total_max": 20,
"chatbot_daily_message_count": 30
},
"platforms": ["youtube", "reddit"],
"channels": ["email", "telegram", "webhook"]
}
~~~
limits carries the seven numeric quotas; platforms and channels are the plan's allowed feeds and delivery channels.
Subscription details
GET /subscription/details
~~~200 OK
{
"data": {
"plan": { "id": "price_…", "name": "Beta", "price": "9.99", "interval": "month", "automatic_summaries_enabled": true, "chatbot_enabled": true, "api_enabled": true, "limits": { "…": "see the plan object above" }, "platforms": ["youtube"], "channels": ["email"] },
"status": "active",
"current_period_end": "2026-11-25T00:00:00Z",
"cancel_at_period_end": false
},
"request_id": "5f0c…"
}
~~~
plan.id is a Stripe price id. Payment details, card state, and billing internals are deliberately absent — the API never exposes them.
Plans
GET /subscription/plans
The live catalog, ranked low → high — the same data the pricing page renders:
~~~200 OK
{
"data": {
"items": [ { "id": "price_…", "name": "Beta", "price": "9.99", "interval": "month", "api_enabled": true, "…": "…" } ]
},
"request_id": "5f0c…"
}
~~~
Upgrade
POST /subscription/upgrade
Request body
| Field | Type | Rules |
|---|---|---|
| plan_id | string | Required. A price id from the catalog |
Request
~~~curl
curl -X POST https://api.daily-tube.com/v1/subscription/upgrade \
-H "Authorization: Bearer dtk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"plan_id":"price_…"}'
~~~
Response
200 with the new subscription block (same shape as details). Never cache a plan name client-side — re-fetch details and read from it; the catalog changes.
Four server-side gates run in order:
| Gate | Failure |
|---|---|
| The plan exists in the catalog | 400 subscription.unknown_plan |
| Same plan group as yours | 409 subscription.cross_group_not_allowed |
| Strictly higher rank | 409 subscription.not_an_upgrade |
| A card on file for paid targets | 409 subscription.card_required |
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": "subscription.not_an_upgrade", "message": "Target plan is not an upgrade over the current plan" },
"request_id": "5f0c…"
}
~~~
| Code | Status | When |
|---|---|---|
| subscription.unknown_plan | 400 | plan_id not in the catalog |
| subscription.card_required | 409 | No card on file for a paid upgrade |
| subscription.cross_group_not_allowed | 409 | Target group differs |
| subscription.not_an_upgrade | 409 | Same or lower rank |
| validation.invalid_input | 400 | Missing or empty plan_id |
Every keyed endpoint on this page shares the per-key budget — Rate limits (/docs/rate-limits).
Reads come from the plan catalog cache (never Stripe directly); the upgrade's gates are computed server-side. One plan object shape appears everywhere:
200 with the new subscription block (same shape as details). Never cache a plan name client-side — re-fetch details and read from it; the catalog changes.
Four server-side gates run in order:
Gate
Failure
The plan exists in the catalog
400 subscription.unknown_plan
Same plan group as yours
409 subscription.cross_group_not_allowed
Strictly higher rank
409 subscription.not_an_upgrade
A card on file for paid targets
409 subscription.card_required
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": "subscription.not_an_upgrade", "message": "Target plan is not an upgrade over the current plan" },
"request_id": "5f0c…"
}
{ "error": { "code": "subscription.not_an_upgrade", "message": "Target plan is not an upgrade over the current plan" }, "request_id": "5f0c…" }
Code
Status
When
subscription.unknown_plan
400
plan_id not in the catalog
subscription.card_required
409
No card on file for a paid upgrade
subscription.cross_group_not_allowed
409
Target group differs
subscription.not_an_upgrade
409
Same or lower rank
validation.invalid_input
400
Missing or empty plan_id
Every keyed endpoint on this page shares the per-key budget — Rate limits.