Payment Orchestrator
Subscription
Plans

Plans

A plan turns a product into something a customer can subscribe to: a price, a currency, an optional trial, and a billing rule that tells the engine when to generate each invoice. The plan also owns the failure story — how many times a failed charge is retried (maxDunningAttempts), on which days (dunningScheduleDays), and how long the customer keeps access while payment is being chased (gracePeriodDays).

price is a minor-unit integer (kuruş): "price": 49900 = ₺499.00.

Endpoints

All Bearer-authenticated (at_live_…); lists use cursor pagination (pageSize, pageToken, filter by productId / includeArchived).

MethodPathDoes
POST/api/v1/subscriptions/plansCreate a plan
GET/api/v1/subscriptions/plansList (filter productId, includeArchived)
GET/api/v1/subscriptions/plans/:idFetch one plan
PATCH/api/v1/subscriptions/plans/:idUpdate
DELETE/api/v1/subscriptions/plans/:idArchive (soft delete)
GET/api/v1/subscriptions/plans/:id/translationsList translations
PATCH/api/v1/subscriptions/plans/:id/translations/:localeCreate or update a translation

Create a plan

curl -X POST https://api-gateway.lunixi.com/api/v1/subscriptions/plans \
  -H "Authorization: Bearer at_live_5f3e2b1a..." \
  -H "Content-Type: application/json" \
  -d '{
    "productId": "prd_4b2e88",
    "code": "pro-monthly",
    "name": "Pro Monthly",
    "price": 49900,
    "currency": "TRY",
    "trialPeriodDays": 7,
    "billing": {
      "frequency": "MONTHLY",
      "interval": 1,
      "byMonthDay": [15],
      "timezone": "Europe/Istanbul",
      "lastDayOfMonthFix": true
    },
    "maxDunningAttempts": 3,
    "dunningScheduleDays": [1, 3, 7],
    "gracePeriodDays": 7
  }'

Fields

FieldTypeNotes
productIdstring, requiredThe product this plan prices.
codestring, requiredYour stable identifier (e.g. pro-monthly), unique per merchant.
namestring, requiredCustomer-facing name (base locale).
descriptionstringCustomer-facing description.
priceinteger, requiredMinor units. 49900 = ₺499.00.
currencystring, requiredISO code, e.g. TRY.
trialPeriodDaysinteger ≥ 0Free trial length; the subscription starts in trial and converts automatically. 0/omitted = no trial.
billingobject, requiredThe billing rule — see below.
maxDunningAttemptsinteger 0–10How many retries after a failed charge before the invoice is given up on.
dunningScheduleDaysinteger[]Days after the failure on which retries run — [1, 3, 7] retries on day 1, 3 and 7.
gracePeriodDaysinteger ≥ 0How long the subscription retains access while dunning is in progress.
metadataobjectFree-form key/value store.

The billing rule

The billing object is a recurrence rule — the same building blocks as an RRULE:

FieldTypeMeaning
frequencyDAILY | WEEKLY | MONTHLY | YEARLY, requiredBase cadence.
intervalinteger 1–365, requiredEvery N frequency units — frequency: MONTHLY, interval: 3 bills quarterly.
byMonthDayinteger[]Pin billing to specific days of the month, e.g. [15].
byWeekDaystring[]For weekly cadences: ["MO", "WE", "FR"].
timezonestring, requiredIANA zone the rule is evaluated in, e.g. Europe/Istanbul. Billing "on the 15th" means the 15th in this zone.
lastDayOfMonthFixboolean, requiredWith true, a cycle anchored on the 31st bills on the last day of shorter months instead of skipping them.
countinteger ≥ 1Total number of occurrences; omit (null) for an open-ended subscription.

Recipes:

{ "frequency": "MONTHLY", "interval": 1,  "timezone": "Europe/Istanbul", "lastDayOfMonthFix": true }          // classic monthly
{ "frequency": "MONTHLY", "interval": 3,  "byMonthDay": [1], "timezone": "Europe/Istanbul", "lastDayOfMonthFix": true }  // quarterly, on the 1st
{ "frequency": "WEEKLY",  "interval": 1,  "byWeekDay": ["MO"], "timezone": "Europe/Istanbul", "lastDayOfMonthFix": false } // every Monday
{ "frequency": "YEARLY",  "interval": 1,  "timezone": "Europe/Istanbul", "lastDayOfMonthFix": true, "count": 3 }          // 3 annual charges, then done

Update & archive

PATCH /api/v1/subscriptions/plans/:id updates name, description, price, trialPeriodDays, billing, the dunning fields and metadata (plus an audit reason). Whether and when a price update reaches existing subscribers is governed by the product/subscription price policy (LOCKED / FOLLOW_PLAN, IMMEDIATE / NEXT_PERIOD) — see Products.

DELETE /api/v1/subscriptions/plans/:id archives the plan: it can no longer be subscribed to and leaves the public catalog, but existing subscriptions on it keep billing normally.

curl -X DELETE https://api-gateway.lunixi.com/api/v1/subscriptions/plans/pln_9f3a1c \
  -H "Authorization: Bearer at_live_5f3e2b1a..." \
  -H "Content-Type: application/json" \
  -d '{ "reason": "superseded by pro-monthly-v2" }'

Translations & display text

Plan name / description localize exactly like products — PATCH …/plans/:id/translations/:locale with { "name": …, "description": … }, served automatically wherever a locale is passed (public catalog, portal).

The marketing bullet lines under a plan on your pricing page — "500 transactions / month", "Priority support" — are the plan's display features: ordered, per-locale display texts managed from the merchant panel and returned with the plan in the Public Catalog API. They are presentation only; enforceable quotas live in Features & entitlements.

Response

{
  "id": "pln_9f3a1c",
  "productId": "prd_4b2e88",
  "code": "pro-monthly",
  "name": "Pro Monthly",
  "price": 49900,
  "currency": "TRY",
  "trialPeriodDays": 7,
  "billing": {
    "frequency": "MONTHLY",
    "interval": 1,
    "byMonthDay": [15],
    "timezone": "Europe/Istanbul",
    "lastDayOfMonthFix": true
  },
  "maxDunningAttempts": 3,
  "dunningScheduleDays": [1, 3, 7],
  "gracePeriodDays": 7,
  "isActive": true,
  "createdAt": "2026-07-06T09:31:00Z"
}

Switching plans

Moving an existing subscriber to another plan is an operation on the subscription, not the plan: POST /api/v1/subscriptions/subscriptions/:id/change-plan with behavior: "IMMEDIATE" (switch and settle now) or "NEXT_PERIOD" (schedule for the period boundary). Upgrades, downgrades and the events they emit are covered in Subscriptions.

Errors

StatusMeaningWhat to do
400Validation failed (negative price, interval out of 1–365, maxDunningAttempts > 10, empty dunningScheduleDays)Fix the flagged fields.
401Missing or invalid API keySend Authorization: Bearer at_live_….
404productId or plan not foundCreate the product first; check the id.
409Duplicate plan codeCodes are unique per merchant.

Webhooks

EventFired when
plan.createdA plan was created.
plan.updatedFields, price or translations changed.
plan.archivedThe plan was archived via DELETE.

Next steps

  • Subscriptions — subscribe customers, cancel, pause, change plan
  • Products — price-change policy and catalog translations
  • Pricing & Portal SDK — render these plans on your own pricing page
  • Invoices — what the billing rule produces each period