# Series, subscriptions and certificates

Use this to sell video people watch on their own time. Creators publish single videos, series of episodes and courses. Viewers buy a video, a whole series or one episode, rent a video for a set time, or pay for a monthly plan. Your player gets a short-lived token for each video, your app tracks how far each viewer has watched, and a viewer who finishes a course earns a certificate anyone can check. Live broadcasts are the other side of Streams: see [Streams](https://docs.vabu.live/guides/streams/index.md) and [paid access](https://docs.vabu.live/guides/streams/paid-access.md).

## How the pieces fit

| Content type | What it is |
|---|---|
| `standalone_video` | One video, with an optional free trailer. |
| `playlist` | A series. Episodes in order, each with its own price and release date if you want. |
| `course` | Chapters with video and text lessons. Finishing every lesson earns a certificate. |

A client uses them in this order:

1. A creator makes a draft with a proposed price, adds episodes or lessons, uploads each video and adds a trailer.
2. The creator asks for approval. Tenant staff review the content and publish it.
3. Your app lists the catalogue. Anyone can watch the trailer for free.
4. A viewer buys with `POST /v1/plus/orders`. The `purchase_type` is `lifetime`, `rental` (with `rental_hours`) or `episode`. The response holds a `checkout_url`, and a guest only needs a name and email. A viewer can also start a plan.
5. To play, your app asks for a playback token for the item, then reports progress as the viewer watches.
6. When a viewer completes every required lesson in a course, a certificate is issued.

A plan costs a set price each month and covers either one organization's catalogue or every organization in the tenant. Tenant staff create plans. Each month the subscriber pays for the next cycle with `POST /v1/plus/subscriptions/{subscription}/renew`, which returns a checkout link. Nothing is charged automatically. A cancel ends the plan at the end of the period, or at once with `at_period_end` set to `false`.

A playback token lasts about ten minutes. The response also gives a `session_id`, `refresh_before_expiry_seconds` and `heartbeat_interval_seconds`. Refresh the token before it expires and send a heartbeat as often as the response says. One device can watch an item at a time. Asking for a token on a second device moves playback there, and the first device gets `403` `SESSION_TAKEN_OVER` on its next refresh or heartbeat. Access is checked again on every refresh and progress ping, so a refund or an ended rental stops playback. An episode before its release date returns `ITEM_NOT_RELEASED`.

A video counts as complete at 95 percent. Text lessons are completed with `POST /v1/plus/watch/items/{item}/complete`. A certificate has a serial number. The public check shows the course title and the holder's first name only.

Content prices and order amounts are decimal KES, such as `500.00`. Payout and fee calls use cents.

## What you can call today

### Creators

| Method | Path | What it does |
|---|---|---|
| GET | `/v1/organizer/plus/contents` | List your content with status counts. |
| POST | `/v1/organizer/plus/contents` | Create a draft video, series or course. |
| GET | `/v1/organizer/plus/contents/{content}` | Get one with its items, trailer and review notes. |
| PUT | `/v1/organizer/plus/contents/{content}` | Update a draft or rejected content. |
| DELETE | `/v1/organizer/plus/contents/{content}` | Delete a draft, rejected or retired content. |
| POST | `/v1/organizer/plus/contents/{content}/request-approval` | Send it to tenant staff for review. |
| POST | `/v1/organizer/plus/contents/{content}/retire` | End it. Past purchases keep working. |
| GET | `/v1/organizer/plus/contents/{content}/items` | List episodes or lessons. |
| POST | `/v1/organizer/plus/contents/{content}/items` | Add an episode, lesson or chapter heading. |
| POST | `/v1/organizer/plus/contents/{content}/items/reorder` | Change the order of items. |
| GET | `/v1/organizer/plus/contents/{content}/items/{item}` | Get one item. |
| PUT | `/v1/organizer/plus/contents/{content}/items/{item}` | Edit an item's title, price or release date. |
| DELETE | `/v1/organizer/plus/contents/{content}/items/{item}` | Delete an item nobody has bought. |
| POST | `/v1/organizer/plus/contents/{content}/items/{item}/upload` | Start an upload for an item's video. |
| GET | `/v1/organizer/plus/contents/{content}/items/{item}/upload` | Check the upload. |
| DELETE | `/v1/organizer/plus/contents/{content}/items/{item}/upload` | Cancel the upload. |
| GET | `/v1/organizer/plus/contents/{content}/trailer` | Get the trailer and its upload state. |
| POST | `/v1/organizer/plus/contents/{content}/trailer` | Add a trailer and start its upload. |
| POST | `/v1/organizer/plus/contents/{content}/trailer/replace` | Replace the trailer. |
| DELETE | `/v1/organizer/plus/contents/{content}/trailer` | Remove the trailer. |
| GET | `/v1/organizer/plus/contents/{content}/fee-preview` | See what you keep from a gross amount. |
| POST | `/v1/organizer/plus/payout-requests` | Ask to be paid out. Owners only. |

### Viewers and the public (sign-in optional)

| Method | Path | What it does |
|---|---|---|
| GET | `/v1/plus/contents` | Browse the catalogue. Filter by `category`, `type` or `search`. |
| GET | `/v1/plus/contents/categories/top` | List the top categories. |
| GET | `/v1/plus/contents/{content}` | Get one content with its items. |
| GET | `/v1/plus/contents/{content}/trailer` | Get the trailer details. |
| POST | `/v1/plus/contents/{content}/trailer/playback-token` | Get a token to play the trailer. |
| POST | `/v1/plus/orders` | Buy content. Returns the order and a `checkout_url`. |
| GET | `/v1/plus/orders/{code}` | Check an order by its code. |
| GET | `/v1/plus/subscription-plans` | List active plans. |
| POST | `/v1/plus/contents/{content}/items/{item}/playback-token` | Get a token to play an item. Send the `order_code` or `buyer_email` if the viewer is a guest. |
| POST | `/v1/plus/playback-tokens/refresh` | Get a fresh token for a `session_id`. |
| POST | `/v1/plus/playback-tokens/heartbeat` | Keep the session alive and learn if another device took over. |
| GET | `/v1/plus/certificates/verify/{serial}` | Check a certificate by its serial number. |

### Signed-in viewers

| Method | Path | What it does |
|---|---|---|
| POST | `/v1/plus/subscriptions/checkout` | Start a plan. Returns a `checkout_url`. |
| POST | `/v1/plus/subscriptions/{subscription}/renew` | Pay for the next month. |
| GET | `/v1/plus/my-subscriptions` | List the viewer's plans. |
| POST | `/v1/plus/my-subscriptions/{subscription}/cancel` | Cancel a plan. |
| GET | `/v1/plus/my-library` | List the viewer's active purchases. |
| POST | `/v1/plus/watch/ping` | Report position and time watched. |
| POST | `/v1/plus/watch/items/{item}/complete` | Mark a text lesson done. |
| GET | `/v1/plus/watch/items/{item}/progress` | Get the viewer's progress on an item. |
| GET | `/v1/plus/my-certificates` | List the viewer's certificates. |
| GET | `/v1/plus/my-certificates/{certificate}` | Get one certificate by serial number. |

Tenant staff review content and manage plans from the tenant admin API. Full parameters and responses are in the [API reference](https://docs.vabu.live/api.md).

Here is a guest buying a video:

```sh
curl -X POST https://api.vabu.live/v1/plus/orders \
  -H "X-Tenant-Code: $TENANT_CODE" \
  -H "Content-Type: application/json" \
  -d '{
    "content_id": "550e8400-e29b-41d4-a716-446655440001",
    "purchase_type": "lifetime",
    "customer": { "name": "Amina Wairimu", "email": "amina@example.co.ke", "phone": "+254712345678" }
  }'
```

The order comes back `pending` with a `code` such as `PLS-A1B2-C3D4` and a `checkout_url`. Once the buyer pays, that code lets them play the video.

## Calling these endpoints

The viewer and public routes marked sign-in optional take only `X-Tenant-Code`, and the certificate check needs nothing. `GET /v1/plus/contents` and `GET /v1/plus/contents/{content}` also accept a secret, restricted (`streams:read`) or publishable key. Every other route takes a signed-in user token (`Authorization: Bearer ...`, a Vabu ID or Payd ID token) plus `X-Tenant-Code`, in live mode. Creator routes also accept an optional `X-Organization-Id` header to pick which organization you act for. Test keys and publishable keys are refused on these routes today, apart from the two catalogue reads. See [which credentials each endpoint takes](https://docs.vabu.live/concepts/keys-and-scopes.md#which-credentials-each-endpoint-takes).

On creator routes, any active member can read, and changes need the Owner, Admin or Editor role. For paging and error shapes, see [pagination](https://docs.vabu.live/concepts/pagination.md) and [errors](https://docs.vabu.live/concepts/errors.md).

## Events you can subscribe to

| Event | When it fires |
|---|---|
| `streams.content.purchased` | A viewer paid for a video, series or episode and now has access. |
| `access.entitlement.granted` | A purchase gave a viewer access. |
| `access.entitlement.extended` | A viewer's access end date changed. |
| `access.entitlement.revoked` | A viewer's access was taken away, for example after a refund. |
| `access.entitlement.expired` | A viewer's access ran out, for example a finished rental. |

See [events](https://docs.vabu.live/events/index.md) for the full payloads and [webhooks](https://docs.vabu.live/concepts/webhooks.md) for delivery and signatures.

## Related

- [Streams](https://docs.vabu.live/guides/streams/index.md) for live broadcasts.
- [Paid access](https://docs.vabu.live/guides/streams/paid-access.md) for the access model behind these purchases.
- [Payments](https://docs.vabu.live/products/payments.md) for checkout and payouts.
- [Fanbases](https://docs.vabu.live/products/fanbases.md) for the community around a creator.
