curl --request POST \
--url https://api.beta.suby.fi/v3/subscriptions/{id}/cancel-plan-change \
--header 'Content-Type: application/json' \
--header 'X-Suby-Api-Key: <api-key>' \
--data '
{
"scheduledChangeId": "ssc_abc123"
}
'import requests
url = "https://api.beta.suby.fi/v3/subscriptions/{id}/cancel-plan-change"
payload = { "scheduledChangeId": "ssc_abc123" }
headers = {
"X-Suby-Api-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-Suby-Api-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({scheduledChangeId: 'ssc_abc123'})
};
fetch('https://api.beta.suby.fi/v3/subscriptions/{id}/cancel-plan-change', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.beta.suby.fi/v3/subscriptions/{id}/cancel-plan-change",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'scheduledChangeId' => 'ssc_abc123'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-Suby-Api-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.beta.suby.fi/v3/subscriptions/{id}/cancel-plan-change"
payload := strings.NewReader("{\n \"scheduledChangeId\": \"ssc_abc123\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-Suby-Api-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.beta.suby.fi/v3/subscriptions/{id}/cancel-plan-change")
.header("X-Suby-Api-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"scheduledChangeId\": \"ssc_abc123\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.beta.suby.fi/v3/subscriptions/{id}/cancel-plan-change")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-Suby-Api-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"scheduledChangeId\": \"ssc_abc123\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"data": {
"subscription": {
"id": "sub_abc123",
"organizationId": "<string>",
"customerId": "<string>",
"productId": "<string>",
"status": "INCOMPLETE",
"currentCycle": 123,
"totalCycles": 123,
"trialEndAt": "2023-11-07T05:31:56Z",
"cancelAtPeriodEnd": true,
"currentCycleDueAt": "2023-11-07T05:31:56Z",
"renewalAttempt": 123,
"nextRenewalAttemptAt": "2023-11-07T05:31:56Z",
"lastDeclineCategory": "SOFT",
"externalRef": "<string>",
"createdAt": "2023-11-07T05:31:56Z",
"updatedAt": "2023-11-07T05:31:56Z"
},
"scheduledChange": {
"id": "ssc_abc123",
"type": "UPGRADE",
"targetPriceId": "pro_9m2k1x8s7d6f",
"scheduledFor": "2023-11-07T05:31:56Z",
"appliedAt": "2023-11-07T05:31:56Z",
"canceledAt": "2023-11-07T05:31:56Z"
}
},
"message": "<string>"
}{
"success": false,
"error": "UNAUTHORIZED",
"message": "Invalid or missing API key"
}{
"success": false,
"error": "NOT_FOUND",
"message": "Resource not found",
"data": "<unknown>"
}{
"success": false,
"error": "NOT_FOUND",
"message": "Resource not found",
"data": "<unknown>"
}Call off a scheduled plan change
Drop a plan change that is queued and has not taken effect yet — the way back out for a customer who asked for a different plan and changed their mind before the next billing cycle.
This is what to call for a downgrade scheduled at the period end. The subscription is left exactly as it is: it stays on the plan it bills today, and the next renewal charges that plan’s price. Nothing is refunded and nothing is charged, because the queued change had not billed anything yet.
Read what is queued from GET /v3/subscriptions/{id} → scheduledChange (null when nothing is queued). Pass scheduledChangeId to make the call idempotent against the change you actually read: if it no longer names the queued change, the request is refused instead of dropping a different one.
Changing the plan back by calling change-plan with the current productId does not work — that answers 409 SAME_PLAN, because it would queue a second change rather than remove the first.
An immediate upgrade cannot be called off here. Its charge is already in flight (or has settled), so dropping the swap would leave the customer paying for a plan they never receive: answer is 409 PLAN_CHANGE_NOT_CANCELABLE. Refund the charge and call change-plan back to the previous plan instead. An upgrade whose charge was refused or abandoned never applies, and that one can be called off to clear the queue.
curl --request POST \
--url https://api.beta.suby.fi/v3/subscriptions/{id}/cancel-plan-change \
--header 'Content-Type: application/json' \
--header 'X-Suby-Api-Key: <api-key>' \
--data '
{
"scheduledChangeId": "ssc_abc123"
}
'import requests
url = "https://api.beta.suby.fi/v3/subscriptions/{id}/cancel-plan-change"
payload = { "scheduledChangeId": "ssc_abc123" }
headers = {
"X-Suby-Api-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-Suby-Api-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({scheduledChangeId: 'ssc_abc123'})
};
fetch('https://api.beta.suby.fi/v3/subscriptions/{id}/cancel-plan-change', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.beta.suby.fi/v3/subscriptions/{id}/cancel-plan-change",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'scheduledChangeId' => 'ssc_abc123'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-Suby-Api-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.beta.suby.fi/v3/subscriptions/{id}/cancel-plan-change"
payload := strings.NewReader("{\n \"scheduledChangeId\": \"ssc_abc123\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-Suby-Api-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.beta.suby.fi/v3/subscriptions/{id}/cancel-plan-change")
.header("X-Suby-Api-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"scheduledChangeId\": \"ssc_abc123\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.beta.suby.fi/v3/subscriptions/{id}/cancel-plan-change")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-Suby-Api-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"scheduledChangeId\": \"ssc_abc123\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"data": {
"subscription": {
"id": "sub_abc123",
"organizationId": "<string>",
"customerId": "<string>",
"productId": "<string>",
"status": "INCOMPLETE",
"currentCycle": 123,
"totalCycles": 123,
"trialEndAt": "2023-11-07T05:31:56Z",
"cancelAtPeriodEnd": true,
"currentCycleDueAt": "2023-11-07T05:31:56Z",
"renewalAttempt": 123,
"nextRenewalAttemptAt": "2023-11-07T05:31:56Z",
"lastDeclineCategory": "SOFT",
"externalRef": "<string>",
"createdAt": "2023-11-07T05:31:56Z",
"updatedAt": "2023-11-07T05:31:56Z"
},
"scheduledChange": {
"id": "ssc_abc123",
"type": "UPGRADE",
"targetPriceId": "pro_9m2k1x8s7d6f",
"scheduledFor": "2023-11-07T05:31:56Z",
"appliedAt": "2023-11-07T05:31:56Z",
"canceledAt": "2023-11-07T05:31:56Z"
}
},
"message": "<string>"
}{
"success": false,
"error": "UNAUTHORIZED",
"message": "Invalid or missing API key"
}{
"success": false,
"error": "NOT_FOUND",
"message": "Resource not found",
"data": "<unknown>"
}{
"success": false,
"error": "NOT_FOUND",
"message": "Resource not found",
"data": "<unknown>"
}Authorizations
Secret API key. sk_live_… (production) or sk_sandbox_… (sandbox).
Headers
Optional key (≤255 chars, e.g. a UUID v4) that makes this POST safe to retry: the first request executes and its response is cached for 24h; a retry with the SAME key replays that response instead of re-executing (no duplicate payment/subscription). A reused key with a different request → 422 IDEMPOTENCY_KEY_CONFLICT; a retry while the first is still in flight → 409. See the Idempotency guide.
255"5f3b9c2e-1a4d-4f2b-9c31-7e2a1b6d8c04"
Path Parameters
"sub_abc123"
Body
Optional guard — the change you read from GET /v3/subscriptions/{id}. When it does not name the queued change the request is refused (NO_SCHEDULED_PLAN_CHANGE) rather than applied to whatever is queued.
"ssc_abc123"
Was this page helpful?

