Admin / Studio Premium — Frontend Integration
Covers two flows:
- Configuring a channel's premium plans (Update Channel Premium)
- Listing premium subscriptions
Both flows are server-rendered through PicTv.Api minimal-API endpoints. All examples assume the standard auth cookie/header used by the existing studio/admin clients.
1. Update Channel Premium
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/studio/v1/channels/{id}/premium |
Read current premium config for the channel (existing plans, prices, currency, active flag). |
PUT |
/studio/v1/channels/{id}/premium |
Create or update the channel's premium plans on Stripe + DB. |
Note on the route prefix: this lives under
/studio/v1/..., not/admin/v1/.... It is gated byCreatorAuthorizationFilter, so the caller must be the channel's creator. If a back-office admin needs to edit any channel's plans, a parallel/admin/v1/channels/{id}/premiumroute + handler is required — it does not exist today.
GET response shape (StudioPremiumProductDto)
{
"id": "prd_…",
"entityType": 1,
"entityId": "ch_…",
"isActive": true,
"premiumPlans": [
{ "id": "pln_…", "name": "Channel - Monthly", "price": 9.99, "currency": "brl", "type": 3 },
{ "id": "pln_…", "name": "Channel - SixMonth", "price": 49.99, "currency": "brl", "type": 5 },
{ "id": "pln_…", "name": "Channel - Yearly", "price": 89.99, "currency": "brl", "type": 4 }
]
}
type is PremiumPlanType:
0Trial1Daily2Weekly3Monthly4Yearly5SixMonth
PUT request body (UpdateChannelPremiumCommand)
{
"isPremium": true,
"monthly": true,
"sixMonth": true,
"yearly": true,
"monthlyPrice": 9.99,
"sixMonthPrice": 49.99,
"yearlyPrice": 89.99,
"trialDays": 7,
"currency": "brl"
}
Field semantics:
| Field | Type | Notes |
|---|---|---|
isPremium |
bool | Master switch. The PUT no-ops if false (and no plan flag is on). |
monthly, sixMonth, yearly |
bool | Per-plan toggles. At least one must be true for the request to do anything. |
monthlyPrice, sixMonthPrice, yearlyPrice |
decimal | Amount in the chosen currency (e.g. 9.99 BRL = 999 cents Stripe-side). |
trialDays |
int? | Optional, applied to all prices created in this request. Stored on the Stripe Price's recurring.trial_period_days. |
currency |
string | ISO 4217 lowercase, default brl. |
Behavior the UI must reflect
- Plans are created on first save, then become immutable Stripe Prices. Once a plan exists for a channel + type, the backend will not recreate it. The UI should treat existing plans as read-only for
priceandtrialDaysafter creation — only the toggle and currency for new plans matter. - Editing a price when active subs exist throws. The handler currently rejects with
"There are active subscriptions for the {monthly|six-month|yearly} plan. You cannot update this plan!"— surface this as an inline error on the price field, not a toast, so the creator knows which row is the problem. - Trial is per-Price, not per-Subscription. Changing
trialDaysafter a plan exists is silently ignored (Stripe Prices are immutable). Disable the trial field after first save, or label it "Trial — set at creation time only". - No partial success rollback. If the monthly plan creates but the yearly fails (e.g. Stripe error), the monthly is already persisted. Re-submitting is safe — the handler skips plans that already exist.
Error responses
| HTTP | When | Body |
|---|---|---|
404 |
Channel not found for the caller | NotFoundResponse |
400 |
Stripe failure, validation, or "active subscriptions exist" | BadRequestResponse with message |
Suggested form layout
[ ] Enable premium for this channel (isPremium)
Currency: [BRL ▼] (currency)
Trial period (days, applied to all plans): [ 7 ] (trialDays)
[ ] Monthly Price: [ 9.99 ] (monthly + monthlyPrice)
[ ] Six months Price: [ 49.99 ] (sixMonth + sixMonthPrice)
[ ] Yearly Price: [ 89.99 ] (yearly + yearlyPrice)
[ Save ]
On mount: GET /studio/v1/channels/{id}/premium → prefill toggles, prices, disable already-created rows' price/trial inputs.
2. List Premium Subscriptions
Endpoint
| Method | Path | Auth |
|---|---|---|
GET |
/admin/v1/premium/subscriptions |
UserAuthorizationFilter |
Query parameters
| Param | Type | Required | Notes |
|---|---|---|---|
page |
int | yes | 1-based. |
pageSize |
int | no | Defaults to 10. |
channelId |
string | no | When provided, filters to that channel and forces entityType = Channel. |
status |
PremiumSubscriptionStatus |
no | Numeric enum. See table below. |
PremiumSubscriptionStatus values:
0Pending1Active2Expired3Canceled4Cancelling5Retry6Refunded7Trial8GrantAccess
Important caveat about the current handler. Despite the route being under
/admin/v1/..., the handler filters byx.UserId == _applicationContext.User.Id— i.e. it only returns subscriptions belonging to the calling user, not all users in the system. If the admin UI needs a system-wide list, the handler needs to be changed (drop theUserIdfilter, or accept an explicituserIdquery param). Document this with backend before shipping the admin screen.
Response shape (Pagination<PremiumSubscriptionDto>)
{
"items": [
{
"id": "sub_…",
"entityType": 1,
"entityId": "ch_…",
"status": 1,
"startDate": "2026-01-15T10:00:00Z",
"endDate": null,
"activePeriodStart": "2026-05-15T10:00:00Z",
"activePeriodEnd": "2026-06-15T10:00:00Z",
"nextBillingDate": "2026-06-15T10:00:00Z",
"canceledAt": null,
"planType": 3,
"entity": {
"id": "ch_…",
"name": "Channel Name",
"image": "https://…"
}
}
],
"totalCount": 42
}
entity is populated by the backend for channel subscriptions (entityType = 1). Other entity types return entity = null today.
Suggested table layout
| Channel | Plan | Status | Period | Next billing | Actions |
|---|---|---|---|---|---|
entity.image + entity.name |
label of planType |
colored chip from status |
activePeriodStart → activePeriodEnd |
nextBillingDate |
(cancel, refund — separate endpoints) |
Filters bar
- Status dropdown — multi-state, defaults "All".
- Channel typeahead → sets
channelId. - Page size — 10 / 25 / 50.
Related endpoints (not covered in depth here)
| Method | Path | Purpose |
|---|---|---|
POST |
/admin/v1/premium/grant-access |
GrantStudioPremiumAccessCommand { userId } — grant free access. |
PUT |
/admin/v1/premium/remove-access |
RevokeStudioPremiumAccessCommand — revoke a granted access. |
POST |
/client/v1/premium/subscriptions/cancel |
Cancel at period end (called from the user-facing app, not admin). |
Open items / TODO before shipping
- Confirm whether
/admin/v1/premium/subscriptionsshould drop theUserIdfilter for true admin scope. - Decide on price-update strategy when active subscriptions exist (grandfather vs migrate vs block — currently blocks). See
docs/channel-tiered-subscriptions.mdand theUpdateChannelPremiumCommandHandlerreview. - No dedicated
admin/v1/channels/{id}/premiumroute exists; if back-office admins need to edit any channel's premium config (not just creators editing their own), add it.