SolumeCore Reseller API: Guide, Reference and Examples
Buy, operate and renew VPS and VDS servers by API: curl quickstart, endpoint reference, errors, rate limits, billing, webhooks and OpenAPI.
Resell VPS and VDS servers from your own systems. With the reseller API you browse the catalog, quote and buy servers, operate them (power, reinstall, passwords, network, SSH keys, firewall), pay renewals, read invoices and receive events, all over HTTPS and JSON.
This is the complete guide: concepts, quickstart, a reference of every endpoint, errors, limits, billing rules, webhooks, best practices and the changelog. The machine-readable contract is the OpenAPI file (production) and the sandbox OpenAPI file. A shorter guide to the free test environment is in the sandbox guide.
Overview
- Who it is for. Hosting resellers, panels, billing systems and automation that sell or manage VPS and VDS servers. Game servers, shared web hosting and apps are not resellable through this API: a plan of another type is rejected with
product_not_resellable. - It costs nothing to use. There is no fee for the API, for calls or for keys. You pay the normal price of each service you buy or renew, minus the discount agreed for your key, plus the card fee agreed for your key when a card is charged. Both default to 0% and are agreed per reseller; every quote and invoice shows each line.
- Base URLs.
| Environment | Base URL | Key prefix |
|---|---|---|
| Production | https://api.solumecore.com/v1 |
sk_live_ |
| Sandbox | https://api.solumecore.com/sandbox/v1 |
sk_test_ |
- Same API in both. The sandbox has the same paths, bodies, errors and headers on a private virtual account (see Sandbox). Moving to production means changing the base URL and the key.
- Machine-readable. Every response is JSON in UTF-8 with English
snake_casefield names. Money is always integer cents.
Getting started
Create a key. In the client panel go to API and webhooks, open API keys and click New key. The secret (
sk_live_...) is shown once; store it in a secret manager. A new key can only read and manage services until staff grants more. You can have up to 10 active live keys; revoke one to create another.Ask for the access you need. Open a ticket from the panel (the Ask to authorize IPs button prepares it) and tell the team:
- the public IP addresses your code will call from. A key only works from IP addresses staff registered for it; any other address gets
403 ip_not_allowed; - whether you want to buy and renew by API (
purchasepermission) and with what spending caps: maximum per order, maximum per rolling 24 hours and orders per rolling hour; - whether you want to manage balance and auto-renew by API (
billingpermission); - the discount and card fee you have agreed.
Only staff can grant permissions, caps and IPs. You can see what your key has been granted in the panel (What your keys can do) and in
GET /v1/me.- the public IP addresses your code will call from. A key only works from IP addresses staff registered for it; any other address gets
Set up payment on the website. The API charges your balance first and then your default saved card, off session. Save the card and accept the current automatic-charge mandate (version 2026-11-v2) in Billing in the panel; it cannot be done through the API. Top up the balance in the panel or with
POST /v1/balance/top-ups. The full list is in Requirements before you buy by API.Try everything first in the sandbox. Create a sandbox key (
sk_test_...) in the same page; it needs no approval and no IPs. See Sandbox.Make your first call.
export SOLUME_KEY=sk_live_xxxxxxxx
curl https://api.solumecore.com/v1/me -H "Authorization: Bearer $SOLUME_KEY"Permissions (scopes)
Each key has a set of permissions granted by staff.
| Scope | Allows |
|---|---|
read |
Catalog, quotes, services, tasks, metrics, invoices, orders, events |
manage |
Power, reinstall, reset password, read the one-time credentials, hostname, rDNS, SSH keys, firewall, cancel and withdraw cancel |
purchase |
Anything that spends money: create orders, renew, pay invoices |
billing |
Balance, top-ups, payment-method info, auto-renew mode |
A call without the needed permission answers 403 insufficient_scope with details.required_scope, before anything else happens. Creating an order with auto_renew other than off also needs billing.
Spending caps
purchase is granted only together with caps. They are in cents of the account currency and are checked before charging (the amount is reserved and released if the payment fails, so parallel calls cannot overshoot):
| Cap | Meaning | Error when exceeded |
|---|---|---|
per_order_cents |
Maximum for one order, renewal or payment | 403 spending_cap_exceeded, details.cap = "per_order" |
daily_cents |
Maximum in a rolling 24 hours | 403 spending_cap_exceeded, details.cap = "daily" |
orders_per_hour |
Orders in a rolling hour (10 if not set) | 403 spending_cap_exceeded, details.cap = "hourly_orders" |
A key with purchase and no caps loaded cannot buy: 403 spending_caps_not_set. At most 3 orders in flight per key; more answers 429 too_many_in_flight. GET /v1/me shows the caps and your usage (spent_today_cents, orders_last_hour), and POST /v1/quotes tells you if an order would exceed a cap (would_exceed_cap).
Requirements before you buy by API
A purchase, a renewal or an invoice payment by API needs all of these. Check each one before you go live; GET /v1/me and GET /v1/payment-method show most of them.
| Requirement | Who does it | How to check | What you get if it is missing |
|---|---|---|---|
| An authorized IP address for the key | Solume staff, after your ticket | GET /me: allowed_ips_count above 0 |
403 ip_not_allowed on every call |
The purchase permission (and billing to change auto-renew or top up) |
Solume staff | GET /me: scopes |
403 insufficient_scope |
| Spending caps loaded: per order and per day (orders per hour defaults to 10) | Solume staff | GET /me: can_purchase: true and caps |
403 spending_caps_not_set, or 403 spending_cap_exceeded |
| A saved default card with the automatic-charge mandate, version 2026-11-v2, accepted for that card | You, in the panel (Billing) | GET /payment-method: chargeable_off_session: true |
402 requires_action with details.reason no_card or no_mandate |
Balance for the total, if you pay with payment: "balance" or want to avoid the card |
You, in the panel or POST /balance/top-ups |
GET /balance |
402 insufficient_balance |
| The order passes the website's trust and anti-fraud checks | Automatic | n/a | 403 order_rejected |
About the mandate: it is the authorization the account owner gives, on the website, to charge a saved card without being present. Version 2026-11-v2 covers renewals, invoice payments and purchases started by your API keys, within the caps; an older mandate (version 2026-10) only covered renewals and automatic top-ups, so a card saved under it cannot be charged by API purchases until the owner accepts the new text. mandate_accepted in GET /payment-method says a mandate of a version that covers API purchases (2026-11-v2) is accepted for the card (an older one does not count, even though it exists); chargeable_off_session says the API can really use it (that version, charges not paused by the owner and automatic charges enabled platform-wide). Both mirror what POST /orders, POST /invoices/{id}/pay and POST /services/{id}/renew enforce with payment: "card" or "auto". One difference: auto_renew: "card" / "balance_and_card" only needs a mandate valid for renewals (any accepted version), so an account that still has the older mandate can enable it while mandate_accepted is false; its renewals are charged by the automatic-renewal job, not by an API purchase. With only a balance you do not need the card, but every payment: "auto" order that the balance does not cover will fail with 402.
Two more cases: auto_renew: "card" or "balance_and_card" (on POST /orders or PUT /services/{id}/auto-renew) also checks that a default card and an accepted mandate exist, and answers 409 conflict with details.reason no_default_card or mandate_not_accepted; and payment: "card" while card payments are switched off answers 402 card_declined with details.reason: "stripe_disabled".
Authentication and environments
Send the key in the Authorization header on every request:
Authorization: Bearer sk_live_xxxxxxxx- Live keys (
sk_live_) work on/v1from registered IPs only. Sandbox keys (sk_test_) work on/sandbox/v1only, from any IP. - A key on the wrong environment answers
403 wrong_environment. - An unknown or revoked key answers
401 unauthorized(both look the same, on purpose). A suspended or closed account answers403 unauthorized. - Failed authentications are throttled per IP (20 per minute by default) before the key is even looked up; beyond that you get
429 rate_limited. - Keys are for the account owner only; there is no delegation. A key only ever sees the services, orders and invoices of its own account: a service id that belongs to someone else answers the same
404 not_foundas one that does not exist.
Conventions
| Topic | Rule |
|---|---|
| Format | JSON in and out, UTF-8. Content-Type: application/json on POST and PUT with a body. Maximum body 32 KB. |
| Strict input | Unknown fields in a body or query string are rejected with 400 invalid_request. |
| Ids | A service id is its public code (for example K3F8QX2P). Orders, invoices, tasks, events and SSH keys use prefixed ids: ord_..., inv_..., tsk_..., evt_..., key_.... Internal ids are never exposed. |
| Money | { "amount_cents": 720, "currency": "USD" }: an integer in minor units, never a float. |
| Dates | ISO 8601 in UTC, for example 2026-11-05T12:00:00.000Z. A date that does not apply is null. |
| Pagination | ?limit= (1 to 100, default 25) and ?cursor=. Lists answer { "data": [...], "next_cursor": "..." | null }. Cursors are opaque: pass back what you received. A null cursor is the last page. |
| Request id | Every response has an X-Request-Id header (also in error.request_id). Log it and quote it to support. |
| Rate headers | Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds) for the general bucket. A 429 also carries Retry-After (seconds). |
| Async work | Anything that runs on the server platform answers 202 with { "task": { "id": "tsk_...", "status": "queued" } }. Poll GET /v1/tasks/{id}. |
| Idempotency | See below. |
Idempotency
Send Idempotency-Key (8 to 64 characters: letters, digits, _ or -) on every call that moves money or is destructive. It is required on:
POST /orders, POST /services/{id}/renew, POST /services/{id}/reinstall, POST /services/{id}/reset-password, POST /services/{id}/cancel, POST /invoices/{id}/pay and POST /balance/top-ups.
How it works:
- Same key and same request within 24 hours: the stored response is replayed with the header
Idempotent-Replay: trueand nothing runs again. - Same key with a different request:
422 idempotency_key_reused. This also holds when the first order was recovered after an interrupted call: the key stays bound to the original request. - Same key while the first call is still running:
409 request_in_progress(retry shortly). - Missing key:
400 idempotency_key_required. Bad format:400 invalid_request. - Keys are per API key. Generate one per intent (for example
order-<your-order-id>), reuse it when you retry, and never reuse it for another purchase. - If the call fails with an error (for example
card_declined) or a5xx, the key is released, so you can retry with the same key after fixing the cause. - Stored responses never contain passwords or other secrets.
- Password-bearing routes (
GET /credentials) do not use idempotency.
Errors
Every non-2xx response has this shape. code is stable and documented; message is human text that may change, so never branch on it.
{
"error": {
"code": "insufficient_scope",
"message": "This API key does not have the \"purchase\" scope.",
"request_id": "req_5b1c0e7d2a9f4c3b8e16",
"details": { "required_scope": "purchase" }
}
}Error codes
| Code | HTTP | Cause | What to do |
|---|---|---|---|
invalid_request |
400 (also 413, 4xx from validation) | A malformed or unknown field, a bad cursor, a body over 32 KB, a bad Idempotency-Key format, a wrong value. details may list the problems. |
Fix the request. Do not retry unchanged. |
idempotency_key_required |
400 | A route that needs Idempotency-Key was called without it. |
Add the header. |
unauthorized |
401 (403 for an inactive account) | Missing, unknown or revoked key; or the account is suspended or closed. | Check the key; contact support for an inactive account. |
ip_not_allowed |
403 | The calling IP is not registered for this key (details.ip), or the IP is blocked. |
Ask support to authorize the IP. Check you are not calling through a proxy or a different egress IP. |
wrong_environment |
403 | A sandbox key on /v1, or a live key on /sandbox/v1. |
Use the matching base URL. |
insufficient_scope |
403 | The key lacks a permission (details.required_scope). Reading the credentials needs manage, not read. |
Ask support for the permission. |
spending_caps_not_set |
403 | The key has purchase but no caps loaded. |
Ask support to set the caps. |
spending_cap_exceeded |
403 | An order, the rolling 24 h total, the hourly order count, or the monthly limit the account owner set on the account was exceeded (details.cap: per_order, daily, hourly_orders, account_monthly; and remaining_cents where it applies). |
Wait for the window to pass, buy less, or ask for higher caps. |
product_not_resellable |
403 | The plan is not a VPS/VDS plan. | Use plan ids from GET /catalog/plans. |
order_rejected |
403 | The order did not pass the trust, anti-fraud and anti-abuse checks that also apply on the website. The message is generic on purpose. | Contact support; do not retry in a loop. |
not_found |
404 | The resource does not exist or is not yours; also an unknown plan, location or template. | Check the id. |
credentials_not_available |
404 | This key did not buy or reset the service, or the 24 h window passed (details.reason). |
Call reset-password to get a new password. |
conflict |
409 | The request conflicts with the current state. details.reason tells why (see below). |
Read details.reason. |
request_in_progress |
409 | The same Idempotency-Key is still running, or an order for it is still being processed. |
Wait and retry with the same key; for orders, poll GET /orders/{id} if details.order_id is present. |
out_of_stock |
409 | No stock in that location, or the order was paid but could not be delivered and was refunded (details.refunded_to). |
Choose another location or plan. |
service_not_ready |
409 | The service is still provisioning, or a reinstall has not finished. Cancelling with at: "now" in that state also answers it. |
Poll the service and retry when it is active. |
service_suspended |
409 | The service is suspended. Cancelling with at: "now" a service that is suspended, paused or in error also answers it: use at: "period_end" or contact support. |
Pay the open invoice (POST /services/{id}/renew). |
firewall_unavailable |
409 / 503 | 409: the service has no firewall (engine none), no IP, or a fake delivery. 503: the firewall provider failed (a failed read is never returned as an empty list), is down or is not configured. |
409: nothing to do. 503: retry later. |
idempotency_key_reused |
422 | The key was used with a different request. | Use a new key for a new request. |
invalid_option |
422 | An unknown option, a repeated option, or an option with no price for that cycle. | Check GET /catalog/options?plan=. |
feature_not_supported |
422 | The route belongs to another firewall engine (details.engine is the service's engine). |
Read GET /services/{id}/firewall first. |
credentials_consumed |
410 | The one-time password was already read. | Call POST /services/{id}/reset-password. |
requires_action |
402 | The card needs the customer. details.reason: authentication_required (3-D Secure), no_card (no saved default card) or no_mandate (the mandate version 2026-11-v2 is not accepted for that card, or automatic charges are paused or disabled). Nothing was created or charged. |
Ask the customer to fix it in the panel (Billing: save the card, accept the mandate) or to authenticate, or top up the balance and use payment: "balance". |
card_declined |
402 | The card was declined (details.reason: declined or insufficient_funds), or card payments are switched off (stripe_disabled). Nothing was created or charged. |
Ask the customer to update the card; retry with balance. |
insufficient_balance |
402 | The balance does not cover the total (details.required_cents). With payment: "auto" it also appears when the balance is short and no card can be used (details.card says why). |
Top up the balance. |
rate_limited |
429 | A bucket is full (details.bucket, details.retry_after). |
Wait Retry-After seconds, then retry. Use exponential backoff. |
too_many_in_flight |
429 | Three orders of the key are still being created. | Wait for them (Retry-After: 15). |
maintenance |
503 | The node blocks panel actions right now. | Retry later. |
internal_error |
500 (502 reading the card; 503 when the platform is temporarily unavailable) | An unexpected error. After a card charge on a renewal or payment, it can mean the outcome could not be confirmed. A 503 (message "The service is temporarily unavailable") comes with Retry-After: 2 when a platform dependency does not answer. |
Quote request_id. On a 503 retry after Retry-After (keep the same Idempotency-Key). Before retrying a payment after a 500, read the invoice (GET /invoices/{id}) or order. |
409 conflict carries details.reason for the cases that have no own code: service_cancelled, service_error, service_paused, cancellation_scheduled, no_billing_cycle, grouped_invoice (pay it with POST /invoices/{id}/pay), invoice_not_payable, payment_in_progress (an automatic card charge or an unconfirmed payment of that invoice is pending; retry later), no_default_card, mandate_not_accepted. A 409 on cancel means the service is already cancelled, is a sub-server, or is a permanent or courtesy service (with at: "now").
Rate limits
Limits use a sliding window and are applied in addition to the general limit. All values are set by Solume (and Solume can override the general and power limits for a single key); GET /v1/me returns the exact limits that apply to your key under rate_limits. Defaults:
| Bucket | Default | Counted per |
|---|---|---|
| general (every call) | 120 / minute | key and IP |
quotes (POST /quotes) |
30 / minute | key |
orders (POST /orders attempts) |
30 / hour | key |
| power | 6 / minute and 40 / hour | service, plus 60 / minute per key |
| reinstall | 1 / 10 minutes and 5 / day | service, plus 20 / day per key |
| reset-password | 3 / hour | service, plus 20 / hour per key |
| renew and pay invoice | 5 / hour | service |
| service-settings (hostname, rDNS, SSH keys, firewall changes) | 10 / hour | service |
| cancel (and withdraw) | 3 / day | service |
| credentials | 10 / hour | service |
service-reads (GET /services/{id}, /status, /metrics, GET /firewall and every gsk read) |
60 / minute | service, plus 300 / minute per key |
| failed authentications | 20 / minute | source IP |
Per-service limits count every key and IP of your account on that service, so adding IPs does not raise them. The service-reads bucket protects the servers and the firewall provider from polling; it counts on top of the general bucket, and those calls also time out after 8 seconds. GET /me does not list service-reads yet. Purchases per hour are also capped by the key's orders_per_hour cap (10 by default). Exceeding a limit answers 429 rate_limited with Retry-After.
Quickstart
The whole life of a server with curl. Replace the ids with those from your own catalog. Examples are trimmed; they are the same shapes the OpenAPI file documents, and each example in it is validated against the real response schema by an automated test. You can run the same flow in the sandbox by changing the base URL and the key.
export API=https://api.solumecore.com/v1
export SOLUME_KEY=sk_live_xxxxxxxx
alias solume='curl -sS -H "Authorization: Bearer $SOLUME_KEY" -H "Content-Type: application/json"'1. Check your key, IP and payment method
solume $API/me
solume $API/payment-method
solume $API/balance/me shows scopes, caps, usage, discount_percent and card_fee_percent. /payment-method must show "chargeable_off_session": true if you want card payments:
{
"payment_method": {
"brand": "visa",
"last4": "4242",
"exp_month": 12,
"exp_year": 2029,
"mandate_accepted": true,
"chargeable_off_session": true
}
}If it is null or mandate_accepted is false, save the card and accept automatic charges in Billing in the panel.
2. Browse the catalog
solume "$API/catalog/plans?type=vps"
solume $API/catalog/locations
solume "$API/catalog/os-templates?plan=cm3x7k2p00001vps2gb"
solume "$API/catalog/options?plan=cm3x7k2p00001vps2gb"A plan shows its specs, the prices per cycle (monthly, quarterly, semiannual, annual) with list_price (website) and your_price (with your discount), and available.
3. Quote
A quote has no side effects and uses the same body as the order:
solume -X POST $API/quotes -d '{
"plan_id": "cm3x7k2p00001vps2gb",
"location_id": "us-mia-1",
"billing_cycle": "monthly",
"os_template_id": "cm3x7k2p00002ubuntu2404",
"hostname": "web-1",
"options": [{ "slug": "anti-ddos", "answer": "yes" }]
}'{
"quote": {
"lines": [
{
"kind": "plan",
"description": "VPS Small - monthly",
"amount": { "amount_cents": 800, "currency": "USD" }
},
{
"kind": "option",
"slug": "anti-ddos",
"description": "Anti-DDoS",
"amount": { "amount_cents": 300, "currency": "USD" }
},
{
"kind": "reseller_discount",
"description": "Reseller discount (10%)",
"amount": { "amount_cents": -110, "currency": "USD" }
}
],
"total": { "amount_cents": 990, "currency": "USD" },
"card_fee": { "percent": 3, "amount": { "amount_cents": 30, "currency": "USD" } },
"total_with_card": { "amount_cents": 1020, "currency": "USD" },
"payable_with": ["balance", "card"],
"would_exceed_cap": false
}
}(Other fields: plan, location, hostname, billing_cycle, currency, list_total, reseller_discount, subtotal, tax, cap, cap_remaining_cents.) total is the price paying with the balance; the card fee is a separate line added only when the payment ends up on the card.
4. Buy
solume -X POST $API/orders -H "Idempotency-Key: order-acme-42-001" -d '{
"plan_id": "cm3x7k2p00001vps2gb",
"location_id": "us-mia-1",
"billing_cycle": "monthly",
"os_template_id": "cm3x7k2p00002ubuntu2404",
"hostname": "web-1",
"ssh_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl me@laptop",
"options": [{ "slug": "anti-ddos", "answer": "yes" }],
"payment": "auto",
"metadata": { "customer": "acme-42" }
}'201 answers the order and the new service in provisioning:
{
"order": {
"id": "ord_9173eb967115608b2f",
"status": "provisioning",
"total": { "amount_cents": 720, "currency": "USD" },
"payment_method": "balance",
"service_id": "K3F8QX2P",
"auto_renew": "off"
},
"service": {
"id": "K3F8QX2P",
"hostname": "web-1",
"status": "provisioning",
"power_state": null,
"ips": []
}
}(Trimmed. Orders also carry lines, subtotal, tax, billing_cycle, invoice_number, invoice_id (the inv_… id that GET /invoices/:id and POST /invoices/:id/pay use), metadata...) The response never contains credentials. If you retry with the same Idempotency-Key you get the same response with Idempotent-Replay: true, and nothing is charged twice. metadata (up to 10 keys, 200 characters per value) is stored on the order and the service and returned everywhere, so you can map it to your own records.
Payment errors create nothing: 402 requires_action, 402 card_declined, 402 insufficient_balance.
5. Wait until the service is active
solume $API/services/K3F8QX2PPoll every few seconds (back off to 10 to 30 seconds) until status is active (error means provisioning failed and the order is refunded; check GET /orders/{id}). Once active, power_state is running and ips has the addresses.
6. Read the credentials once
solume $API/services/K3F8QX2P/credentials{ "username": "root", "password": "wm^j&Xt&=L!bkZEK&M2A", "expires_at": "2026-11-06T12:00:11.000Z" }The password is available for 24 hours after provisioning and is consumed by the first successful read: a second read answers 410 credentials_consumed. Store it right away in your secret store and do not log it. The response has Cache-Control: no-store. Only the key that bought the service (or that asked for a reset or reinstall) can read it, and it needs the manage permission (a key with only read gets 403 insufficient_scope). There is no HEAD version of this route, so nothing can consume the password by accident. Windows servers use the user Administrator.
7. Operate the server
# restart: 202 + task
solume -X POST $API/services/K3F8QX2P/power/restart
solume $API/tasks/tsk_883e0ff8c42d09b420
# live status and metrics
solume $API/services/K3F8QX2P/status
solume "$API/services/K3F8QX2P/metrics?range=24h"A task goes queued, running, then succeeded or failed (with error.code).
8. Renew
solume -X POST $API/services/K3F8QX2P/renew -H "Idempotency-Key: renew-K3F8QX2P-2026-12" -d '{"payment":"auto"}'It pays the open renewal invoice (creating it if the renewal window has opened) with the same caps and payment order as orders. See Billing and lifecycle.
9. Reinstall or change the operating system
solume -X POST $API/services/K3F8QX2P/reinstall -H "Idempotency-Key: reinstall-K3F8QX2P-001" \
-d '{"os_template_id":"cm3x7k2p00002ubuntu2404","confirm":true}'This wipes the disk, so confirm: true is required. It returns a task; when it succeeds, read the new password with GET /credentials (a reinstall generates a new one).
10. Reset the password
solume -X POST $API/services/K3F8QX2P/reset-password -H "Idempotency-Key: reset-K3F8QX2P-001"
# when the task succeeds:
solume $API/services/K3F8QX2P/credentials11. Firewall
solume $API/services/K3F8QX2P/firewall
solume -X POST $API/services/K3F8QX2P/firewall/geoblock -d '{"country":"CN"}'The available operations depend on the service's firewall engine; see Firewall.
12. Cancel
# at the end of the paid period (can be withdrawn)
solume -X POST $API/services/K3F8QX2P/cancel -H "Idempotency-Key: cancel-K3F8QX2P-001" -d '{"at":"period_end","confirm":true}'
solume -X POST $API/services/K3F8QX2P/cancel/withdrawat: "now" is destructive and cannot be withdrawn, and it only works on an active service: while it is provisioning or being reinstalled you get 409 service_not_ready, and for a suspended, paused or errored service 409 service_suspended (an unpaid or staff suspension cannot be escaped by cancelling). at: "period_end" is always possible. confirm: true is required for both.
The same flow in Node.js and Python
Buy, wait for active, read the credentials once.
const API = 'https://api.solumecore.com/v1';
const KEY = process.env.SOLUME_KEY; // sk_live_... (or sk_test_... with /sandbox/v1)
async function call(method, path, body, idempotencyKey) {
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch(API + path, {
method,
headers: {
Authorization: `Bearer ${KEY}`,
...(body ? { 'Content-Type': 'application/json' } : {}),
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
if (res.status === 429 || res.status >= 500) {
const wait = Number(res.headers.get('retry-after')) || 2 ** attempt;
await new Promise((r) => setTimeout(r, wait * 1000));
continue; // safe: same Idempotency-Key
}
const json = await res.json();
if (!res.ok)
throw Object.assign(new Error(json.error.code), {
response: json,
requestId: res.headers.get('x-request-id'),
});
return json;
}
throw new Error('too many retries');
}
const { service } = await call(
'POST',
'/orders',
{
plan_id: 'cm3x7k2p00001vps2gb',
location_id: 'us-mia-1',
billing_cycle: 'monthly',
os_template_id: 'cm3x7k2p00002ubuntu2404',
hostname: 'web-1',
payment: 'auto',
},
'order-acme-42-001',
);
let current = service;
while (current.status === 'provisioning') {
await new Promise((r) => setTimeout(r, 5000));
current = await call('GET', `/services/${service.id}`);
}
if (current.status !== 'active') throw new Error(`provisioning ended as ${current.status}`);
const { username, password } = await call('GET', `/services/${service.id}/credentials`);
// Store the password in your secret manager NOW: a second read answers 410.
console.log(username, current.ips[0].address);import os, time, requests
API = "https://api.solumecore.com/v1"
KEY = os.environ["SOLUME_KEY"] # sk_live_... (or sk_test_... with /sandbox/v1)
def call(method, path, body=None, idempotency_key=None):
headers = {"Authorization": f"Bearer {KEY}"}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
for attempt in range(5):
res = requests.request(method, API + path, json=body, headers=headers, timeout=30)
if res.status_code == 429 or res.status_code >= 500:
time.sleep(int(res.headers.get("Retry-After", 2 ** attempt)))
continue # safe: same Idempotency-Key
if not res.ok:
err = res.json()["error"]
raise RuntimeError(f'{err["code"]} (request {err["request_id"]})')
return res.json()
raise RuntimeError("too many retries")
service = call("POST", "/orders", {
"plan_id": "cm3x7k2p00001vps2gb",
"location_id": "us-mia-1",
"billing_cycle": "monthly",
"os_template_id": "cm3x7k2p00002ubuntu2404",
"hostname": "web-1",
"payment": "auto",
}, "order-acme-42-001")["service"]
while service["status"] == "provisioning":
time.sleep(5)
service = call("GET", f'/services/{service["id"]}')
if service["status"] != "active":
raise RuntimeError(f'provisioning ended as {service["status"]}')
creds = call("GET", f'/services/{service["id"]}/credentials')
# Store creds["password"] in your secret manager NOW: a second read answers 410.
print(creds["username"], service["ips"][0]["address"])Endpoint reference
R read, M manage, P purchase, B billing. "Idem" means Idempotency-Key is required. Every route can also answer 400, 401, 403, 429 and 500 as described in Errors; a route with {id} answers 404 not_found for a service that is not yours or does not exist. The limit column is the bucket that counts besides the general one (see Rate limits).
Account
| Method and path | Scope | Limit | Notes |
|---|---|---|---|
GET /me |
any | The key (name, key_prefix, tag), scopes, can_purchase, caps, usage, discount_percent, card_fee_percent, currency, allowed_ips_count and the rate_limits that apply. |
|
GET /balance |
B | { "balance": { "amount_cents": 2500, "currency": "USD" } } |
|
POST /balance/top-ups |
B, Idem | Body { "amount_cents": 5000, "method": "card" | "paypal" | "crypto" }. 201 with a hosted checkout_url and expires_at; the customer pays there. Same minimum, maximum and limits as the website top-up (a 400 shows them). 409 conflict when that method or top-ups are not available. |
|
GET /payment-method |
B | The default card and whether it can be charged off session: { "payment_method": { brand, last4, exp_month, exp_year, mandate_accepted, chargeable_off_session } | null }. Read-only. |
|
GET /events?since=&type=&limit=&cursor= |
R | Pull alternative to webhooks. See Webhooks and events. |
Catalog (VPS and VDS only)
| Method and path | Scope | Limit | Notes |
|---|---|---|---|
GET /catalog/plans?type=vps|vds&location= |
R | Plans with specs, prices per cycle (list_price, your_price), available and availability (in_stock, out_of_stock, coming_soon). |
|
GET /catalog/locations |
R | id (use as location_id), name, country, city (null for now), in_stock, coming_soon. |
|
GET /catalog/os-templates?plan= |
R | Templates valid for that plan: id, name, family (linux or windows), distro, version, min_ram_mb, default_user. plan is required. |
|
GET /catalog/options?plan= |
R | Purchase options (for example Anti-DDoS) with their prices per cycle: recurring, one_time and first_invoice, or null for a cycle without a price. |
|
POST /quotes |
R | quotes | Same body as POST /orders. Returns the full breakdown with no side effects. Errors: 403 product_not_resellable, 404, 422 invalid_option. |
Orders
| Method and path | Scope | Limit | Notes |
|---|---|---|---|
POST /orders |
P, Idem | orders | See the body below. 201 { order, service }. |
GET /orders/{id} |
R | { order, service }; the service is null until one exists. |
|
GET /orders?limit=&cursor= |
R | Your API orders, newest first. |
Body of POST /orders and POST /quotes:
| Field | Type | Notes |
|---|---|---|
plan_id |
string | From GET /catalog/plans. |
location_id |
string | The location code, from GET /catalog/locations. Must match the plan. |
billing_cycle |
monthly, quarterly, semiannual, annual |
|
os_template_id |
string | From GET /catalog/os-templates?plan=. |
hostname |
string, 3 to 63 | Lowercase letters, digits and hyphens; cannot start or end with -. |
ssh_public_key |
string, optional | One public key line. Not together with ssh_key_ids. |
ssh_key_ids |
array, optional | Up to 5 ids of SSH keys already in your account. |
options |
array, optional | [{ "slug": "anti-ddos", "answer": "yes" | "no" }], up to 20. |
payment |
auto (default), balance, card |
See Payments. |
auto_renew |
off (default), balance, balance_and_card, card |
Anything but off needs billing. |
metadata |
object, optional | Up to 10 keys (letters, digits, _, ., -), values up to 200 characters. Echoed back. |
Order status is pending, paid, provisioning, completed, failed or refunded. Errors of POST /orders: 402 (requires_action, card_declined, insufficient_balance), 403 (spending_cap_exceeded, spending_caps_not_set, order_rejected, product_not_resellable), 409 (out_of_stock, request_in_progress), 422 invalid_option, 429 too_many_in_flight.
Services
The service object: id, hostname, status (provisioning, active, suspended, paused, cancelled, error), power_state (running, stopped, paused, unknown), plan (id, name, cores, ram_mb, disk_gb), location (id, name, country), os, ips (address, version, primary, rdns), created_at, origin (api or web), metadata and billing (see Billing and lifecycle). List items omit power_state and billing.
| Method and path | Scope | Limit | Notes |
|---|---|---|---|
GET /services?status=&limit=&cursor= |
R | Your VPS and VDS services. | |
GET /services/{id} |
R | service-reads | The full object. |
GET /services/{id}/status |
R | service-reads | Cheap live snapshot: power_state, uptime_seconds, last_seen, cpu_percent, ram (used_bytes, total_bytes), network (in_bytes, out_bytes). |
GET /services/{id}/metrics?range=1h|24h|7d |
R | service-reads | { range, available, points: [{ time, cpu_percent, ram_percent, disk_read_bytes_per_sec, ... }] }. |
GET /services/{id}/credentials |
M | credentials | One-time { username, password, expires_at }. See the quickstart. Errors: 404 credentials_not_available, 409 service_not_ready, 410 credentials_consumed. |
POST /services/{id}/power/{action} |
M | power | start, stop (hard power-off), restart (graceful reboot) or shutdown (graceful). 202 task. Errors: 409 service_suspended or service_not_ready, 503 maintenance. |
POST /services/{id}/reinstall |
M, Idem | reinstall | { "os_template_id": "...", "ssh_key_ids": [...]?, "confirm": true }. Wipes the disk. 202 task. |
POST /services/{id}/reset-password |
M, Idem | reset-password | 202 task; read the new password with GET /credentials. |
GET /tasks/{id} |
R | { id, type, status, service_id, created_at, started_at, finished_at, error }. type: power.start, power.stop, power.restart, power.shutdown, reinstall, reset_password. error.code: hypervisor_error, guest_agent_error, reinstall_failed or timeout. Tasks are kept 7 days and any key of the account can read them. |
|
PUT /services/{id}/hostname |
M | service-settings | { "hostname": "web-prod-1" } changes the real hostname through the guest agent. Answers { hostname, requires_reboot } (Windows applies it on reboot). |
GET /services/{id}/network |
R | ips with their rdns (ptr, status: pending, applied, rejected, forward_confirmed) and rdns_available. |
|
PUT /services/{id}/network/ips/{ip}/rdns |
M | service-settings | { "ptr": "mail.example.com" }. Answers { ip, ptr, status: "pending" }: the record is applied by staff. |
GET /services/{id}/ssh-keys |
R | { data: [{ id, type, fingerprint, comment, public_key }] }. Public keys only. |
|
POST /services/{id}/ssh-keys |
M | service-settings | { "public_key": "...", "name": "laptop"? }. 201 with the key. The key is also saved in your account (up to 30). Ids (key_...) are the same for ssh_key_ids. |
DELETE /services/{id}/ssh-keys/{keyId} |
M | service-settings | 204. |
POST /services/{id}/cancel |
M, Idem | cancel | { "at": "period_end" | "now", "confirm": true }. Answers { status: "scheduled" | "requested" | "cancelled", cancel_at, cancel_at_period_end }. |
POST /services/{id}/cancel/withdraw |
M | cancel | Undoes a period_end cancellation. { "cancel_at_period_end": false }. Not possible after an immediate cancellation. |
Power, hostname, SSH key and reinstall actions need the service to be active; otherwise 409 service_not_ready or service_suspended. Hostname, SSH keys and rDNS run synchronously (200, 201, 204); only power, reinstall and reset-password return tasks.
Billing
| Method and path | Scope | Limit | Notes |
|---|---|---|---|
POST /services/{id}/renew |
P, Idem | renew | { "payment": "auto" | "balance" | "card", "cycles": 1? } (body optional). 200 { invoice, service, already_paid }. |
PUT /services/{id}/auto-renew |
B | service-settings | { "mode": "off" | "balance" | "balance_and_card" | "card" }. Answers the service billing. |
GET /invoices?status=&service_id=&limit=&cursor= |
R | status: open, overdue, paid, cancelled, disputed. Invoice: id, number, status, total, created_at, due_at, paid_at, lines (description, type, service_id, quantity, unit_price, total). Line type: plan, storage, ips, recurring, option, discount, fee, other. |
|
GET /invoices/{id} |
R | One invoice. | |
POST /invoices/{id}/pay |
P, Idem | renew | { "payment": ... } (optional). 200 { invoice, already_paid }. Use it for a grouped renewal invoice. |
Only invoices whose services are all VPS or VDS are visible and payable through the API (no top-ups, drafts or node-migration invoices); their id is inv_ plus the invoice id.
Firewall
The firewall of a service depends on the node it runs on, like in the panel. The IP is always the service's primary IP (you never send it). GET /services/{id}/firewall tells you which engine you have; call it before using the other routes.
engine |
What it offers | Routes |
|---|---|---|
shield |
Per-IP features: country block, source allow-list, ASN block, port gates, filters, blacklist | GET /firewall, POST and DELETE /firewall/{feature} |
gsk |
Mitigation profiles and rules, attacks, traffic metrics, cache ports | GET/PUT /firewall/rules, GET /firewall/profiles, GET /firewall/attacks, GET /firewall/metrics, cache-ports routes |
none |
Nothing: capabilities is [] and state is null |
GET /firewall only |
curl $API/services/K3F8QX2P/firewall -H "Authorization: Bearer $SOLUME_KEY"{
"engine": "shield",
"capabilities": ["geoblock", "srcwl", "asn-block", "..."],
"state": {
"ip": "203.0.113.10",
"geoblock": ["CN"],
"geo_policy": "blacklist",
"srcwl": [],
"asn_block": [],
"blacklist": []
}
}shield. capabilities lists the features you can change. state has ip, geoblock (country codes), geo_policy (whitelist or blacklist), srcwl (allowed sources), asn_block and blacklist ({ ip, manual }). Change a feature with POST /firewall/{feature} (add) or DELETE /firewall/{feature} (remove) and a JSON body that depends on the feature; both answer { "ok": true }:
| Feature | Body |
|---|---|
geoblock |
{ "country": "CN" } |
srcwl |
{ "cidr": "198.51.100.0/24" } |
asn-block |
{ "asn": "4837" } |
tcp-allow-perdst, udp-allow-perdst, ports-fivem, ports-rdp, ports-ssh, syn-gate |
{ "port": 22 } |
blacklist |
{ "src_addr": "192.0.2.9" } |
geo-rate |
{ "country": "CN", "proto": "tcp" | "udp", "pps": 1000 }; DELETE takes country and proto |
fivem-cache |
{} |
rdp-filter, ssh-filter, udp-strict |
{ "enabled": true } (a setting: send a new value, DELETE is a 400) |
geoblock-policy |
{ "policy": 0 | 1 } (setting) |
filter-mode |
{ "mode": "filter" | "drop" } (setting) |
An unknown feature is 404 not_found. The exact validation (ports 1 to 65535, and so on) comes from the same rules as the panel.
gsk.
| Route | Notes |
|---|---|
GET /firewall/rules, PUT /firewall/rules |
PUT { "rules": [{ "profile", "source", "port", "protocol": "tcp" | "udp" }] } replaces all rules (up to 200); [] clears them. |
GET /firewall/profiles |
{ profiles: [{ id, name, description }] } |
GET /firewall/attacks?page= |
{ attacks: [{ id, address, date, peak_pps, peak_bps, duration_seconds }] } for this IP. |
GET /firewall/metrics |
{ samples: [{ time, pass_bps, drop_bps, pass_pps, drop_pps }] } |
GET /firewall/cache-ports |
{ cache: [{ type, ports }] }. Types: steam-a2s, raknet, sampp, fivem, minecraft-java, source, teamspeak. |
POST /firewall/cache-ports/{type} |
{ "ports": [1 to 65535] } (1 to 50 ports) answers { ports }. |
DELETE /firewall/cache-ports/{type}/ports |
{ "ports": [...] } removes some ports. DELETE /firewall/cache-ports/{type} clears a type; DELETE /firewall/cache-ports clears everything. |
Errors. Calling a route of the other engine answers 422 feature_not_supported with details.engine. A service on a node with no firewall (or with no IP) answers 409 firewall_unavailable (message "This service has no firewall." for none), without calling any provider. If the provider is not configured or is down you get 503 firewall_unavailable with a generic message (provider text and credentials are never returned); a provider answer that means our own credentials are wrong (401, 403, 404 or 422) is also a 503, and only a provider 400 or 409 becomes a generic 400 invalid_request; a provider throttle is 429 rate_limited. Shield bodies are strict, and a gsk rule profile must exist at the provider (details.valid_profiles lists them). Shield results are always limited to the service IP, and gsk attacks are returned only for that IP. Reads count against service-reads (60 per minute per service, 300 per minute per key) besides the general limit; changes count against service-settings (10 per hour per service).
Billing and lifecycle
Prices and discounts
your_price = list_price x (1 - discount/100) per cycle and per option. The discount of your key applies to purchases and to renewals: a service bought through the API remembers the key that bought it, and every renewal invoice carries a negative Reseller discount line equal to the key's current discount percentage. Changing the percentage affects invoices issued afterwards, never the ones already issued. A service bought on the website has no discount. On a card payment, the Card fee (a percentage of your price) is a separate line, added only when the card is charged and not refunded. billing.price shows the amount of the next renewal.
The billing object
cycle, price (what the next renewal costs you), current_period_end, next_invoice_at, open_invoice_id, due_at, grace_ends_at, suspends_at, terminates_at, auto_renew, cancel_at_period_end and renewable. Dates that do not apply are null. Use them to show your customers when to pay.
How renewals work
- A renewal invoice is generated by a daily run, a number of days before the renewal date (
dunning.diasAvisoPrevio, 3 days by default). Its due date is the renewal date. You getinvoice.created(see Webhooks). - You pay it with
POST /services/{id}/renew(orPOST /invoices/{id}/pay), or automatically, depending on theauto_renewmode. - If the invoice is unpaid when it falls due, the service is suspended for non-payment at that moment. There is no grace period before suspension;
grace_ends_atequalssuspends_at. - While suspended, card retries follow the schedule
dunning.reintentosHoras(24, 72 and 168 hours by default) after a failed charge. Paying the invoice unsuspends the service (service.unsuspended). - A service suspended for
dunning.diasGraciaCancelaciondays (15 by default) is terminated: the server is stopped and its network is cut (service.terminated, statuscancelled). The virtual machine is never deleted automatically.
These numbers are platform settings and can change; the dates for each service are always in its billing object, so read those instead of hard-coding the values.
POST /services/{id}/renew is safe to call more than once: a paid invoice answers 200 with already_paid: true, and asking before the renewal window opens charges nothing and pre-pays nothing (already_paid: true, with the last paid invoice). A cancelled, errored or paused service answers 409 conflict with details.reason.
Auto-renew modes
Services bought by API start with auto_renew: "off". Change it with PUT /services/{id}/auto-renew (needs billing) or set it when ordering.
| Mode | Behaviour |
|---|---|
off |
Nothing is ever charged automatically. You renew with /renew. The invoice, the reminders, the suspension and the termination still follow the schedule above. |
balance |
A pass that runs every 10 minutes pays the pending or overdue renewal invoice from the balance whenever the balance covers the whole total (no partial payments). If it does not, nothing is charged. |
balance_and_card |
Balance when it covers the whole total (same 10-minute pass); otherwise the default card is charged on the card schedule below. |
card |
The default card. The first attempt is made at the earlier of: 2 days after the invoice is generated (comercio.cobroAutomatico.diasAvisoPrevio, a setting) and 24 hours before the due date; right away if less than 12 hours are left. Needs a card and the accepted mandate (409 no_default_card / mandate_not_accepted otherwise). |
If the balance runs low after a payment you get balance.low.
Payments
- Order of payment with
payment: "auto": the account balance first; if it does not cover the total, the default saved card is charged off session through the card processor, using the mandate the customer accepted on the website.payment: "balance"or"card"force one method. A payment is never split. - PayPal and crypto are not charged in the background. They can only top up the balance (
POST /balance/top-upsreturns a hosted checkout URL for the customer), after which you pay with the balance. - Card and mandate: card payments by API (
payment: "card", or"auto"when the balance is short) need a saved default card with the mandate version 2026-11-v2 accepted. Without it the call answers402 requires_actionwithdetails.reasonno_cardorno_mandatebefore anything is created. See Requirements before you buy by API. - When the card needs the customer (3-D Secure) the call answers
402 requires_action(details.reason: "authentication_required"); if it is declined,402 card_declined. Nothing is created or provisioned, the reserved spend is released, and the sameIdempotency-Keycan be retried after fixing the cause.402 insufficient_balancemeans the balance does not cover the total (details.required_cents). - What has to be done on the website: save the card, accept the automatic-charge mandate, top up the balance by PayPal or crypto if you want to, and have staff grant
purchase,billing, the IPs and the caps. The panel shows your card status under Card for API purchases. - A
500 internal_errorafter a card charge on a renewal or payment means the outcome could not be confirmed: the spend stays counted, the invoice is not cancelled, and no other payment of it is accepted for 2 hours. ReadGET /invoices/{id}. - If an order is paid but cannot be delivered, the amount is returned to the balance or the card and you get
409 out_of_stockwithdetails.order_id.
Webhooks and events
Register an HTTPS endpoint under API and webhooks in the panel (or read the webhooks article). Every delivery is a POST signed with HMAC-SHA256 and retried automatically. Each webhook chooses its events, or * for all.
Delivery format
{
"id": "cm0entrega0ejemplo",
"evento": "invoice.paid",
"creado": "2026-12-02T06:01:12.000Z",
"datos": {
"invoice_id": "inv_cm3z4c9s00007renew02",
"number": "INV-2026-000412",
"status": "paid",
"total": { "amount_cents": 720, "currency": "USD" },
"due_at": "2026-12-05T06:00:00.000Z",
"paid_at": "2026-12-02T06:01:12.000Z",
"service_ids": ["K3F8QX2P"]
}
}The envelope keys (id, evento, creado, datos) are the platform's webhook envelope and stay as they are; the content of datos of reseller events is in English. Headers: Solume-Signature: t=<unix seconds>,v1=<hex>, Solume-Event, Solume-Delivery (same as id) and User-Agent: SolumeCore-Webhooks/1.0.
Verify the signature: v1 is the hex HMAC-SHA256 of <t>.<raw body> keyed with your whsec_ signing secret (the whole string). Use the raw bytes before parsing, compare in constant time and reject a t more than five minutes from your clock.
import crypto from 'node:crypto';
export function isValidSignature(header, rawBody, secret, tolerance = 300) {
const parts = Object.fromEntries((header ?? '').split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!Number.isInteger(t) || !parts.v1 || Math.abs(Date.now() / 1000 - t) > tolerance)
return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest();
const received = Buffer.from(parts.v1, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}Retries: any 2xx is delivered; answer within 10 seconds. Network errors, timeouts, 408, 429 and 5xx are retried up to 8 attempts with growing waits (from about 30 seconds up to 4 hours); redirects and other 4xx fail without retries. After 20 consecutive failed deliveries the webhook is paused. Retries reuse the same id, so deduplicate on it. Order is not guaranteed; use creado.
Events
These reseller events exist and are emitted today:
| Event | When | datos |
|---|---|---|
order.paid |
An order was paid (right after POST /orders) |
order_id, status, service_id, total, payment_method |
order.failed |
A paid order could not be delivered and was refunded | order_id, status |
invoice.created |
A renewal invoice was generated | invoice_id, number, status, total, due_at, paid_at, service_ids |
invoice.paid |
An invoice was paid, by any path | same as invoice.created |
invoice.overdue |
An invoice passed its due date unpaid | same as invoice.created |
service.renewed |
A renewal was paid | service_id, invoice_id, current_period_end |
service.suspended |
A service was suspended | service_id, reason (non_payment or other) |
service.unsuspended |
A suspended service was reactivated by a payment | service_id, reason (payment_received) |
service.terminated |
A suspended service reached the end of the grace days, or a period-end cancellation took effect | service_id, reason (non_payment or cancelled_at_period_end) |
balance.low |
A balance payment left less than that payment | balance |
service.provisioned |
Your VPS/VDS bought through the API was delivered and is now active |
service_id, order_id, status: "active" |
service.provision_failed |
Provisioning ran out of attempts and the service went to error (a later service.provisioned can still follow if our retries succeed) |
service_id, order_id, status: "error", error: { code: "provision_failed", message } (a fixed generic message) |
task.succeeded |
A task ended well (power, reset-password, reinstall); GET /tasks/{id} says the same |
task_id, type, status: "succeeded", service_id |
task.failed |
A task failed, or was given up as timeout (10 minutes for power and reset-password, 3 hours for a reinstall), even if nobody polled it |
task_id, type, status: "failed", service_id, error: { code } |
service.cancel_scheduled |
POST /services/{id}/cancel with at: "period_end" (or the same request from the panel) |
service_id, cancel_at (null when staff processes the cancellation because there is no automatic cycle) |
They are only sent for accounts with an active API key, once per object (deduplicated for 24 hours), and never carry credentials, node data, hostnames or IP addresses. service.provisioned and service.provision_failed are only sent for servers bought through the API, not for web purchases. A task produces one closing event: task.succeeded or task.failed, never both. service.cancel_scheduled is sent once per request: repeating the request, at: "now" and cancel/withdraw emit nothing. Your webhook can also subscribe to the other events of your account (power, backups, billing notices... see the webhooks article).
You do not have to wait for events: polling GET /services/{id} (until status is active) and GET /tasks/{id} always works and is the safe fallback if a delivery is missed. The sandbox emits the same events as test.<event>.
GET /events: pull instead of push
GET /v1/events?since=&type=&limit=&cursor= returns the events of the last 30 days, oldest first, built from the deliveries of your webhooks:
{
"data": [
{
"id": "evt_cm3z0a1b20001xyz",
"event": "invoice.paid",
"created_at": "2026-12-02T06:01:12.000Z",
"service": null,
"actor": null,
"data": {
"invoice_id": "inv_cm3z4c9s00007renew02",
"status": "paid",
"service_ids": ["K3F8QX2P"]
}
}
],
"next_cursor": null
}since is an ISO 8601 date and type filters one event. An account with no webhook registered has no events yet: register a webhook (even one that points at a sink) to feed this endpoint. Use id to deduplicate.
Sandbox
The sandbox is a free test copy at https://api.solumecore.com/sandbox/v1, with its own sk_test_ keys. Highlights (the sandbox guide has every detail):
- Keys: you create up to 3 yourself in the panel (API and webhooks, API keys, Sandbox keys); no approval, no IPs, all permissions.
- Real: the catalog (plans, locations, templates, options). Virtual: a 1000.00 USD balance, a card (none in a new account, like a new customer: add it with
POST /sandbox/v1/payment-method/setup, which also accepts the mandate), services, orders, invoices, tasks and fake one-time credentials. Nothing is charged or provisioned. - Lifecycle: a new service becomes
activeafter about 10 seconds (X-Sandbox-Provision-Seconds: 0to120). - Scenarios: send
X-Sandbox-Scenariowithcard_declined,requires_action,insufficient_balance,out_of_stock,provision_failed,task_failed,sloworrate_limitedto test your error handling.X-Sandbox-Firewall-Engine: shield|gsk|nonepicks the firewall engine of a new service. Magic hostnamesfail-card,fail-stock,fail-provisiondo the same on an order. - Time travel:
POST /sandbox/v1/time/advance {"days": 30}runs the renewal, suspension and termination rules;POST /sandbox/v1/resetwipes the account (and its card).time/advancehas its own limit (30 per hour per key). A sandbox account is kept for 7 days after its last use. - Webhooks: delivered to your registered endpoints but they can never be mistaken for production ones: the event name is prefixed (
test.order.paid; a webhook subscribed toorder.paid, totest.order.paidor to*receives it), theSolume-Livemode: falseheader is sent, the payload has"livemode": false, and the signature uses a derived secret, so it does not verify with your realwhsec_secret. Sandbox deliveries are never retried. One operation delivers at most 50 events plus atest.sandbox.events_summarizedsummary. - Not simulated: IP allowlist, staff approval, real card processing, real 3-D Secure, PayPal and crypto, real provisioning time, anti-fraud checks and real stock.
- Every response has
X-Sandbox: true. A live key on the sandbox, or the reverse, answers403 wrong_environment.
Go-live checklist
- Switch the base URL to
https://api.solumecore.com/v1and the key tosk_live_. - Ask support for your IPs, the
purchaseandbillingpermissions and your spending caps. - Save the card and accept the automatic-charge mandate in the panel; check
GET /payment-method. - Check
GET /me: scopes, caps, discount, card fee and rate limits. - Test your handling of
402,403,409,429and5xx, and of repeatedIdempotency-Keys. - Register a webhook endpoint and verify signatures.
- Start with one small real order and renewal before opening to customers.
Best practices
- Idempotency always. Derive the
Idempotency-Keyfrom your own order id, send it on every retry, and treatIdempotent-Replay: trueas success. - Retry with backoff. Retry
429(honourRetry-After),503and5xxwith exponential backoff and jitter. Do not retry4xxother than408/409 request_in_progress/429unchanged. - Log
X-Request-Idof every call (anderror.request_id) and quote it in tickets. - Never cache or log credentials. Move the password to your secret store on the first read. If you lose it,
reset-password. - Handle
402.requires_actionandcard_declinedneed the customer; fall back topayment: "balance"and keep a funded balance. - Watch your caps and limits. Use
would_exceed_capin quotes,GET /me(usage) and theX-RateLimit-*headers to slow down before you hit429. - Prefer webhooks, poll with care. Use webhooks or
GET /eventsfor billing and lifecycle, and pollGET /services/{id}every 5 to 30 seconds only while waiting foractive. UseGET /services/{id}/statusfor cheap live checks. - Paginate. Follow
next_cursoruntil it isnull; do not build cursors yourself. - Reconcile. Store
metadatawith your own ids when you order, and periodically compareGET /servicesandGET /invoiceswith your records. - Make destructive calls explicit. Reinstall and cancel need
confirm: true; ask for confirmation in your own UI as well.
Security
- IP allowlist. Keep the list of registered IPs minimal and ask support to remove addresses you no longer use. Call from fixed egress IPs.
- Least privilege. Ask for only the scopes you need: for example a monitoring job needs
read; the code that buys needspurchaseand nothing more. Use several keys (one per system) so you can revoke one without stopping the others. - Caps are a safety net. Keep per-order and daily caps near what you really spend.
- Keys and logs. An account can have up to 10 active live keys. Deleting a live key is a soft delete (it stops working at once and its history is kept) and asks you to confirm your identity. Every call made with a key, including rejected ones, is logged and kept for 90 days.
- Rotate keys. Create the new key, deploy it, then revoke the old one in the panel. Creating or revoking keys asks you to confirm your identity and sends notifications. Rotate immediately if a key may have leaked.
- Keep secrets out of code and logs. Use environment variables or a secret manager; never put a key in a browser, a mobile app or a public repository. Never log request headers or credentials.
- Verify webhooks. Check the signature with the raw body and a time tolerance, and deduplicate by
id. - Do not share keys between customers, send
Authorizationto third parties, build a public proxy that exposes the API without your own authentication and limits, or disable certificate verification. - Staff are alerted about repeated
429andip_not_allowed; unusual spend also hits your caps.
Changelog
v1.1 (2026-11)
Changes after the security audit.
GET /services/{id}/credentialsnow needs themanagepermission (it wasread);HEADis not served.- New
service-readsrate-limit bucket: 60 per minute per service and 300 per minute per key forGET /services/{id},/status,/metrics,GET /firewalland the gsk reads. POST /services/{id}/cancelwithat: "now"only works on anactiveservice (409 service_suspendedorservice_not_readyotherwise).- Purchases and payments by API with a card need the automatic-charge mandate version 2026-11-v2: without it,
402 requires_actionwithdetails.reason: "no_mandate".auto_renewwith a card checks the card and the mandate (409 conflict). - Orders return
invoice_id(inv_...); a reusedIdempotency-Keywith another body is422also after an order was recovered. - Firewall: a provider failure is
503 firewall_unavailable; reads are never an empty list on failure. - A platform dependency that does not answer gives
503withRetry-Afteron/v1. - Sandbox: webhooks travel as
test.<event>withSolume-Livemode: falseand a derived signing secret, without retries;time/advancehas its own limit and delivers at most 50 events plus a summary; accounts are kept 7 days. - Up to 3 sandbox keys and 10 live keys per account; usage logs are kept 90 days.
v1.0 (2026-11)
First complete release of the reseller API, replacing the previous client API (/v1/services with power and firewall only), which had no active users.
- Catalog (
/catalog/*), quotes and orders for VPS and VDS, with per-key discount and card fee. - Services: status, metrics, one-time credentials, power, reinstall, reset password, hostname, rDNS, SSH keys, network, cancel and withdraw, and asynchronous tasks.
- Firewall with three engines (
shield,gsk,none). - Billing: renewals, auto-renew modes, invoices, balance, top-ups and payment-method info.
- Permissions (
read,manage,purchase,billing), spending caps, IP allowlist, idempotency, per-bucket rate limits and a single error format. - Webhooks and events for orders, invoices and the service lifecycle, plus
GET /events. - Sandbox with virtual account, scenarios, time travel and
livemode: falsewebhooks. - OpenAPI files for production and the sandbox.
Related articles
- Reseller API guide
- Reseller API sandbox
- Account API keys
- Webhooks and account events
- Which SolumeCore API to use
FAQ
Does using the SolumeCore reseller API cost anything?
No. There is no fee for the API, calls or keys. You pay the normal price of each VPS or VDS you buy or renew, minus the discount agreed for your key and plus the card fee agreed for your key when a card is charged. Both default to 0%.
Can I resell game servers or web hosting through the API?
No. The reseller API covers VPS and VDS servers only. A plan of another type is rejected with the error product_not_resellable.
Why does my API key answer ip_not_allowed?
A live key only works from the IP addresses that Solume staff registered for it. Open a ticket from the panel with the public IPs your code calls from. Sandbox keys do not need IP addresses.
How do I test without spending money?
Use the sandbox at https://api.solumecore.com/sandbox/v1 with an sk_test_ key created in the client panel. It has the same endpoints and errors as production on a virtual account with a 1000.00 USD virtual balance.
How is a purchase paid when I use the API?
With payment set to auto, the account balance is used first and, if it does not cover the total, the default saved card is charged off session with the automatic-charge mandate accepted on the website. PayPal and crypto can only top up the balance.