OI Payments Docs
Admin dashboard

Subscriptions (operations)

Drive the subscription billing sweeps, search and inspect subscriptions across applications, and cancel, pause, or resume them from the admin dashboard.

The admin subscription surface is for operators, not integrating apps. It does three things: it lets you trigger the recurring-billing sweeps on demand (the same ones the schedulers run), it gives you a cross-app read (search + detail) over every subscription your grants cover, and it lets you cancel, pause, or resume an individual subscription through the same ops engine the app API uses.

App-facing subscription creation and the customer-managed lifecycle live in the Subscriptions guide and the Catalog & subscriptions concept. This page is the operator's view.

All endpoints below are admin, session-authenticated — send your session token:

-H "Authorization: Bearer SESSION_TOKEN"

Every response uses the standard envelope: { "data": …, "meta": { "success", "message", "errorCode", "timestamp" }, "pagination": … }. The examples show just the data payload after the first one. See Responses & errors for the full shape.

Money and mode

Subscription cycle invoices are billed in integer minor units (paisa)150000 is 1,500.00 BDT — and currency is BDT-only in v1. See Money. Every subscription carries an app_id and a mode (TEST/LIVE); one app never sees another app's or another mode's data. See App isolation and Modes.

The subscription lifecycle

A subscription moves through five states. A subscription created against a trial price starts TRIALING (no charge yet; the first invoice falls at trial end); without a trial it starts ACTIVE (billed from its start date). A TRIALING or ACTIVE subscription is entitling — see Entitlements.

StatusMeaning
TRIALINGIn a trial; no charge yet. The first invoice falls at trial end. Entitling.
ACTIVEBilled each cycle from the start date. Entitling.
PAST_DUEA renewal payment failed; the subscription is in the dunning window.
PAUSEDThe app (or an operator) paused billing. No cycles are billed until resumed.
CANCELEDTerminal. Reached by an at-period-end or immediate cancel, or by dunning exhaustion.

Permissions and scope

Reads gate on subscription:read; writes gate on subscription:write. The two behave differently with respect to app scope.

OperationEndpointAuthorityApp scope
Billing sweepPOST /admin/subscriptions/billing/runsubscription:writeNone — system-wide
Trial-reminder sweepPOST /admin/subscriptions/trial-reminderssubscription:writeNone — system-wide
Dunning sweepPOST /admin/subscriptions/dunning/runsubscription:writeNone — system-wide
SearchGET /admin/subscriptionssubscription:readNarrowed to your grants
DetailGET /admin/subscriptions/{id}subscription:readNarrowed to your grants
CancelPOST /admin/subscriptions/{id}/cancelsubscription:writePer-app, on the owning app
PausePOST /admin/subscriptions/{id}/pausesubscription:writePer-app, on the owning app
ResumePOST /admin/subscriptions/{id}/resumesubscription:writePer-app, on the owning app
MetricsGET /admin/subscriptions/metricssubscription:readPer-app (zeros if out of scope)

The three sweeps are system-wide

The billing, trial-reminder, and dunning sweeps gate only on subscription:write and carry no app scope — they process every app's subscriptions in both modes, exactly as the schedulers do. Treat subscription:write for sweep triggers as a privileged, ops-level grant.

Out-of-scope behaviour differs by verb. A read of a subscription outside your grants is indistinguishable from a missing one — GET /admin/subscriptions/{id} returns 404 so the dashboard never leaks that another app's subscription exists, and a search simply omits it. A write resolves the subscription's owning app first: a non-existent id is 404, while an id that exists but sits outside your grants is 403. Metrics for an app you are not scoped to returns all-zero counts, not an error. App scope (RBAC-4) is enforced inside each use case — see RBAC.

Mode is never supplied by the operator

The mode (TEST or LIVE) of a subscription is a property of the subscription — derived from the app credential that originally created it, never from request input. For cancel, pause, and resume, the server resolves the subscription's owning app_id + mode cross-app and scopes the op exactly as the app path would; the client never states — and could never misstate — a subscription's mode. The one place mode is an explicit input is the metrics query parameter (see below), mirroring the dashboard analytics endpoints.

On-demand sweeps

The recurring-billing machinery runs on schedulers by default. These three POST endpoints fire the same use cases on demand — useful for testing, for catching up after downtime, or for an operator-driven run. Each returns a per-run tally and each is resilient: a single bad subscription is logged and skipped so it never aborts the rest of the run, and per-row pessimistic locking makes a concurrent sweep safe (a candidate that is no longer eligible under the lock is counted as skipped). The sweeps are not audited; cancel/pause/resume are.

Configuration

Each sweep has an enable flag, an interval, and a tuning knob. The interval and tuning values default as shown; override them with the env-mapped properties.

SweepEnable flag (default)Interval property (default)Tuning property (default)
Billingapp.subscription.billing-sweep.enabled (true)app.subscription.billing-sweep.interval-ms (3600000 — 1h)
Dunningshares app.subscription.billing-sweep.enabled (true)app.subscription.dunning.interval-ms (3600000 — 1h)app.subscription.dunning.grace-days (7)
Trial reminderapp.subscription.trial-reminder.enabled (true)app.subscription.trial-reminder.interval-ms (86400000 — 24h)app.subscription.trial-reminder.lead-days (3)

Behavioural rules these knobs drive:

  • Billing bills every due TRIALING/ACTIVE cycle and matures any subscription whose at-period-end cancel has come due (it is canceled with no further invoice).
  • Trial reminder fires on the exact lead day: it selects TRIALING subscriptions whose trialEndDate equals today + lead-days (not <=), and emits one subscription.trial_will_end notification each. Running once a day therefore notifies each subscription exactly once, with no "reminder sent" flag.
  • Dunning cancels every subscription stuck PAST_DUE whose past_due_since is on or before now − grace-days (i.e. past the grace window). A subscription whose payment recovers before the grace window expires returns to ACTIVE and is never touched by this sweep.

Run a sweep

curl -X POST http://localhost:8080/api/v1/admin/subscriptions/billing/run \
  -H "Authorization: Bearer SESSION_TOKEN"

Full envelope:

{
  "data": { "candidates": 12, "billed": 9, "canceled": 2, "skipped": 1, "failed": 0 },
  "meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T12:00:00Z" },
  "pagination": null
}
curl -X POST http://localhost:8080/api/v1/admin/subscriptions/trial-reminders \
  -H "Authorization: Bearer SESSION_TOKEN"
{ "candidates": 4, "notified": 4, "failed": 0 }
curl -X POST http://localhost:8080/api/v1/admin/subscriptions/dunning/run \
  -H "Authorization: Bearer SESSION_TOKEN"
{ "candidates": 3, "canceled": 2, "skipped": 1, "failed": 0 }

Run-summary fields

BillingRunSummaryPOST /admin/subscriptions/billing/run

FieldTypeNotes
candidatesintDue candidate subscriptions the sweep examined.
billedintCycles successfully billed (invoice issued + calendar advanced).
canceledintSubscriptions canceled at their scheduled period end (no invoice).
skippedintCandidates no longer due under the lock; left untouched.
failedintCandidates whose billing threw; logged and skipped, run continued.

TrialReminderRunSummaryPOST /admin/subscriptions/trial-reminders

FieldTypeNotes
candidatesintTrialing subscriptions whose trial ends on the lead day.
notifiedintReminders emitted (subscription.trial_will_end).
failedintRows that threw while processing; skipped.

DunningCancelRunSummaryPOST /admin/subscriptions/dunning/run

FieldTypeNotes
candidatesintPAST_DUE subscriptions selected as beyond the grace window.
canceledintSubscriptions actually canceled (PAST_DUECANCELED).
skippedintRows no longer eligible when re-checked under the lock.
failedintRows that threw while processing; skipped.

The subscription.trial_will_end event is delivered to the owning app like any other webhook — see Webhooks and Webhooks & logs.

Cross-app search and detail

GET /admin/subscriptions returns a paginated, filterable list of subscription summaries across every app your subscription:read grants cover (a Super Admin or holder of a global grant is unrestricted). Results are automatically narrowed to your scope.

Query parameters

FieldTypeRequiredNotes
pageintNoZero-based page index. Default 0.
sizeintNoPage size. Default 20.
sortBystringNoSort field. Default createdAt.
orderASC | DESCNoSort direction. Default DESC.
paginatebooleanNofalse returns the full set unpaged. Default true.
applicationIdLongNoRestrict to one app within your scope.
modeTEST | LIVENoFilter by mode.
statusenumNoTRIALING, ACTIVE, PAST_DUE, PAUSED, or CANCELED.
customerReferencestringNoThe app's external customer id. Requires applicationId to resolve — see below.

customerReference requires applicationId

customerReference is the app's external customer id, which is only unique within an app. The server resolves it to an internal id keyed by applicationId, so a customerReference filter without an applicationId cannot resolve and yields an empty page — as does a reference the app has never seen. Always pair the two.

curl -G http://localhost:8080/api/v1/admin/subscriptions \
  -H "Authorization: Bearer SESSION_TOKEN" \
  --data-urlencode "applicationId=42" \
  --data-urlencode "mode=LIVE" \
  --data-urlencode "status=PAST_DUE"
{
  "data": [
    {
      "id": 9007,
      "status": "PAST_DUE",
      "productId": 310,
      "priceId": 540,
      "startDate": "2026-01-15",
      "nextBillingDate": "2026-06-15",
      "cancelAtPeriodEnd": false,
      "mode": "LIVE",
      "createdAt": "2026-01-15T09:30:00Z"
    }
  ],
  "meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T12:00:00Z" },
  "pagination": { "page": 0, "size": 20, "totalElements": 1, "totalPages": 1 }
}

SubscriptionSummaryDto (list row)

FieldTypeNotes
idLongSubscription id.
statusenumTRIALING | ACTIVE | PAST_DUE | PAUSED | CANCELED.
productIdLongThe product the subscription bills against.
priceIdLongThe recurring price.
startDatedateWhen billing started (yyyy-MM-dd).
nextBillingDatedateNext scheduled cycle date.
cancelAtPeriodEndbooleanA cancel is scheduled for the current period end.
modeTEST | LIVEThe subscription's mode.
createdAtdatetimeCreation timestamp.

Detail

GET /admin/subscriptions/{id} returns the full read model for one subscription, including the resolved external customerReference. A subscription outside your scope reads as 404, exactly like a missing id.

curl http://localhost:8080/api/v1/admin/subscriptions/9007 \
  -H "Authorization: Bearer SESSION_TOKEN"
{
  "id": 9007,
  "status": "PAST_DUE",
  "productId": 310,
  "priceId": 540,
  "customerReference": "cust_a1b2c3",
  "startDate": "2026-01-15",
  "currentPeriodStart": "2026-05-15",
  "nextBillingDate": "2026-06-15",
  "cancelAtPeriodEnd": false,
  "mode": "LIVE",
  "createdAt": "2026-01-15T09:30:00Z",
  "updatedAt": "2026-06-16T02:10:00Z"
}

SubscriptionDto (detail)

FieldTypeNotes
idLongSubscription id.
statusenumTRIALING | ACTIVE | PAST_DUE | PAUSED | CANCELED.
productIdLongThe product the subscription bills against.
priceIdLongThe recurring price.
customerReferencestringThe app's external customer id; attached by the use case, not stored on the entity.
startDatedateWhen billing started.
currentPeriodStartdateStart of the current billing period.
nextBillingDatedateNext scheduled cycle date.
trialEndDatedateTrial end; omitted when there is no trial (NON_NULL).
cancelAtPeriodEndbooleanA cancel is scheduled for the current period end.
canceledAtdatetimeWhen the subscription was canceled; omitted until canceled.
modeTEST | LIVEThe subscription's mode.
createdAtdatetimeCreation timestamp.
updatedAtdatetimeLast update timestamp.

Cancel, pause, and resume

These three writes drive the same ops engine as the app API — there is no parallel admin path. Each resolves the subscription's owning app_id + mode cross-app, enforces the per-app subscription:write scope (403 if you lack it on that app), runs the op, and writes an append-only audit entry atomically with the change. A non-existent id is 404.

OpEndpointAudit actionBody
CancelPOST /admin/subscriptions/{id}/cancelSUBSCRIPTION_CANCELEDCancelSubscriptionRequest (optional)
PausePOST /admin/subscriptions/{id}/pauseSUBSCRIPTION_PAUSEDnone
ResumePOST /admin/subscriptions/{id}/resumeSUBSCRIPTION_RESUMEDnone

CancelSubscriptionRequest

FieldTypeRequiredNotes
atPeriodEndBooleanNotrue (the default) keeps billing through the paid period, then cancels; false cancels immediately. A null field — or an entirely absent body — is treated as true (cancel at period end).
curl -X POST http://localhost:8080/api/v1/admin/subscriptions/9007/cancel \
  -H "Authorization: Bearer SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "atPeriodEnd": true }'
{
  "id": 9007,
  "status": "PAST_DUE",
  "productId": 310,
  "priceId": 540,
  "customerReference": "cust_a1b2c3",
  "startDate": "2026-01-15",
  "currentPeriodStart": "2026-05-15",
  "nextBillingDate": "2026-06-15",
  "cancelAtPeriodEnd": true,
  "mode": "LIVE",
  "createdAt": "2026-01-15T09:30:00Z",
  "updatedAt": "2026-06-30T12:00:00Z"
}

With atPeriodEnd: true the subscription keeps its status and nextBillingDate but flips cancelAtPeriodEnd to true; the next billing sweep matures the cancel when the period ends. With atPeriodEnd: false the subscription transitions to CANCELED immediately. Pause and resume take no body:

curl -X POST http://localhost:8080/api/v1/admin/subscriptions/9007/pause \
  -H "Authorization: Bearer SESSION_TOKEN"

curl -X POST http://localhost:8080/api/v1/admin/subscriptions/9007/resume \
  -H "Authorization: Bearer SESSION_TOKEN"

Both return the updated SubscriptionDto.

Metrics

GET /admin/subscriptions/metrics returns the per-app, mode-aware subscription counts for the dashboard metrics row. Both applicationId and mode are required query parameters.

Mode is a required parameter here

Unlike the app API — where the mode is derived from the credential, never the request — this metrics endpoint takes mode as a required query parameter, the same pattern as the dashboard analytics endpoints. If you are not scoped to the requested applicationId, the response is all zeros (RBAC-4), never an error.

FieldTypeRequiredNotes
applicationIdLongYesThe app to report on.
modeTEST | LIVEYesWhich mode's counts to return.
fromdateNoWindow start, inclusive (yyyy-MM-dd). Defaults to an open lower bound.
todateNoWindow end, inclusive by day (the window is [from, to + 1 day)). Defaults to an open upper bound.
curl -G http://localhost:8080/api/v1/admin/subscriptions/metrics \
  -H "Authorization: Bearer SESSION_TOKEN" \
  --data-urlencode "applicationId=42" \
  --data-urlencode "mode=LIVE" \
  --data-urlencode "from=2026-06-01" \
  --data-urlencode "to=2026-06-30"
{
  "totalCount": 128,
  "activeCount": 96,
  "trialingCount": 12,
  "pastDueCount": 7,
  "canceledCount": 13
}

SubscriptionMetricsDto

FieldTypeNotes
totalCountLongAll subscriptions created in the [from, to) window. Always a concrete number, never null.
activeCountLongOf those window subscriptions, the count whose current status is ACTIVE.
trialingCountLongOf those window subscriptions, the count whose current status is TRIALING.
pastDueCountLongOf those window subscriptions, the count whose current status is PAST_DUE.
canceledCountLongOf those window subscriptions, the count whose current status is CANCELED.

On this page