curl --request POST \
--url https://app.sideshift.app/api/oauth/v1/agency/billing/subscription-link \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"subaccountId": "<string>",
"interval": "monthly",
"priceId": "<string>"
}
'import requests
url = "https://app.sideshift.app/api/oauth/v1/agency/billing/subscription-link"
payload = {
"subaccountId": "<string>",
"interval": "monthly",
"priceId": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({subaccountId: '<string>', interval: 'monthly', priceId: '<string>'})
};
fetch('https://app.sideshift.app/api/oauth/v1/agency/billing/subscription-link', 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://app.sideshift.app/api/oauth/v1/agency/billing/subscription-link",
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([
'subaccountId' => '<string>',
'interval' => 'monthly',
'priceId' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$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://app.sideshift.app/api/oauth/v1/agency/billing/subscription-link"
payload := strings.NewReader("{\n \"subaccountId\": \"<string>\",\n \"interval\": \"monthly\",\n \"priceId\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
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://app.sideshift.app/api/oauth/v1/agency/billing/subscription-link")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"subaccountId\": \"<string>\",\n \"interval\": \"monthly\",\n \"priceId\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.sideshift.app/api/oauth/v1/agency/billing/subscription-link")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"subaccountId\": \"<string>\",\n \"interval\": \"monthly\",\n \"priceId\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"data": {
"success": true,
"checkoutUrl": "<string>",
"subaccountName": "<string>",
"expiresAt": "2023-11-07T05:31:56Z",
"included": true,
"billingAccount": "agency",
"tier": "<string>",
"interval": "<string>"
}
}{
"error": {
"code": "unauthorized",
"message": "Missing bearer access token",
"requestId": "req_..."
}
}{
"error": {
"code": "insufficient_scope",
"message": "Requires scope 'campaigns:write'",
"requestId": "req_..."
}
}{
"error": {
"code": "not_found",
"message": "Resource not found",
"requestId": "req_..."
}
}{
"error": {
"code": "idempotency_conflict",
"message": "Idempotency-Key was reused with a different request body",
"requestId": "req_..."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded",
"requestId": "req_..."
}
}Start agency client checkout
Generate a Stripe Checkout URL for an agency subaccount client on a tiered client plan (starter, growth, or scale), billed monthly or annually. Included clients cannot use this endpoint (managed through the agency subscription). Additional clients require a paid, renewing agency base. Requires the agency-billing:write scope.
curl --request POST \
--url https://app.sideshift.app/api/oauth/v1/agency/billing/subscription-link \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"subaccountId": "<string>",
"interval": "monthly",
"priceId": "<string>"
}
'import requests
url = "https://app.sideshift.app/api/oauth/v1/agency/billing/subscription-link"
payload = {
"subaccountId": "<string>",
"interval": "monthly",
"priceId": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({subaccountId: '<string>', interval: 'monthly', priceId: '<string>'})
};
fetch('https://app.sideshift.app/api/oauth/v1/agency/billing/subscription-link', 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://app.sideshift.app/api/oauth/v1/agency/billing/subscription-link",
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([
'subaccountId' => '<string>',
'interval' => 'monthly',
'priceId' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$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://app.sideshift.app/api/oauth/v1/agency/billing/subscription-link"
payload := strings.NewReader("{\n \"subaccountId\": \"<string>\",\n \"interval\": \"monthly\",\n \"priceId\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
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://app.sideshift.app/api/oauth/v1/agency/billing/subscription-link")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"subaccountId\": \"<string>\",\n \"interval\": \"monthly\",\n \"priceId\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.sideshift.app/api/oauth/v1/agency/billing/subscription-link")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"subaccountId\": \"<string>\",\n \"interval\": \"monthly\",\n \"priceId\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"data": {
"success": true,
"checkoutUrl": "<string>",
"subaccountName": "<string>",
"expiresAt": "2023-11-07T05:31:56Z",
"included": true,
"billingAccount": "agency",
"tier": "<string>",
"interval": "<string>"
}
}{
"error": {
"code": "unauthorized",
"message": "Missing bearer access token",
"requestId": "req_..."
}
}{
"error": {
"code": "insufficient_scope",
"message": "Requires scope 'campaigns:write'",
"requestId": "req_..."
}
}{
"error": {
"code": "not_found",
"message": "Resource not found",
"requestId": "req_..."
}
}{
"error": {
"code": "idempotency_conflict",
"message": "Idempotency-Key was reused with a different request body",
"requestId": "req_..."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded",
"requestId": "req_..."
}
}Authorizations
OAuth 2.1 authorization code + PKCE (S256). Tokens are tenant-bound (company_id) and scoped. Discover endpoints via /.well-known/oauth-authorization-server.
Headers
Client-chosen key; replaying the same key + body returns the original response, a different body returns 409.
Body
Generate a per-client Stripe Checkout link for an agency subaccount on a tiered client plan. Choose tier (starter, growth, or scale) and interval (monthly or annual). Optional priceId must match the server price for that tier and interval.
The agency subaccount (client) company id to bill. Must be the token's own company or a subaccount whose parentCompanyId is the token company.
1Client plan tier. Required in practice — if omitted (and priceId does not identify a client tier), the API returns invalid_request ("Choose Starter, Growth, or Scale for this client.").
starter, growth, scale Billing interval for the client plan. Defaults to monthly.
monthly, annual Optional Stripe price id. When supplied, must match the server price for the selected client tier and interval.
Response
Stripe Checkout link for the agency billing manager.
Envelope for POST /agency/billing/subscription-link.
Show child attributes
Show child attributes