Sign inCreate server
API

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.

52 min read#reseller api#vps api#vds api#rest api#openapi#webhooks#api rate limits

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_case field names. Money is always integer cents.

Getting started

  1. 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.

  2. 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 (purchase permission) 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 (billing permission);
    • 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.

  3. 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.

  4. 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.

  5. Make your first call.

bash
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:

text
Authorization: Bearer sk_live_xxxxxxxx
  • Live keys (sk_live_) work on /v1 from registered IPs only. Sandbox keys (sk_test_) work on /sandbox/v1 only, 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 answers 403 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_found as 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: true and 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 a 5xx, 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.

json
{
  "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.

bash
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

bash
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:

json
{
  "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

bash
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:

bash
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" }]
}'
json
{
  "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

bash
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:

json
{
  "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

bash
solume $API/services/K3F8QX2P

Poll 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

bash
solume $API/services/K3F8QX2P/credentials
json
{ "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

bash
# 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

bash
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

bash
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

bash
solume -X POST $API/services/K3F8QX2P/reset-password -H "Idempotency-Key: reset-K3F8QX2P-001"
# when the task succeeds:
solume $API/services/K3F8QX2P/credentials

11. Firewall

bash
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

bash
# 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/withdraw

at: "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.

js
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);
python
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
bash
curl $API/services/K3F8QX2P/firewall -H "Authorization: Bearer $SOLUME_KEY"
json
{
  "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

  1. 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 get invoice.created (see Webhooks).
  2. You pay it with POST /services/{id}/renew (or POST /invoices/{id}/pay), or automatically, depending on the auto_renew mode.
  3. 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_at equals suspends_at.
  4. 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).
  5. A service suspended for dunning.diasGraciaCancelacion days (15 by default) is terminated: the server is stopped and its network is cut (service.terminated, status cancelled). 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-ups returns 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 answers 402 requires_action with details.reason no_card or no_mandate before 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 same Idempotency-Key can be retried after fixing the cause. 402 insufficient_balance means 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_error after 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. Read GET /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_stock with details.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

json
{
  "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.

js
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:

json
{
  "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 active after about 10 seconds (X-Sandbox-Provision-Seconds: 0 to 120).
  • Scenarios: send X-Sandbox-Scenario with card_declined, requires_action, insufficient_balance, out_of_stock, provision_failed, task_failed, slow or rate_limited to test your error handling. X-Sandbox-Firewall-Engine: shield|gsk|none picks the firewall engine of a new service. Magic hostnames fail-card, fail-stock, fail-provision do the same on an order.
  • Time travel: POST /sandbox/v1/time/advance {"days": 30} runs the renewal, suspension and termination rules; POST /sandbox/v1/reset wipes the account (and its card). time/advance has 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 to order.paid, to test.order.paid or to * receives it), the Solume-Livemode: false header is sent, the payload has "livemode": false, and the signature uses a derived secret, so it does not verify with your real whsec_ secret. Sandbox deliveries are never retried. One operation delivers at most 50 events plus a test.sandbox.events_summarized summary.
  • 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, answers 403 wrong_environment.

Go-live checklist

  1. Switch the base URL to https://api.solumecore.com/v1 and the key to sk_live_.
  2. Ask support for your IPs, the purchase and billing permissions and your spending caps.
  3. Save the card and accept the automatic-charge mandate in the panel; check GET /payment-method.
  4. Check GET /me: scopes, caps, discount, card fee and rate limits.
  5. Test your handling of 402, 403, 409, 429 and 5xx, and of repeated Idempotency-Keys.
  6. Register a webhook endpoint and verify signatures.
  7. Start with one small real order and renewal before opening to customers.

Best practices

  • Idempotency always. Derive the Idempotency-Key from your own order id, send it on every retry, and treat Idempotent-Replay: true as success.
  • Retry with backoff. Retry 429 (honour Retry-After), 503 and 5xx with exponential backoff and jitter. Do not retry 4xx other than 408/409 request_in_progress/429 unchanged.
  • Log X-Request-Id of every call (and error.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_action and card_declined need the customer; fall back to payment: "balance" and keep a funded balance.
  • Watch your caps and limits. Use would_exceed_cap in quotes, GET /me (usage) and the X-RateLimit-* headers to slow down before you hit 429.
  • Prefer webhooks, poll with care. Use webhooks or GET /events for billing and lifecycle, and poll GET /services/{id} every 5 to 30 seconds only while waiting for active. Use GET /services/{id}/status for cheap live checks.
  • Paginate. Follow next_cursor until it is null; do not build cursors yourself.
  • Reconcile. Store metadata with your own ids when you order, and periodically compare GET /services and GET /invoices with 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 needs purchase and 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 Authorization to 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 429 and ip_not_allowed; unusual spend also hits your caps.

Changelog

v1.1 (2026-11)

Changes after the security audit.

  • GET /services/{id}/credentials now needs the manage permission (it was read); HEAD is not served.
  • New service-reads rate-limit bucket: 60 per minute per service and 300 per minute per key for GET /services/{id}, /status, /metrics, GET /firewall and the gsk reads.
  • POST /services/{id}/cancel with at: "now" only works on an active service (409 service_suspended or service_not_ready otherwise).
  • Purchases and payments by API with a card need the automatic-charge mandate version 2026-11-v2: without it, 402 requires_action with details.reason: "no_mandate". auto_renew with a card checks the card and the mandate (409 conflict).
  • Orders return invoice_id (inv_...); a reused Idempotency-Key with another body is 422 also 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 503 with Retry-After on /v1.
  • Sandbox: webhooks travel as test.<event> with Solume-Livemode: false and a derived signing secret, without retries; time/advance has 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: false webhooks.
  • OpenAPI files for production and the sandbox.

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.

Was this article helpful?
SolumeCore Reseller API: Guide, Reference and Examples