# Paid access

You sell the pass in your own checkout, with your own payment provider. Vabu decides who gets to press play. The link between the two is one call from your server: grant an **entitlement** after the payment clears.

An entitlement says that one customer may use one resource, for a period, within limits. The resource is a live stream (type `ls`) or an asset (type `ast`).

## Grant access after a payment

Make this call from your server when your payment provider confirms the money. Use a secret key, never a browser key.

```http
POST /v1/access/entitlements
Authorization: Bearer vk_live_...
Idempotency-Key: grant-pay-8842

{
  "customer": "user_42",
  "resource": { "type": "ls", "id": "ls_01J9ZQ4M8T3K6Y2E5W7N0B1C9D" },
  "source": "tenant_grant",
  "source_ref": "pay-8842",
  "starts_at": "2026-10-16T16:00:00Z",
  "ends_at": "2026-10-17T04:00:00Z",
  "limits": {
    "max_devices": 2,
    "max_concurrent_sessions": 1,
    "concurrency_policy": "replace_oldest"
  },
  "metadata": { "ticket": "VIP" }
}
```

You get `201` and an entitlement with `"status": "active"`.

| Field | Meaning |
|---|---|
| `customer` | Your own id for the buyer. It must match the `sub` of the customer token you sign for them. If you have no accounts, send `contact_hash` instead: a hash of a verified phone or email, salted with your own secret. |
| `resource` | What they may watch. `ls` for a live stream, `ast` for a video asset. |
| `source` | Why they have access, for your reports: `tenant_grant` (the default), `payment`, `comp`, `subscription`, `membership_tier`, `ticket` or `promotion`. |
| `source_ref` | The payment, order or grant behind it. Your payment id is a good value. |
| `starts_at`, `ends_at` | The window. Both need a time zone offset, such as `Z`. Leave them out for no limit. |
| `limits` | Device and viewing rules. See below. |
| `external_id`, `metadata` | Your own references. |

### Make it safe to repeat

Payment providers send the same confirmation more than once, so your handler will run more than once. This is safe. Two things protect you:

- The `Idempotency-Key` header is required. Send the same key and you get the stored response back.
- With a `source_ref`, a second grant for the same source, reference, customer and resource changes nothing and returns the existing entitlement with `200`.

A repeat of a revoked grant is refused with `409 entitlement_revoked`. If you want to give access again, use a new `source_ref`.

### Check before you show a button

`POST /v1/access/check` answers whether a customer may watch right now:

```json
{ "data": { "allowed": false, "reason": "expired", "entitlement": { "id": "ent_...", "status": "expired" } } }
```

`reason` is `null` when allowed. Otherwise it is `no_entitlement`, `expired`, `revoked`, `suspended`, `not_started`, `country_not_allowed` or `domain_not_allowed`. Use it to choose between "Watch" and "Buy a pass". The answer follows every change at once, including a revoke.

## Limit devices

The `limits` object decides what a second screen does.

| You want | Set |
|---|---|
| One screen at a time, the latest wins (a viewer who picks up their phone moves the stream there) | `max_concurrent_sessions: 1`, `concurrency_policy: "replace_oldest"` |
| One screen at a time, the first holds the seat | `max_concurrent_sessions: 1`, `concurrency_policy: "reject_new"` |
| A household with up to 2 devices in all | `max_devices: 2` |
| A pass that works for 3 viewings | `max_views: 3` |
| Only viewers in Kenya and Uganda | `allowed_countries: ["KE", "UG"]` |
| Only on your own site | `allowed_domains: ["example.com"]` |

Your page sends a `device_id` when it opens a session. Use one stable id for each browser or phone, for example one saved in local storage. Vabu counts different ids as different devices.

With `replace_oldest`, the older session ends at its next heartbeat, within 30 seconds, and its player gets `403 session_ended` with the reason `replaced_by_newer_session`. Show "You are watching on another device". With `reject_new`, the second device is refused with `409 concurrency_limit_reached`.

How sessions and heartbeats work is in [playback and sessions](https://docs.vabu.live/concepts/playback-and-sessions.md).

## Revoke access

Take access away when a payment is refunded or reversed:

```http
DELETE /v1/access/entitlements/ent_...
```

This is final. The response is the entitlement with `"status": "revoked"`. For a pause you can undo, use `POST /v1/access/entitlements/{id}/suspend` and `.../resume`.

What happens next:

- The next `check` says no, at once.
- A new session is refused at once.
- A session that is playing ends at its next heartbeat.
- The playback link already in the viewer's player works until its token runs out, at most about 11 minutes. See [signed playback](https://docs.vabu.live/concepts/signed-playback.md).

Find a customer's entitlements with `GET /v1/access/entitlements?customer=user_42`, or by your own id with `GET /v1/access/entitlements/external/{external_id}`.

## Sell replays

A replay is an asset, so it takes an entitlement of its own, with resource type `ast`.

1. Add the recording as an asset. See [assets and recordings](https://docs.vabu.live/concepts/assets-and-recordings.md). Wait for `streams.asset.ready`.
2. When someone buys the replay, grant an entitlement on the asset, with an `ends_at` that matches what you sold, for example 7 days.
3. To include the replay in a live pass, grant two entitlements in the same handler: one on the stream and one on the asset, with a later `ends_at` on the asset.

The viewer opens a session on the asset exactly as on a live stream. Because the replay entitlement has its own limits, you can sell a replay at one screen per purchase.

## Tickets that include a stream

If you sell tickets with Vabu Events, listen for `events.order.paid` and grant the stream in your handler, with `source: "ticket"`, the order code as `source_ref` and your own id for the buyer. See [how the products work together](https://docs.vabu.live/products/how-products-work-together.md).

## Keep your records in step

Every grant, revoke, suspend, resume and expiry sends an event: `access.entitlement.granted`, `revoked`, `suspended`, `resumed`, `expired` and `extended`. Use them to update your own database. See [stream events](https://docs.vabu.live/concepts/stream-events.md).
