Workspaces
Four endpoints. Workspaces are the billed unit, so creating and removing them changes what you pay.
A workspace owns a set of proxy assignments and their credentials. Your plan defines how many IPs each workspace gets; the number of workspaces defines how many seats you pay for. Separating traffic by workspace is how you keep one project's IPs from mixing with another's.
List workspaces
/workspaces Every workspace in your organization.
Returns all workspaces, including those still provisioning. Not paginated — the list is bounded by your plan, so everything comes back in one response.
Example request
curl https://api.proxycharm.com/api/v1/public/workspaces \
-H "Authorization: Bearer $PROXYCHARM_API_KEY" import os
import requests
API_KEY = os.environ["PROXYCHARM_API_KEY"]
response = requests.get(
"https://api.proxycharm.com/api/v1/public/workspaces",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=30,
)
payload = response.json()
if not payload["success"]:
raise RuntimeError(payload["error"]["message"])
print(payload["data"]) const apiKey = process.env.PROXYCHARM_API_KEY;
const response = await fetch("https://api.proxycharm.com/api/v1/public/workspaces", {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
const payload = await response.json();
if (!payload.success) {
throw new Error(payload.error.message);
}
console.log(payload.data); package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
)
type envelope struct {
Success bool `json:"success"`
Data json.RawMessage `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func main() {
req, err := http.NewRequest("GET", "https://api.proxycharm.com/api/v1/public/workspaces", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("PROXYCHARM_API_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var payload envelope
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
panic(err)
}
if !payload.Success {
panic(payload.Error.Message)
}
fmt.Println(string(payload.Data))
} <?php
$apiKey = getenv("PROXYCHARM_API_KEY");
$ch = curl_init("https://api.proxycharm.com/api/v1/public/workspaces");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $apiKey,
],
]);
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
if (!$payload["success"]) {
throw new RuntimeException($payload["error"]["message"]);
}
print_r($payload["data"]); Example response
{
"success": true,
"data": [
{
"id": "3b7d2c48-6e19-4a5f-8b0c-9d1e2f3a4b5c",
"name": "Scraper US",
"address": "500 W Madison St, Chicago, IL",
"country": "US",
"status": "active"
},
{
"id": "5d9f4e6a-8c2b-4713-9e5d-0f1a2b3c4d5e",
"name": "Scraper EU",
"address": null,
"country": "DE",
"status": "pending_provision"
}
],
"error": null,
"context": {
"affected_entities": [],
"suggested_next_actions": [
"get_organization",
"list_proxies"
],
"warnings": []
},
"request_id": null,
"timestamp": "2026-02-18T09:31:04.512Z"
} Response fields
| Name | Type | Description |
|---|---|---|
(array) | array<object> | data is the array itself, not an object wrapping it. |
id | string | Workspace id. Use this wherever an endpoint takes {account_id}. |
name | string | Display name you chose for the workspace. |
address | string | null | Optional operating address recorded for the workspace. |
country | string | null | Optional ISO 3166-1 alpha-2 country the workspace operates from. |
status | string | Lifecycle state. pending_payment is awaiting checkout, pending_provision is paid but waiting for an IP, active has live proxies, removed is retired. |
Errors
| Code | HTTP | What it means |
|---|---|---|
API_KEY_INVALID | 401 | The Authorization header is missing the key prefix, the key does not exist, or its organization has been deleted. |
API_KEY_REVOKED | 401 | The key was revoked in the dashboard. |
API_KEY_EXPIRED | 401 | The key passed the expiry date it was created with. |
RATE_LIMIT_EXCEEDED | 429 | The key exceeded its request budget for the current window. |
Get workspace
/workspaces/{account_id} Read one workspace by id.
Returns a single workspace. Requesting a workspace from another organization returns HTTP 403 rather than 404, so a caller cannot use this endpoint to probe for ids that exist elsewhere.
Path parameters
| Name | Type | Description |
|---|---|---|
account_id Required | string | Workspace id, from GET /workspaces or GET /organization. |
Example request
curl https://api.proxycharm.com/api/v1/public/workspaces/3b7d2c48-6e19-4a5f-8b0c-9d1e2f3a4b5c \
-H "Authorization: Bearer $PROXYCHARM_API_KEY" import os
import requests
API_KEY = os.environ["PROXYCHARM_API_KEY"]
response = requests.get(
"https://api.proxycharm.com/api/v1/public/workspaces/3b7d2c48-6e19-4a5f-8b0c-9d1e2f3a4b5c",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=30,
)
payload = response.json()
if not payload["success"]:
raise RuntimeError(payload["error"]["message"])
print(payload["data"]) const apiKey = process.env.PROXYCHARM_API_KEY;
const response = await fetch("https://api.proxycharm.com/api/v1/public/workspaces/3b7d2c48-6e19-4a5f-8b0c-9d1e2f3a4b5c", {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
const payload = await response.json();
if (!payload.success) {
throw new Error(payload.error.message);
}
console.log(payload.data); package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
)
type envelope struct {
Success bool `json:"success"`
Data json.RawMessage `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func main() {
req, err := http.NewRequest("GET", "https://api.proxycharm.com/api/v1/public/workspaces/3b7d2c48-6e19-4a5f-8b0c-9d1e2f3a4b5c", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("PROXYCHARM_API_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var payload envelope
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
panic(err)
}
if !payload.Success {
panic(payload.Error.Message)
}
fmt.Println(string(payload.Data))
} <?php
$apiKey = getenv("PROXYCHARM_API_KEY");
$ch = curl_init("https://api.proxycharm.com/api/v1/public/workspaces/3b7d2c48-6e19-4a5f-8b0c-9d1e2f3a4b5c");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $apiKey,
],
]);
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
if (!$payload["success"]) {
throw new RuntimeException($payload["error"]["message"]);
}
print_r($payload["data"]); Example response
{
"success": true,
"data": {
"id": "3b7d2c48-6e19-4a5f-8b0c-9d1e2f3a4b5c",
"name": "Scraper US",
"address": "500 W Madison St, Chicago, IL",
"country": "US",
"status": "active"
},
"error": null,
"context": {
"affected_entities": [],
"suggested_next_actions": [
"list_workspaces",
"get_workspace_proxies"
],
"warnings": []
},
"request_id": null,
"timestamp": "2026-02-18T09:31:04.512Z"
} Response fields
| Name | Type | Description |
|---|---|---|
id | string | Workspace id. Use this wherever an endpoint takes {account_id}. |
name | string | Display name you chose for the workspace. |
address | string | null | Optional operating address recorded for the workspace. |
country | string | null | Optional ISO 3166-1 alpha-2 country the workspace operates from. |
status | string | Lifecycle state. pending_payment is awaiting checkout, pending_provision is paid but waiting for an IP, active has live proxies, removed is retired. |
Errors
| Code | HTTP | What it means |
|---|---|---|
ORGANIZATION_PERMISSION_DENIED | 403 | The workspace id in the path belongs to another organization, or the profile that created the key is not an owner. Invoices require owner-level access. |
API_KEY_INVALID | 401 | The Authorization header is missing the key prefix, the key does not exist, or its organization has been deleted. |
API_KEY_REVOKED | 401 | The key was revoked in the dashboard. |
API_KEY_EXPIRED | 401 | The key passed the expiry date it was created with. |
RATE_LIMIT_EXCEEDED | 429 | The key exceeded its request budget for the current window. |
Add workspace
/workspaces Create a workspace, taking an open seat or adding a paid one.
Adds a workspace to the organization. If your subscription has an open seat, the new workspace fills it at no extra cost. If not, a seat is added and charged pro rata. The workspace comes back as pending_provision and becomes active once its IPs are assigned, usually within a few minutes.
Request body
| Name | Type | Description |
|---|---|---|
name Required | string | Display name for the workspace. |
slots Required | array<object> | One entry per IP the plan includes. Each entry is an object with a country key: an ISO 3166-1 alpha-2 code, or null for any country. Send exactly subscription.plan.ip_count entries or you get SLOT_COUNT_MISMATCH. |
address Optional | string | null | Operating address to record against the workspace. |
country Optional | string | null | ISO 3166-1 alpha-2 country the workspace operates from. Unrelated to the exit country, which comes from slots. |
idempotency_key Optional | string | null | Unique value that makes retries safe. Retrying with the same key returns the original workspace instead of creating a second one. |
Example request
curl https://api.proxycharm.com/api/v1/public/workspaces \
-X POST \
-H "Authorization: Bearer $PROXYCHARM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Scraper EU",
"slots": [
{
"country": "DE"
}
],
"country": "DE",
"idempotency_key": "6f9619ff-8b86-d011-b42d-00c04fc964ff"
}' import os
import requests
API_KEY = os.environ["PROXYCHARM_API_KEY"]
response = requests.post(
"https://api.proxycharm.com/api/v1/public/workspaces",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"name": "Scraper EU",
"slots": [
{
"country": "DE"
}
],
"country": "DE",
"idempotency_key": "6f9619ff-8b86-d011-b42d-00c04fc964ff"
},
timeout=30,
)
payload = response.json()
if not payload["success"]:
raise RuntimeError(payload["error"]["message"])
print(payload["data"]) const apiKey = process.env.PROXYCHARM_API_KEY;
const response = await fetch("https://api.proxycharm.com/api/v1/public/workspaces", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Scraper EU",
"slots": [
{
"country": "DE"
}
],
"country": "DE",
"idempotency_key": "6f9619ff-8b86-d011-b42d-00c04fc964ff"
}),
});
const payload = await response.json();
if (!payload.success) {
throw new Error(payload.error.message);
}
console.log(payload.data); package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
type envelope struct {
Success bool `json:"success"`
Data json.RawMessage `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func main() {
body := []byte(`{
"name": "Scraper EU",
"slots": [
{
"country": "DE"
}
],
"country": "DE",
"idempotency_key": "6f9619ff-8b86-d011-b42d-00c04fc964ff"
}`)
req, err := http.NewRequest("POST", "https://api.proxycharm.com/api/v1/public/workspaces", bytes.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("PROXYCHARM_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var payload envelope
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
panic(err)
}
if !payload.Success {
panic(payload.Error.Message)
}
fmt.Println(string(payload.Data))
} <?php
$apiKey = getenv("PROXYCHARM_API_KEY");
$ch = curl_init("https://api.proxycharm.com/api/v1/public/workspaces");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $apiKey,
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"name" => "Scraper EU",
"slots" => [
[
"country" => "DE",
],
],
"country" => "DE",
"idempotency_key" => "6f9619ff-8b86-d011-b42d-00c04fc964ff",
]),
]);
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
if (!$payload["success"]) {
throw new RuntimeException($payload["error"]["message"]);
}
print_r($payload["data"]); Example response
{
"success": true,
"data": {
"account_id": "5d9f4e6a-8c2b-4713-9e5d-0f1a2b3c4d5e",
"subscription": {
"id": "7a2e4c19-3d5b-4f8a-9e6c-1b0d2f4a6c8e",
"status": "active",
"plan": {
"id": "1f3a5c79-2b4d-4e6f-8a0c-3d5e7f9a1b2c",
"name": "Starter",
"ip_count": 1,
"interval": "month",
"amount_cents": 1200,
"currency": "usd",
"amount_display": "12.00 USD",
"active": true,
"popular": true
},
"slots": [
{
"slot_index": 0,
"country": "US",
"status": "assigned",
"assignment_id": "9c1f7a2e-4b5d-4c3a-8e11-2d7b6f0a91c4",
"account_id": "3b7d2c48-6e19-4a5f-8b0c-9d1e2f3a4b5c"
},
{
"slot_index": 1,
"country": "DE",
"status": "pending",
"assignment_id": null,
"account_id": "5d9f4e6a-8c2b-4713-9e5d-0f1a2b3c4d5e"
}
],
"current_period_end": "2026-03-18T09:00:00Z",
"cancel_at_period_end": false,
"payment_action_required": false,
"pending_change": null,
"paid_seat_count": 2,
"open_seats": 0,
"accounts": [
{
"account_id": "3b7d2c48-6e19-4a5f-8b0c-9d1e2f3a4b5c",
"name": "Scraper US",
"status": "active",
"slots": [
{
"slot_index": 0,
"country": "US",
"status": "assigned",
"assignment_id": "9c1f7a2e-4b5d-4c3a-8e11-2d7b6f0a91c4",
"account_id": "3b7d2c48-6e19-4a5f-8b0c-9d1e2f3a4b5c"
}
]
},
{
"account_id": "5d9f4e6a-8c2b-4713-9e5d-0f1a2b3c4d5e",
"name": "Scraper EU",
"status": "pending_provision",
"slots": [
{
"slot_index": 1,
"country": "DE",
"status": "pending",
"assignment_id": null,
"account_id": "5d9f4e6a-8c2b-4713-9e5d-0f1a2b3c4d5e"
}
]
}
]
}
},
"error": null,
"context": {
"affected_entities": [
"account:5d9f4e6a-8c2b-4713-9e5d-0f1a2b3c4d5e"
],
"suggested_next_actions": [
"list_workspaces",
"get_subscription"
],
"warnings": []
},
"request_id": null,
"timestamp": "2026-02-18T09:31:04.512Z"
} Response fields
| Name | Type | Description |
|---|---|---|
account_id | string | Id of the workspace that was created or reused. |
subscription | object | The subscription after the change, so you can see the new seat count without a second call. |
id | string | Subscription id. |
status | string | One of incomplete, active, past_due, canceling, canceled, unpaid. Only active accepts workspace changes. |
plan | object | The plan this subscription is on. |
id | string | Plan id. |
name | string | Plan name as shown on the pricing page. |
ip_count | integer | IPs included per workspace. POST /workspaces must send exactly this many slots. |
interval | string | Billing interval: month or year. |
amount_cents | integer | Price per interval in minor units. |
currency | string | Lowercase ISO 4217 currency code. |
amount_display | string | Pre-formatted price, e.g. 12.00 USD. |
active | boolean | False when the plan is archived from new checkouts. |
popular | boolean | True for the featured plan at this interval. |
slots | array<object> | Every IP slot across the subscription, flattened. |
slot_index | integer | Zero-based index of the IP slot within the subscription. |
country | string | null | Requested ISO 3166-1 alpha-2 country, or null for any country. |
status | string | pending while awaiting an IP, assigned once filled, dropped when released. |
assignment_id | string | null | Assignment holding this slot, once one is assigned. |
account_id | string | null | Workspace this slot belongs to. |
current_period_end | string | null | ISO timestamp when the current period ends. |
cancel_at_period_end | boolean | True when cancellation is scheduled for the period boundary. |
payment_action_required | boolean | True when the cardholder must complete an authentication step. |
pending_change | object | null | A plan change awaiting payment. While this is non-null, POST /workspaces returns ADD_ACCOUNT_PENDING. |
plan_id | string | Plan the subscription is moving to. |
add_slots | array<object> | Slot countries being added. |
drop_slot_indexes | array<integer> | Slot indexes being released. |
accounts | array<object> | Per-workspace add and drop intent. |
paid_seat_count | integer | Number of workspaces you are paying for. |
open_seats | integer | Paid seats with no workspace attached. While this is above zero, POST /workspaces fills a seat you already pay for instead of charging you again. |
accounts | array<object> | Billed workspaces on this subscription, each with its own slots. |
account_id | string | Workspace id. |
name | string | Workspace display name. |
status | string | Workspace lifecycle state. |
slots | array<object> | Slots belonging to this workspace. |
slot_index | integer | Zero-based index of the IP slot within the subscription. |
country | string | null | Requested ISO 3166-1 alpha-2 country, or null for any country. |
status | string | pending while awaiting an IP, assigned once filled, dropped when released. |
assignment_id | string | null | Assignment holding this slot, once one is assigned. |
account_id | string | null | Workspace this slot belongs to. |
Errors
| Code | HTTP | What it means |
|---|---|---|
ACCOUNT_NAME_INVALID | 200 | The workspace name is empty or longer than the allowed length. |
ACCOUNT_ADDRESS_INVALID | 200 | The address exceeds the allowed length. |
ACCOUNT_COUNTRY_INVALID | 200 | The workspace country is not a two-letter ISO code. |
SLOT_INVALID_COUNTRY | 200 | A slot entry was not an object, or its country was not a two-letter ISO code. |
SLOT_COUNT_MISMATCH | 200 | The number of slots you sent does not match the IPs per workspace on your plan. |
SUBSCRIPTION_NOT_ACTIVE | 200 | There is no live subscription, or it has not finished checkout. |
SUBSCRIPTION_PAST_DUE | 200 | A payment failed, so seat changes are blocked. |
SUBSCRIPTION_CANCELING | 200 | The plan is set to end at the period boundary. |
ADD_ACCOUNT_PENDING | 200 | A plan change is still confirming payment. |
ADD_ACCOUNT_CONFLICT | 200 | Another seat change for the same organization was in flight, so this one lost the race. |
ADD_ACCOUNT_FAILED | 200 | The workspace could not be created — commonly because the idempotency key you sent already belongs to another organization's workspace. |
STRIPE_CUSTOMER_MISSING | 200 | The organization has no billing customer, so a paid seat cannot be charged. |
PLAN_NOT_FOUND | 200 | The plan behind the live subscription could not be loaded. |
API_KEY_INVALID | 401 | The Authorization header is missing the key prefix, the key does not exist, or its organization has been deleted. |
API_KEY_REVOKED | 401 | The key was revoked in the dashboard. |
API_KEY_EXPIRED | 401 | The key passed the expiry date it was created with. |
RATE_LIMIT_EXCEEDED | 429 | The key exceeded its request budget for the current window. |
Remove workspace
/workspaces/{account_id} Retire a workspace and release its IPs.
Marks the workspace removed and releases the IPs held by its slots. Returns the subscription after the change. The seat stays paid for the rest of the period and shows up as an open seat, so you can attach a new workspace to it without paying twice.
Path parameters
| Name | Type | Description |
|---|---|---|
account_id Required | string | Workspace id to remove. |
Example request
curl https://api.proxycharm.com/api/v1/public/workspaces/5d9f4e6a-8c2b-4713-9e5d-0f1a2b3c4d5e \
-X DELETE \
-H "Authorization: Bearer $PROXYCHARM_API_KEY" import os
import requests
API_KEY = os.environ["PROXYCHARM_API_KEY"]
response = requests.delete(
"https://api.proxycharm.com/api/v1/public/workspaces/5d9f4e6a-8c2b-4713-9e5d-0f1a2b3c4d5e",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=30,
)
payload = response.json()
if not payload["success"]:
raise RuntimeError(payload["error"]["message"])
print(payload["data"]) const apiKey = process.env.PROXYCHARM_API_KEY;
const response = await fetch("https://api.proxycharm.com/api/v1/public/workspaces/5d9f4e6a-8c2b-4713-9e5d-0f1a2b3c4d5e", {
method: "DELETE",
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
const payload = await response.json();
if (!payload.success) {
throw new Error(payload.error.message);
}
console.log(payload.data); package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
)
type envelope struct {
Success bool `json:"success"`
Data json.RawMessage `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func main() {
req, err := http.NewRequest("DELETE", "https://api.proxycharm.com/api/v1/public/workspaces/5d9f4e6a-8c2b-4713-9e5d-0f1a2b3c4d5e", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("PROXYCHARM_API_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var payload envelope
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
panic(err)
}
if !payload.Success {
panic(payload.Error.Message)
}
fmt.Println(string(payload.Data))
} <?php
$apiKey = getenv("PROXYCHARM_API_KEY");
$ch = curl_init("https://api.proxycharm.com/api/v1/public/workspaces/5d9f4e6a-8c2b-4713-9e5d-0f1a2b3c4d5e");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $apiKey,
],
]);
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
if (!$payload["success"]) {
throw new RuntimeException($payload["error"]["message"]);
}
print_r($payload["data"]); Example response
{
"success": true,
"data": {
"subscription": {
"id": "7a2e4c19-3d5b-4f8a-9e6c-1b0d2f4a6c8e",
"status": "active",
"plan": {
"id": "1f3a5c79-2b4d-4e6f-8a0c-3d5e7f9a1b2c",
"name": "Starter",
"ip_count": 1,
"interval": "month",
"amount_cents": 1200,
"currency": "usd",
"amount_display": "12.00 USD",
"active": true,
"popular": true
},
"slots": [
{
"slot_index": 0,
"country": "US",
"status": "assigned",
"assignment_id": "9c1f7a2e-4b5d-4c3a-8e11-2d7b6f0a91c4",
"account_id": "3b7d2c48-6e19-4a5f-8b0c-9d1e2f3a4b5c"
}
],
"current_period_end": "2026-03-18T09:00:00Z",
"cancel_at_period_end": false,
"payment_action_required": false,
"pending_change": null,
"paid_seat_count": 1,
"open_seats": 0,
"accounts": [
{
"account_id": "3b7d2c48-6e19-4a5f-8b0c-9d1e2f3a4b5c",
"name": "Scraper US",
"status": "active",
"slots": [
{
"slot_index": 0,
"country": "US",
"status": "assigned",
"assignment_id": "9c1f7a2e-4b5d-4c3a-8e11-2d7b6f0a91c4",
"account_id": "3b7d2c48-6e19-4a5f-8b0c-9d1e2f3a4b5c"
}
]
}
]
}
},
"error": null,
"context": {
"affected_entities": [
"account:5d9f4e6a-8c2b-4713-9e5d-0f1a2b3c4d5e",
"subscription:7a2e4c19-3d5b-4f8a-9e6c-1b0d2f4a6c8e"
],
"suggested_next_actions": [],
"warnings": []
},
"request_id": null,
"timestamp": "2026-02-18T09:31:04.512Z"
} Response fields
| Name | Type | Description |
|---|---|---|
subscription | object | null | The live subscription, or null when the organization has none. |
id | string | Subscription id. |
status | string | One of incomplete, active, past_due, canceling, canceled, unpaid. Only active accepts workspace changes. |
plan | object | The plan this subscription is on. |
id | string | Plan id. |
name | string | Plan name as shown on the pricing page. |
ip_count | integer | IPs included per workspace. POST /workspaces must send exactly this many slots. |
interval | string | Billing interval: month or year. |
amount_cents | integer | Price per interval in minor units. |
currency | string | Lowercase ISO 4217 currency code. |
amount_display | string | Pre-formatted price, e.g. 12.00 USD. |
active | boolean | False when the plan is archived from new checkouts. |
popular | boolean | True for the featured plan at this interval. |
slots | array<object> | Every IP slot across the subscription, flattened. |
slot_index | integer | Zero-based index of the IP slot within the subscription. |
country | string | null | Requested ISO 3166-1 alpha-2 country, or null for any country. |
status | string | pending while awaiting an IP, assigned once filled, dropped when released. |
assignment_id | string | null | Assignment holding this slot, once one is assigned. |
account_id | string | null | Workspace this slot belongs to. |
current_period_end | string | null | ISO timestamp when the current period ends. |
cancel_at_period_end | boolean | True when cancellation is scheduled for the period boundary. |
payment_action_required | boolean | True when the cardholder must complete an authentication step. |
pending_change | object | null | A plan change awaiting payment. While this is non-null, POST /workspaces returns ADD_ACCOUNT_PENDING. |
plan_id | string | Plan the subscription is moving to. |
add_slots | array<object> | Slot countries being added. |
drop_slot_indexes | array<integer> | Slot indexes being released. |
accounts | array<object> | Per-workspace add and drop intent. |
paid_seat_count | integer | Number of workspaces you are paying for. |
open_seats | integer | Paid seats with no workspace attached. While this is above zero, POST /workspaces fills a seat you already pay for instead of charging you again. |
accounts | array<object> | Billed workspaces on this subscription, each with its own slots. |
account_id | string | Workspace id. |
name | string | Workspace display name. |
status | string | Workspace lifecycle state. |
slots | array<object> | Slots belonging to this workspace. |
slot_index | integer | Zero-based index of the IP slot within the subscription. |
country | string | null | Requested ISO 3166-1 alpha-2 country, or null for any country. |
status | string | pending while awaiting an IP, assigned once filled, dropped when released. |
assignment_id | string | null | Assignment holding this slot, once one is assigned. |
account_id | string | null | Workspace this slot belongs to. |
Errors
| Code | HTTP | What it means |
|---|---|---|
ORGANIZATION_PERMISSION_DENIED | 403 | The workspace id in the path belongs to another organization, or the profile that created the key is not an owner. Invoices require owner-level access. |
ACCOUNT_NOT_FOUND | 200 | The workspace was deleted between your read and your write. |
ACCOUNT_NOT_REMOVABLE | 200 | The workspace is already removed, or is still confirming payment, so it is not in a removable state. |
LAST_ACCOUNT_CANCEL_ONLY | 200 | You tried to remove the only live workspace on the plan. |
SUBSCRIPTION_NOT_ACTIVE | 200 | There is no live subscription, or it has not finished checkout. |
API_KEY_INVALID | 401 | The Authorization header is missing the key prefix, the key does not exist, or its organization has been deleted. |
API_KEY_REVOKED | 401 | The key was revoked in the dashboard. |
API_KEY_EXPIRED | 401 | The key passed the expiry date it was created with. |
RATE_LIMIT_EXCEEDED | 429 | The key exceeded its request budget for the current window. |