plans

Stable. Describes behaviour that is built and covered by tests in this repository.

API reference

GET /v1/plans #

List plans

Not paged: the response is the complete list.

Responses

StatusMeaningBody
200OKListPlansResponse
Example response
{
  "plans": [
    {
      "id": "0192f0b4-6f6e-7c3a-9a3e-6a1f2b3c4d5e",
      "code": "string",
      "name": "string",
      "latest_version": 1,
      "create_time": "2026-11-03T10:15:00Z"
    }
  ]
}
curl
curl -X GET https://YOUR-API-ADDRESS/v1/plans \
  -H "Authorization: Bearer $PAYRUDDER_API_KEY"

POST /v1/plans #

Create a plan

Request body

FieldTypeDescription
code requiredstring
name requiredstring
Example
{
  "code": "string",
  "name": "string"
}

Responses

StatusMeaningBody
201CreatedPlan
400Error envelopeError
409Error envelopeError
Example response
{
  "id": "0192f0b4-6f6e-7c3a-9a3e-6a1f2b3c4d5e",
  "code": "string",
  "name": "string",
  "latest_version": 1,
  "create_time": "2026-11-03T10:15:00Z"
}
curl
curl -X POST https://YOUR-API-ADDRESS/v1/plans \
  -H "Authorization: Bearer $PAYRUDDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"code":"string","name":"string"}'

POST /v1/plans/{code}/versions #

Publish the next immutable plan version

Parameters

NameInTypeDescription
code requiredpathstring

Request body

FieldTypeDescription
base_fee_minorstring · int64
feesarray of CurrencyFeeInputFixed fees other than the tenant currency's monthly one (base_fee_minor), per currency and interval; no duplicates. A yearly subscription needs a yearly fee in its currency, including the tenant's
pricesarray of PriceInput
entitlementsarray of EntitlementInput
trial_daysintegerThe trial a new subscription to this version gets unless its request says otherwise; 0 is none
prorate_after_trialbooleanLeft out: true. When a trial ends inside a billing period, charge the first period from the trial's end to the period's end instead of the whole fee; false charges the whole fee as versions published before this rule do
Example
{}

Responses

StatusMeaningBody
201CreatedPlanVersion
400Error envelopeError
404Error envelopeError
Example response
{
  "id": "0192f0b4-6f6e-7c3a-9a3e-6a1f2b3c4d5e",
  "plan_id": "0192f0b4-6f6e-7c3a-9a3e-6a1f2b3c4d5e",
  "plan_code": "string",
  "version": 1,
  "base_fee_minor": "string",
  "prices": [
    {
      "meter_code": "string",
      "included": "1000"
    }
  ],
  "entitlements": [
    {
      "meter_code": "string",
      "limit": "string",
      "alert_thresholds_pct": [
        50,
        80,
        100
      ],
      "window": "QUOTA_WINDOW_UNSPECIFIED",
      "mode": "QUOTA_MODE_UNSPECIFIED"
    }
  ],
  "publish_time": "2026-11-03T10:15:00Z",
  "trial_days": 1,
  "prorate_after_trial": false,
  "fees": [
    {
      "currency": "string",
      "base_fee_minor": "string",
      "interval": "SUBSCRIPTION_INTERVAL_MONTH"
    }
  ]
}
curl
curl -X POST https://YOUR-API-ADDRESS/v1/plans/<code>/versions \
  -H "Authorization: Bearer $PAYRUDDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

GET /v1/plans/{code}/versions/{version} #

Get a plan version (a number or "latest")

Parameters

NameInTypeDescription
code requiredpathstring
version requiredpathstring

Responses

StatusMeaningBody
200OKPlanVersion
404Error envelopeError
Example response
{
  "id": "0192f0b4-6f6e-7c3a-9a3e-6a1f2b3c4d5e",
  "plan_id": "0192f0b4-6f6e-7c3a-9a3e-6a1f2b3c4d5e",
  "plan_code": "string",
  "version": 1,
  "base_fee_minor": "string",
  "prices": [
    {
      "meter_code": "string",
      "included": "1000"
    }
  ],
  "entitlements": [
    {
      "meter_code": "string",
      "limit": "string",
      "alert_thresholds_pct": [
        50,
        80,
        100
      ],
      "window": "QUOTA_WINDOW_UNSPECIFIED",
      "mode": "QUOTA_MODE_UNSPECIFIED"
    }
  ],
  "publish_time": "2026-11-03T10:15:00Z",
  "trial_days": 1,
  "prorate_after_trial": false,
  "fees": [
    {
      "currency": "string",
      "base_fee_minor": "string",
      "interval": "SUBSCRIPTION_INTERVAL_MONTH"
    }
  ]
}
curl
curl -X GET https://YOUR-API-ADDRESS/v1/plans/<code>/versions/<version> \
  -H "Authorization: Bearer $PAYRUDDER_API_KEY"

POST /v1/plans/{code}/price-changes #

Move every subscription on one plan version to another at its next renewal, and tell the customers

For each Payrudder-run subscription on from_version whose next renewal is at least notice_days away (default 30, at least 14), a change to to_version (default: the latest published) is scheduled for that renewal and the customer is mailed, in their language, when the new price starts and what each renewal costs. With effective_from the subscription moves on its first period start on or after that date instead. The customer is mailed when the run schedules it, so at least notice_days before; with notice_send_days_before the mail is held and goes out that many days before the new price (never later than the customer's notice; a mail that is due already goes at once). The notice is the longer of notice_days and the operator's minimum for the customer's country (not legal advice, UNVERIFIED). The others are listed in skipped with why: renews_soon (run it again later), has_pending_change (a change is waiting and is never replaced), run_by_provider, not_changeable (for example a fee invoiced in arrears) and customer_blocked (a blocked customer is not offered a new price). dry_run counts and changes nothing. Each scheduled change also sends the subscription.change_scheduled webhook event, and the run sends price_change.scheduled once. The run is recorded as a price change (price_change_id; see GET /v1/price-changes). Running it again for the same plan and versions continues the open one: the skipped subscriptions are tried again, and the ones it scheduled before are counted in already_scheduled and neither scheduled nor mailed a second time.

Parameters

NameInTypeDescription
code requiredpathstring

Request body

FieldTypeDescription
from_version requiredintegerThe plan version the subscriptions are on now
to_versionintegerThe version they move to; 0 or absent is the latest published
notice_daysintegerDays before the new price starts that the customer must be told at the least: absent or 0 is the platform's default (30 until the operator changes it), otherwise at least 14. A customer in a country with a longer minimum (the operator's table) is told that much earlier, or skipped as `renews_soon`.
effective_fromstring · date-timeThe date the new price starts from. Each subscription then moves on its first period start on or after it (a monthly one on the first renewal from that date, a yearly one perhaps months later) instead of on its next renewal. At least `notice_days` from now and at most two years. A run that continues an open price change must name the date that price change has, or none if it has none (409 `plans.price_change.date_differs`).
notice_send_days_beforeintegerOptional: send each customer's notice mail this many days before their new price starts, instead of when the run schedules it. Absent or 0 sends at once; otherwise at least `notice_days` and at most 365. A customer is never told later than their notice (the longer of `notice_days` and the minimum of their country), and a mail that is due already goes at once. A held mail waits in the price change and is sent by a sweep within minutes of its time; withdrawing the price change cancels it, and the operator's kill switch holds it. A run that continues an open price change must name the same value (409 `plans.price_change.date_differs`).
dry_runbooleanOnly count; change nothing and mail nobody
Example
{
  "from_version": 1
}

Responses

StatusMeaningBody
200OKSchedulePriceChangeResponse
400Error envelopeError
404Error envelopeError
Example response
{
  "scheduled": 1,
  "scheduled_subscription_ids": [
    "0192f0b4-6f6e-7c3a-9a3e-6a1f2b3c4d5e"
  ],
  "skipped": [
    {
      "subscription_id": "0192f0b4-6f6e-7c3a-9a3e-6a1f2b3c4d5e",
      "customer_external_id": "string",
      "reason": "renews_soon"
    }
  ],
  "to_version": 1,
  "notice_days": 1
}
curl
curl -X POST https://YOUR-API-ADDRESS/v1/plans/<code>/price-changes \
  -H "Authorization: Bearer $PAYRUDDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"from_version":1}'

Models

CreatePlanRequest

FieldTypeDescription
code requiredstring
name requiredstring

CurrencyFee

FieldTypeDescription
currency requiredstring
base_fee_minor requiredstring · int64
interval requiredSUBSCRIPTION_INTERVAL_MONTH | SUBSCRIPTION_INTERVAL_YEARDefault month

CurrencyFeeInput

FieldTypeDescription
currencystring
base_fee_minorstring · int64
intervalSUBSCRIPTION_INTERVAL_MONTH | SUBSCRIPTION_INTERVAL_YEARDefault month

Entitlement

FieldTypeDescription
meter_code requiredstring
limit requiredstringDecimal in meter units; "0" = unlimited
alert_thresholds_pct requiredarray of integerStrictly ascending percentages of the limit at which an alert fires; empty = 80 and 100
window requiredQuotaWindowWhat a limit counts over, in the tenant's time zone. Unspecified is the month (the billing period). A year is the calendar year (twelve billing periods); a lifetime limit never starts again.
mode requiredQuotaModeWhat reaching the limit means for `allowed` (Payrudder never refuses usage events). HARD (also unspecified): allowed turns false at the limit. SOFT: allowed stays true and the usage over the limit is billed at the meter's price, which the package must have. NOTIFY: allowed stays true; only the alerts are sent.

EntitlementInput

FieldTypeDescription
meter_code requiredstring
limit requiredstringDecimal in meter units; "0" = unlimited
alert_thresholds_pctarray of integerStrictly ascending percentages of the limit at which an alert fires; empty = 80 and 100
windowQuotaWindowWhat a limit counts over, in the tenant's time zone. Unspecified is the month (the billing period). A year is the calendar year (twelve billing periods); a lifetime limit never starts again.
modeQuotaModeWhat reaching the limit means for `allowed` (Payrudder never refuses usage events). HARD (also unspecified): allowed turns false at the limit. SOFT: allowed stays true and the usage over the limit is billed at the meter's price, which the package must have. NOTIFY: allowed stays true; only the alerts are sent.

Error

FieldTypeDescription
error requiredobject

ListPlansResponse

FieldTypeDescription
plans requiredarray of Plan

Plan

FieldTypeDescription
id requiredstring · uuid
code requiredstring
name requiredstring
latest_version requiredinteger
create_time requiredstring · date-time

PlanVersion

FieldTypeDescription
id requiredstring · uuid
plan_id requiredstring · uuid
plan_code requiredstring
version requiredinteger
base_fee_minor requiredstring · int64
prices requiredarray of Price
entitlements requiredarray of Entitlement
publish_time requiredstring · date-time
trial_days requiredintegerThe trial a new subscription to this version gets unless its request says otherwise; 0 is none
prorate_after_trial requiredbooleanWhen a trial ends inside a billing period, the fixed fee of that first period is charged from the trial's end to the period's end instead of in full. False for a version published before the rule existed (it charged the whole period and keeps doing so)
fees requiredarray of CurrencyFeeFixed fees other than the tenant currency's monthly one, per currency and interval

Price

FieldTypeDescription
meter_code requiredstring
per_unitobject
tieredobject
packageobject
included requiredstringQuantity of every billing period that is free: the model prices only what is above it. Empty or 0 = none

PriceChangeSkipped

FieldTypeDescription
subscription_id requiredstring · uuid
customer_external_id requiredstring
reason requiredrenews_soon | has_pending_change | run_by_provider | not_changeable | customer_blocked

PriceInput

FieldTypeDescription
meter_code requiredstring
per_unitobject
tieredobject
packageobject
includedstringQuantity of every billing period that is free: the model prices only what is above it. Empty or 0 = none

PublishPlanVersionRequest

FieldTypeDescription
base_fee_minorstring · int64
feesarray of CurrencyFeeInputFixed fees other than the tenant currency's monthly one (base_fee_minor), per currency and interval; no duplicates. A yearly subscription needs a yearly fee in its currency, including the tenant's
pricesarray of PriceInput
entitlementsarray of EntitlementInput
trial_daysintegerThe trial a new subscription to this version gets unless its request says otherwise; 0 is none
prorate_after_trialbooleanLeft out: true. When a trial ends inside a billing period, charge the first period from the trial's end to the period's end instead of the whole fee; false charges the whole fee as versions published before this rule do

QuotaMode

QUOTA_MODE_UNSPECIFIED | QUOTA_MODE_HARD | QUOTA_MODE_SOFT | QUOTA_MODE_NOTIFY

QuotaWindow

QUOTA_WINDOW_UNSPECIFIED | QUOTA_WINDOW_MONTH | QUOTA_WINDOW_HOUR | QUOTA_WINDOW_DAY | QUOTA_WINDOW_WEEK | QUOTA_WINDOW_YEAR | QUOTA_WINDOW_LIFETIME

SchedulePriceChangeRequest

FieldTypeDescription
from_version requiredintegerThe plan version the subscriptions are on now
to_versionintegerThe version they move to; 0 or absent is the latest published
notice_daysintegerDays before the new price starts that the customer must be told at the least: absent or 0 is the platform's default (30 until the operator changes it), otherwise at least 14. A customer in a country with a longer minimum (the operator's table) is told that much earlier, or skipped as `renews_soon`.
effective_fromstring · date-timeThe date the new price starts from. Each subscription then moves on its first period start on or after it (a monthly one on the first renewal from that date, a yearly one perhaps months later) instead of on its next renewal. At least `notice_days` from now and at most two years. A run that continues an open price change must name the date that price change has, or none if it has none (409 `plans.price_change.date_differs`).
notice_send_days_beforeintegerOptional: send each customer's notice mail this many days before their new price starts, instead of when the run schedules it. Absent or 0 sends at once; otherwise at least `notice_days` and at most 365. A customer is never told later than their notice (the longer of `notice_days` and the minimum of their country), and a mail that is due already goes at once. A held mail waits in the price change and is sent by a sweep within minutes of its time; withdrawing the price change cancels it, and the operator's kill switch holds it. A run that continues an open price change must name the same value (409 `plans.price_change.date_differs`).
dry_runbooleanOnly count; change nothing and mail nobody

SchedulePriceChangeResponse

FieldTypeDescription
scheduled requiredintegerScheduled by this run
scheduled_subscription_ids requiredarray of string · uuid
skipped requiredarray of PriceChangeSkipped
to_version requiredinteger
notice_days requiredinteger
price_change_idstringThe price change this run made or continued; empty for a dry run when none is open
already_scheduledintegerScheduled by this price change before: not scheduled or mailed again