Subscription Plans
The subscription plan model
The subscription plan model contains all the information about the subscription plans you create, including the name, description, price, and billing interval.
Properties
| Name | Type | Description |
|---|---|---|
id | string | Unique identifier for the subscription plan (always starts with subscription_plan_). |
resource | string | The resource type. Always subscription_plan. |
testmode | boolean | Whether this plan is in test mode. |
name | string | Display name of the plan. |
description | string | Detailed description of the plan. |
basePrice | Money | Price per billing interval. A Money object with value (decimal string) and currency (ISO 4217 code). Interpretation depends on taxBehavior: when exclusive, basePrice is the net amount and tax is added at checkout; when inclusive, basePrice already includes tax and the net is back-computed from the buyer's jurisdiction. |
taxBehavior | string | Whether basePrice is tax-exclusive (exclusive, the B2B convention) or tax-inclusive (inclusive, the B2C convention). Immutable after plan creation — create a new plan for the other mode. A checkout may not mix products with different taxBehavior values. |
interval | string | Billing interval unit. Can be day, week, month, or year. |
intervalCount | integer | Number of interval units between billing cycles. For example, interval: month with intervalCount: 3 bills every 3 months. |
productType | string | What kind of product this plan sells. Always saas — an e-book is a one-time purchase, so sell it as a one-off product instead. Set when the plan is created. |
status | string | Current status of the plan. Can be active (plan is active and can be subscribed to), pending (plan is awaiting approval), or rejected (plan has been rejected). |
archivedAt | string | null | When this plan was archived (ISO 8601), or null while it is open to new business. Always present. An archived plan is hidden from GET /v1/subscription-plans, refused by POST /v1/checkouts, and cannot be switched to from PATCH /v1/subscriptions/:id. |
pendingUpdates | object | null | The changes that will take effect once a submitted update is approved, or null when there is no pending update. Only the fields that actually differ from the live plan are present. |
updateStatus | string | null | Lifecycle of a pending update, or null when there is none. Can be pending (an update was submitted and is awaiting review) or reviewing (the update is being reviewed). |
createdAt | string | When this plan was created (ISO 8601 format). |
links | object | HATEOAS links to related resources. Contains self link. |
List all subscription plans
GET /v1/subscription-plans
This endpoint retrieves a paginated list of all subscription plans. Only plans with active status can be used in checkouts. Archived plans are excluded unless includeArchived=true is passed.
Optional query parameters
| Name | Type | Description |
|---|---|---|
limit | integer | The number of subscription plans to return (default: 10, max: 100). |
startingAfter | string | A cursor for use in pagination. Returns results after this plan ID. |
endingBefore | string | A cursor for use in pagination. Returns results before this plan ID. |
includeArchived | boolean | Include archived plans in the listing. Archived plans are hidden by default because they cannot be sold; set this to true to see them alongside the live ones (tell them apart by the non-null archivedAt). Default: false. |
curl -G https://api.vatly.com/v1/subscription-plans \
-H "Authorization: Bearer live_your_api_key_here" \
-d limit=10
$vatly = new \Vatly\API\VatlyApiClient();
$vatly->setApiKey('live_your_api_key_here');
$plans = $vatly->subscriptionPlans->page();
{
"data": [
{
"id": "subscription_plan_Bm7xNvPwKr3YjTgHcZaE",
"resource": "subscription_plan",
"testmode": false,
"name": "Pro Monthly",
"description": "Full access to all Pro features, billed monthly",
"basePrice": {
"value": "29.00",
"currency": "EUR"
},
"taxBehavior": "exclusive",
"interval": "month",
"intervalCount": 1,
"productType": "saas",
"status": "active",
"archivedAt": null,
"createdAt": "2024-01-15T10:30:00Z",
"links": {
"self": {
"href": "https://api.vatly.com/v1/subscription-plans/subscription_plan_Bm7xNvPwKr3YjTgHcZaE",
"type": "application/json"
}
}
},
{
"id": "subscription_plan_Wt5mNvBxKw7YcZaEjLhR",
"resource": "subscription_plan",
"testmode": false,
"name": "Pro Yearly",
"description": "Full access to all Pro features, billed yearly",
"basePrice": {
"value": "290.00",
"currency": "EUR"
},
"taxBehavior": "exclusive",
"interval": "year",
"intervalCount": 1,
"productType": "saas",
"status": "active",
"archivedAt": null,
"createdAt": "2024-01-15T10:30:00Z",
"links": {
"self": {
"href": "https://api.vatly.com/v1/subscription-plans/subscription_plan_Wt5mNvBxKw7YcZaEjLhR",
"type": "application/json"
}
}
}
],
"count": 2,
"links": {
"self": {
"href": "https://api.vatly.com/v1/subscription-plans",
"type": "application/json"
},
"next": null,
"prev": null
}
}
Create a subscription plan
POST /v1/subscription-plans
Creates a new subscription plan for the authenticated merchant, in the testmode determined from the API token.
A plan created with a live_ token starts in pending status and must be approved by Vatly before it can be used in checkouts — the same review that applies to plans created in the dashboard. A plan created with a test_ token is auto-approved (active) so you can trial checkout immediately.
Constraints:
productTypemust besaas— e-books are one-off purchases and cannot be sold on a recurring basis.- The
dayinterval is sandbox-only; live plans supportweek,month, andyear. intervalCountis bounded per unit: up to 365 days, 52 weeks, or 12 months.yearalways bills once per year (intervalCountis ignored).
Required attributes
| Name | Type | Description |
|---|---|---|
name | string | Display name of the plan (3–255 characters). |
description | string | Detailed description of the plan. |
basePrice | Money | Price per billing interval. A Money object with value (decimal string) and currency (ISO 4217 code). |
productType | string | Tax product classification. Must be saas. |
interval | string | Billing interval unit. One of day (sandbox-only), week, month, or year. |
intervalCount | integer | Number of interval units between billing cycles (at least 1). |
Optional attributes
| Name | Type | Description |
|---|---|---|
taxBehavior | string | Whether basePrice is tax-exclusive (exclusive, the B2B convention) or tax-inclusive (inclusive, the B2C convention). Defaults to exclusive. Immutable after plan creation — create a new plan for the other mode. |
curl https://api.vatly.com/v1/subscription-plans \
-H "Authorization: Bearer live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Pro Monthly",
"description": "Full access to all Pro features, billed monthly",
"basePrice": { "value": "29.00", "currency": "EUR" },
"productType": "saas",
"interval": "month",
"intervalCount": 1
}'
$vatly = new \Vatly\API\VatlyApiClient();
$vatly->setApiKey('live_your_api_key_here');
$plan = $vatly->subscriptionPlans->create([
'name' => 'Pro Monthly',
'description' => 'Full access to all Pro features, billed monthly',
'basePrice' => ['value' => '29.00', 'currency' => 'EUR'],
'productType' => 'saas',
'interval' => 'month',
'intervalCount' => 1,
]);
{
"id": "subscription_plan_Bm7xNvPwKr3YjTgHcZaE",
"resource": "subscription_plan",
"testmode": false,
"name": "Pro Monthly",
"description": "Full access to all Pro features, billed monthly",
"basePrice": {
"value": "29.00",
"currency": "EUR"
},
"taxBehavior": "exclusive",
"interval": "month",
"intervalCount": 1,
"productType": "saas",
"status": "pending",
"archivedAt": null,
"createdAt": "2024-01-15T10:30:00Z",
"links": {
"self": {
"href": "https://api.vatly.com/v1/subscription-plans/subscription_plan_Bm7xNvPwKr3YjTgHcZaE",
"type": "application/json"
}
}
}
Retrieve a subscription plan
GET /v1/subscription-plans/:id
This endpoint retrieves a specific subscription plan by its ID.
URL parameters
| Name | Type | Description |
|---|---|---|
id | string | The ID of the subscription plan to retrieve. |
curl https://api.vatly.com/v1/subscription-plans/subscription_plan_Bm7xNvPwKr3YjTgHcZaE \
-H "Authorization: Bearer live_your_api_key_here"
$vatly = new \Vatly\API\VatlyApiClient();
$vatly->setApiKey('live_your_api_key_here');
$plan = $vatly->subscriptionPlans->get('subscription_plan_Bm7xNvPwKr3YjTgHcZaE');
{
"id": "subscription_plan_Bm7xNvPwKr3YjTgHcZaE",
"resource": "subscription_plan",
"testmode": false,
"name": "Pro Monthly",
"description": "Full access to all Pro features, billed monthly",
"basePrice": {
"value": "29.00",
"currency": "EUR"
},
"taxBehavior": "exclusive",
"interval": "month",
"intervalCount": 1,
"productType": "saas",
"status": "active",
"archivedAt": null,
"createdAt": "2024-01-15T10:30:00Z",
"links": {
"self": {
"href": "https://api.vatly.com/v1/subscription-plans/subscription_plan_Bm7xNvPwKr3YjTgHcZaE",
"type": "application/json"
}
}
}
Update a subscription plan
PATCH /v1/subscription-plans/:id
Submits an update to a live subscription plan. Because plans drive VAT-bearing recurring sales, the change is held as a pending update and reviewed by Vatly before it takes effect (updateStatus moves pending → reviewing → applied). In test mode the update is approved automatically.
Each request is the complete set of changes relative to the current live plan, and must contain at least one field. Fields equal to the live value are ignored, and a request that nets to no change clears any pending update. A new request replaces the not-yet-reviewed one; while an update is being reviewed, further requests return 409. Changing the interval on a plan that has ever been used by a subscription — active or not — also returns 409. The price stays changeable.
The submitted change is surfaced on the plan resource as pendingUpdates (only the fields that differ), with updateStatus tracking the review. Approval is signalled by the subscriptionPlan.update_submitted, subscriptionPlan.update_approved, and subscriptionPlan.update_rejected webhook events.
URL parameters
| Name | Type | Description |
|---|---|---|
id | string | The ID of the subscription plan to update. |
Optional attributes
At least one field must be provided.
| Name | Type | Description |
|---|---|---|
name | string | New display name of the plan (3–255 characters). |
description | string | New description of the plan. |
basePrice | Money | New price per billing interval. A Money object with value (decimal string) and currency (ISO 4217 code). |
productType | string | Tax product classification. Must be saas. |
interval | string | New billing interval unit. One of day (sandbox-only), week, month, or year. |
intervalCount | integer | Number of interval units between billing cycles (at least 1). Required when interval is provided. |
curl -X PATCH https://api.vatly.com/v1/subscription-plans/subscription_plan_Bm7xNvPwKr3YjTgHcZaE \
-H "Authorization: Bearer live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Pro Weekly",
"basePrice": { "value": "9.00", "currency": "EUR" },
"interval": "week",
"intervalCount": 1
}'
$vatly = new \Vatly\API\VatlyApiClient();
$vatly->setApiKey('live_your_api_key_here');
$plan = $vatly->subscriptionPlans->update('subscription_plan_Bm7xNvPwKr3YjTgHcZaE', [
'name' => 'Pro Weekly',
'basePrice' => ['value' => '9.00', 'currency' => 'EUR'],
'interval' => 'week',
'intervalCount' => 1,
]);
{
"id": "subscription_plan_Bm7xNvPwKr3YjTgHcZaE",
"resource": "subscription_plan",
"testmode": false,
"name": "Pro Monthly",
"description": "Full access to all Pro features, billed monthly",
"basePrice": {
"value": "29.00",
"currency": "EUR"
},
"taxBehavior": "exclusive",
"interval": "month",
"intervalCount": 1,
"productType": "saas",
"status": "active",
"archivedAt": null,
"pendingUpdates": {
"name": "Pro Weekly",
"basePrice": {
"value": "9.00",
"currency": "EUR"
},
"interval": "week",
"intervalCount": 1
},
"updateStatus": "pending",
"createdAt": "2024-01-15T10:30:00Z",
"links": {
"self": {
"href": "https://api.vatly.com/v1/subscription-plans/subscription_plan_Bm7xNvPwKr3YjTgHcZaE",
"type": "application/json"
}
}
}
Archive a subscription plan
POST /v1/subscription-plans/:id/archive
Closes the plan to new business. It is hidden from GET /v1/subscription-plans (unless includeArchived=true), refused by POST /v1/checkouts, and can no longer be switched to from PATCH /v1/subscriptions/:id.
Subscribers already on the plan are deliberately untouched: a subscription snapshots its plan at signup and renews off that snapshot, so archiving never cancels anyone or changes what they are billed. To end a subscription, cancel it with DELETE /v1/subscriptions/:id.
Likewise a checkout created before the plan was archived snapshots its product data at creation time and can still be completed. Archiving applies to new checkouts only.
Nothing is deleted — the plan remains readable by id, now carrying a non-null archivedAt. Repeating the request is a no-op that returns 204 and does not move archivedAt. Reverse it by unarchiving the plan.
URL parameters
| Name | Type | Description |
|---|---|---|
id | string | The ID of the subscription plan to archive. |
curl -X POST https://api.vatly.com/v1/subscription-plans/subscription_plan_Bm7xNvPwKr3YjTgHcZaE/archive \
-H "Authorization: Bearer live_your_api_key_here"
$vatly = new \Vatly\API\VatlyApiClient();
$vatly->setApiKey('live_your_api_key_here');
$vatly->subscriptionPlans->archive('subscription_plan_Bm7xNvPwKr3YjTgHcZaE');
Returns 204 No Content on success (or if the plan was already archived).
Unarchive a subscription plan
DELETE /v1/subscription-plans/:id/archive
Re-opens an archived plan to new business: it reappears in GET /v1/subscription-plans and can be added to checkouts and switched to again. Calling it on a plan that is not archived is a no-op. The refreshed plan is returned in the response body.
URL parameters
| Name | Type | Description |
|---|---|---|
id | string | The ID of the subscription plan to unarchive. |
curl -X DELETE https://api.vatly.com/v1/subscription-plans/subscription_plan_Wt5mNvBxKw7YcZaEjLhR/archive \
-H "Authorization: Bearer live_your_api_key_here"
$vatly = new \Vatly\API\VatlyApiClient();
$vatly->setApiKey('live_your_api_key_here');
$plan = $vatly->subscriptionPlans->unarchive('subscription_plan_Wt5mNvBxKw7YcZaEjLhR');
{
"id": "subscription_plan_Wt5mNvBxKw7YcZaEjLhR",
"resource": "subscription_plan",
"testmode": false,
"name": "Pro Monthly",
"description": "Full access to all Pro features, billed monthly",
"basePrice": {
"value": "29.00",
"currency": "EUR"
},
"taxBehavior": "exclusive",
"interval": "month",
"intervalCount": 1,
"productType": "saas",
"status": "active",
"archivedAt": null,
"createdAt": "2024-01-15T10:30:00Z",
"links": {
"self": {
"href": "https://api.vatly.com/v1/subscription-plans/subscription_plan_Wt5mNvBxKw7YcZaEjLhR",
"type": "application/json"
}
}
}