plans
Stable. Describes behaviour that is built and covered by tests in this repository.
GET /v1/plans #
List plans
Not paged: the response is the complete list.
Responses
| Status | Meaning | Body |
|---|---|---|
200 | OK | ListPlansResponse |
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
| Field | Type | Description |
|---|---|---|
code required | string | |
name required | string |
Example
{
"code": "string",
"name": "string"
}Responses
| Status | Meaning | Body |
|---|---|---|
201 | Created | Plan |
400 | Error envelope | Error |
409 | Error envelope | Error |
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
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string |
Request body
| Field | Type | Description |
|---|---|---|
base_fee_minor | string · int64 | |
fees | array of CurrencyFeeInput | Fixed 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 |
prices | array of PriceInput | |
entitlements | array of EntitlementInput | |
trial_days | integer | The trial a new subscription to this version gets unless its request says otherwise; 0 is none |
prorate_after_trial | boolean | Left 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
| Status | Meaning | Body |
|---|---|---|
201 | Created | PlanVersion |
400 | Error envelope | Error |
404 | Error envelope | Error |
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
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | |
version required | path | string |
Responses
| Status | Meaning | Body |
|---|---|---|
200 | OK | PlanVersion |
404 | Error envelope | Error |
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
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string |
Request body
| Field | Type | Description |
|---|---|---|
from_version required | integer | The plan version the subscriptions are on now |
to_version | integer | The version they move to; 0 or absent is the latest published |
notice_days | integer | Days 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_from | string · date-time | The 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_before | integer | Optional: 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_run | boolean | Only count; change nothing and mail nobody |
Example
{
"from_version": 1
}Responses
| Status | Meaning | Body |
|---|---|---|
200 | OK | SchedulePriceChangeResponse |
400 | Error envelope | Error |
404 | Error envelope | Error |
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
| Field | Type | Description |
|---|---|---|
code required | string | |
name required | string |
CurrencyFee
| Field | Type | Description |
|---|---|---|
currency required | string | |
base_fee_minor required | string · int64 | |
interval required | SUBSCRIPTION_INTERVAL_MONTH | SUBSCRIPTION_INTERVAL_YEAR | Default month |
CurrencyFeeInput
| Field | Type | Description |
|---|---|---|
currency | string | |
base_fee_minor | string · int64 | |
interval | SUBSCRIPTION_INTERVAL_MONTH | SUBSCRIPTION_INTERVAL_YEAR | Default month |
Entitlement
| Field | Type | Description |
|---|---|---|
meter_code required | string | |
limit required | string | Decimal in meter units; "0" = unlimited |
alert_thresholds_pct required | array of integer | Strictly ascending percentages of the limit at which an alert fires; empty = 80 and 100 |
window required | QuotaWindow | What 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 required | QuotaMode | What 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
| Field | Type | Description |
|---|---|---|
meter_code required | string | |
limit required | string | Decimal in meter units; "0" = unlimited |
alert_thresholds_pct | array of integer | Strictly ascending percentages of the limit at which an alert fires; empty = 80 and 100 |
window | QuotaWindow | What 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 | QuotaMode | What 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
| Field | Type | Description |
|---|---|---|
error required | object |
ListPlansResponse
| Field | Type | Description |
|---|---|---|
plans required | array of Plan |
Plan
| Field | Type | Description |
|---|---|---|
id required | string · uuid | |
code required | string | |
name required | string | |
latest_version required | integer | |
create_time required | string · date-time |
PlanVersion
| Field | Type | Description |
|---|---|---|
id required | string · uuid | |
plan_id required | string · uuid | |
plan_code required | string | |
version required | integer | |
base_fee_minor required | string · int64 | |
prices required | array of Price | |
entitlements required | array of Entitlement | |
publish_time required | string · date-time | |
trial_days required | integer | The trial a new subscription to this version gets unless its request says otherwise; 0 is none |
prorate_after_trial required | boolean | When 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 required | array of CurrencyFee | Fixed fees other than the tenant currency's monthly one, per currency and interval |
Price
| Field | Type | Description |
|---|---|---|
meter_code required | string | |
per_unit | object | |
tiered | object | |
package | object | |
included required | string | Quantity of every billing period that is free: the model prices only what is above it. Empty or 0 = none |
PriceChangeSkipped
| Field | Type | Description |
|---|---|---|
subscription_id required | string · uuid | |
customer_external_id required | string | |
reason required | renews_soon | has_pending_change | run_by_provider | not_changeable | customer_blocked |
PriceInput
| Field | Type | Description |
|---|---|---|
meter_code required | string | |
per_unit | object | |
tiered | object | |
package | object | |
included | string | Quantity of every billing period that is free: the model prices only what is above it. Empty or 0 = none |
PublishPlanVersionRequest
| Field | Type | Description |
|---|---|---|
base_fee_minor | string · int64 | |
fees | array of CurrencyFeeInput | Fixed 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 |
prices | array of PriceInput | |
entitlements | array of EntitlementInput | |
trial_days | integer | The trial a new subscription to this version gets unless its request says otherwise; 0 is none |
prorate_after_trial | boolean | Left 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
| Field | Type | Description |
|---|---|---|
from_version required | integer | The plan version the subscriptions are on now |
to_version | integer | The version they move to; 0 or absent is the latest published |
notice_days | integer | Days 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_from | string · date-time | The 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_before | integer | Optional: 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_run | boolean | Only count; change nothing and mail nobody |
SchedulePriceChangeResponse
| Field | Type | Description |
|---|---|---|
scheduled required | integer | Scheduled by this run |
scheduled_subscription_ids required | array of string · uuid | |
skipped required | array of PriceChangeSkipped | |
to_version required | integer | |
notice_days required | integer | |
price_change_id | string | The price change this run made or continued; empty for a dry run when none is open |
already_scheduled | integer | Scheduled by this price change before: not scheduled or mailed again |