Sign inCreate server
API

Reseller API Sandbox: Test Your Integration for Free

Test your SolumeCore API integration in a free environment: sk_test_ keys, virtual balance, error scenarios, time travel and webhooks.

30 min read#api sandbox#reseller api#test api keys#sk_test#time travel#api scenarios#vps api

The sandbox is a test copy of the reseller API. You build and test your whole integration against it (orders, renewals, suspensions, webhooks, error handling) without spending money and without touching a real server.

This guide covers what is specific to the sandbox. Endpoints, request bodies and response shapes are the ones of production: see the full guide and the OpenAPI file.

Sandbox Production
Base URL https://api.solumecore.com/sandbox/v1 https://api.solumecore.com/v1
Key prefix sk_test_ sk_live_
Money Virtual Real
Servers Simulated records Real VPS and VDS

1. What it is and what it is for

  • Same API. The sandbox exposes the same paths, request bodies, errors and headers as /v1. Going to production means changing the base URL and the key, nothing else.
  • Private virtual account. Every sandbox key has its own account: a virtual balance, a virtual card, and its own services, orders, invoices and tasks. Two keys never see each other's data.
  • Sandbox-only tools. Headers to force failures, POST /time/advance to jump forward in time, POST /reset to start over, and POST /payment-method/setup / DELETE /payment-method to add or remove the virtual card.
  • No side effects. A sandbox request never reads or writes a real service, order, invoice, balance, card or payment object.

First request:

bash
export SOLUME_KEY=sk_test_xxxxxxxx

curl https://api.solumecore.com/sandbox/v1/balance \
  -H "Authorization: Bearer $SOLUME_KEY"

It returns the virtual balance. A new account starts with 1000.00 USD.

2. Sandbox keys (sk_test_)

Create them yourself in the client panel:

  1. Go to API and webhooks and open the API keys tab.
  2. In the Sandbox keys section, click New sandbox key and give it a name (for example CI tests). Like a live key, it asks you to confirm your identity again, and it cannot be created from a support session.
  3. Copy the secret (sk_test_...). It is shown once. Only a fingerprint is stored, so a lost key cannot be recovered: revoke it and create another.
Property Sandbox key
Approval None. It is created instantly.
IP allowlist None. It works from any IP.
Active keys per account Up to 3 sandbox keys (and, separately, up to 10 live keys)
Permissions All: read, manage, purchase, billing
Virtual spending caps 1000.00 USD per order, 3000.00 USD per rolling 24 hours, 10 orders per rolling hour
Discount and card fee 0% and 0%

The caps exist so you can test spending_cap_exceeded. GET /sandbox/v1/me shows the caps, your usage and every rate-limit tier. Revoking or deleting a key also deletes its virtual account.

Keys and environments never mix:

bash
# A sandbox key on production
curl https://api.solumecore.com/v1/balance -H "Authorization: Bearer sk_test_xxxxxxxx"
# 403
{ "error": { "code": "wrong_environment", "message": "This is a sandbox API key (sk_test_...). Use https://api.solumecore.com/sandbox/v1 with it, or call /v1 with a live key.", "request_id": "req_..." } }

A live key (sk_live_) on /sandbox/v1 also answers 403 wrong_environment, and it counts as a failed authentication (see section 8).

3. What is real and what is virtual

Item In the sandbox
Catalog (plans, locations, OS templates, options, prices) Real, read-only. Quotes use real prices and your key's discount.
Balance Virtual. Starts at 1000.00 USD.
Card Virtual: a Visa ending in 4242 that you add yourself; a new account has none (see below).
Services, orders, invoices, tasks Virtual records in your private account.
Credentials Fake, random, readable once.
IP addresses Documentation ranges (192.0.2.x, 198.51.100.x, 203.0.113.x, 2001:db8::/32). They are not routable.
Taxes None: tax is always 0.00.
Payment, provisioning Nothing is charged and nothing is provisioned. There is no machine to connect to.

Card and mandate: a new account has none, like a new customer. In production a card payment needs a saved default card with the automatic-charge mandate version 2026-11-v2 accepted, which the owner sets up on the website. The sandbox applies the same rules to a virtual card:

  • GET /payment-method answers { "payment_method": null } and a quote lists only balance in payable_with.
  • payment: "card" answers 402 requires_action with details.reason no_card (no card) or no_mandate (card without the mandate). payment: "auto" pays with the balance when it covers the total; otherwise it answers 402 insufficient_balance with details.card set to the same reason.
  • auto_renew: "card" or "balance_and_card" (on an order or with PUT /services/:id/auto-renew) answers 409 conflict with details.reason no_default_card or mandate_not_accepted.
  • The automatic card charge during POST /time/advance only uses a card with the mandate accepted; without it the invoice goes overdue.

There is no website in the sandbox, so two endpoints that exist only in /sandbox/v1 do that part (both need the billing permission):

bash
# Add the virtual Visa 4242 and accept the mandate (the body is optional; accept_mandate defaults to true)
curl -X POST https://api.solumecore.com/sandbox/v1/payment-method/setup \
  -H "Authorization: Bearer $SOLUME_KEY" -H "Content-Type: application/json" \
  -d '{ "accept_mandate": true }'

# Keep the card but not the mandate, to test no_mandate / mandate_not_accepted
#   -d '{ "accept_mandate": false }'

# Remove the card and the mandate
curl -X DELETE https://api.solumecore.com/sandbox/v1/payment-method \
  -H "Authorization: Bearer $SOLUME_KEY"

Both answer like GET /payment-method ({ "payment_method": { "brand": "visa", "last4": "4242", "mandate_accepted": true, "chargeable_off_session": true, ... } }, or null after the delete). Calling setup again only updates the mandate of the same card, and POST /reset removes the card. With a card and the mandate, card payments succeed unless you send the card_declined or requires_action scenario (section 5), which take precedence over a missing card; the 3-D Secure error carries details.reason: "authentication_required" as in production. Check the real mandate with GET /payment-method on your live key before going live (section 10).

Because the catalog is real, a plan that is sold out in production shows available: false in the sandbox too, but it does not block a sandbox order (use the out_of_stock scenario to test that).

To add virtual money, call POST /sandbox/v1/balance/top-ups (method: card, paypal or crypto). It succeeds instantly and the balance is credited. The returned checkout_url is a placeholder: there is nothing to pay.

4. Simulated lifecycle

An order charges the virtual balance or card, then creates a service in provisioning.

Step Time
Order accepted, service provisioning Immediate (201)
Service becomes active and service.provisioned is emitted About 10 seconds. Change it per order with X-Sandbox-Provision-Seconds (0 to 120).
Power action (start, stop, restart, shutdown) queued for 1 s, done in 4 to 5 s
Reset password Done in about 4 s
Reinstall Done in about 9 s
Reverse DNS (PTR) request pending, then applied one virtual minute later
Cancel now Immediate: 200 with status: cancelled (it is not a task)

Power, reinstall and reset password return 202 with a task. Poll GET /sandbox/v1/tasks/:id or wait for the task.succeeded or task.failed webhook.

Credentials work like production: the initial password of a new service (and the one produced by a reset password or a reinstall) is random, valid for 24 hours and readable once at GET /services/:id/credentials. A second read answers 410 credentials_consumed; after 24 virtual hours it answers 404 credentials_not_available. Request a new one with POST /services/:id/reset-password. As in production, that read needs the manage permission (403 insufficient_scope otherwise, without using up the read) and there is no HEAD route (it answers 404).

bash
curl -X POST https://api.solumecore.com/sandbox/v1/orders \
  -H "Authorization: Bearer $SOLUME_KEY" \
  -H "Idempotency-Key: order-0001" \
  -H "X-Sandbox-Provision-Seconds: 3" \
  -H "Content-Type: application/json" \
  -d '{
    "plan_id": "<plan_id>",
    "location_id": "<location_id>",
    "billing_cycle": "monthly",
    "os_template_id": "<os_template_id>",
    "hostname": "web-1",
    "payment": "auto",
    "auto_renew": "off"
  }'

Take the ids from GET /catalog/plans, /catalog/locations and /catalog/os-templates. The sandbox only sells VPS and VDS, like production (product_not_resellable otherwise).

Time here is virtual time: the account clock is the real clock plus whatever you advanced (section 6).

5. Scenario control

Send these headers to make the sandbox fail on purpose. They are ignored by /v1. An unknown value answers 400 invalid_request with the allowed values in details.

X-Sandbox-Scenario

The scenario applies only to the request that carries it.

Value Where it applies What happens
card_declined Every payment that would use the card: POST /orders, /services/:id/renew, /invoices/:id/pay with payment card or auto; POST /balance/top-ups with method: card; the automatic card charge of POST /time/advance 402 card_declined. Nothing is created. payment: balance is not affected.
requires_action Same routes as card_declined 402 requires_action (the card needs 3-D Secure). Nothing is created.
insufficient_balance Every payment that would use the balance (balance or auto) and the automatic balance charge of POST /time/advance 402 insufficient_balance even if the balance covers it, with required_cents in details. payment: card is not affected.
out_of_stock POST /orders (a quote carries no availability, so POST /quotes is not affected) 409 out_of_stock. Nothing is created.
provision_failed POST /orders The order is accepted (201). When provisioning ends the service goes to error, the order to failed, the amount is refunded to the virtual balance, and service.provision_failed and order.failed are emitted.
task_failed Requests that create a task: power, reinstall, reset password 202 as usual, then the task ends failed (error.code: task_failed) and task.failed is emitted. The service is left as it was.
slow Any request Adds 3 seconds of latency. The request then runs normally.
rate_limited Any request Immediate 429 rate_limited with Retry-After: 30. Nothing else runs.
bash
curl -X POST https://api.solumecore.com/sandbox/v1/orders \
  -H "Authorization: Bearer $SOLUME_KEY" \
  -H "Idempotency-Key: order-0002" \
  -H "X-Sandbox-Scenario: card_declined" \
  -H "Content-Type: application/json" \
  -d '{ "plan_id": "<plan_id>", "location_id": "<location_id>", "billing_cycle": "monthly", "os_template_id": "<os_template_id>", "hostname": "web-1", "payment": "card" }'
json
{
  "error": {
    "code": "card_declined",
    "message": "The card was declined. Nothing was created.",
    "request_id": "req_...",
    "details": { "reason": "declined" }
  }
}

The other scenario errors have the same shape:

Code Status details
requires_action 402 { "reason": "authentication_required" }
insufficient_balance 402 { "required_cents": 500 }
out_of_stock 409 none

A missing card or mandate is not a scenario: it is the real state of your virtual account (section 3) and it answers 402 requires_action with details.reason no_card or no_mandate. The scenarios take precedence over it. Production can also answer card_declined with details.reason insufficient_funds or stripe_disabled, which the sandbox never produces.

The rate_limited scenario answers:

http
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Remaining: 0
X-Sandbox: true
json
{
  "error": {
    "code": "rate_limited",
    "message": "Simulated rate limit (X-Sandbox-Scenario: rate_limited).",
    "request_id": "req_...",
    "details": { "retry_after": 30 }
  }
}

Magic hostnames

On POST /orders, a hostname that starts with one of these words has the same effect as the scenario. The match is case-insensitive and may be followed by - and anything (fail-card-2). If the header and the hostname disagree, the header wins (slow and rate_limited never touch the payment, so the hostname still applies).

Hostname Same as
fail-card card_declined
fail-stock out_of_stock
fail-provision provision_failed

Other request headers

Header Values Effect
X-Sandbox-Provision-Seconds Integer 0 to 120 (default 10) How long the services created by that request stay in provisioning. Any other value is 400 invalid_request.
X-Sandbox-Firewall-Engine shield (default), gsk, none Firewall engine of the services created by that order, like the node of a real service. Calling a feature of the other engine answers 422 feature_not_supported; with none, 409 firewall_unavailable.

Response headers

Header Meaning
X-Sandbox: true On every sandbox response, errors included.
X-Sandbox-Virtual-Time The account's virtual clock (ISO-8601), on responses that read the account.
X-Request-Id, X-RateLimit-*, Retry-After, Idempotent-Replay Same as production.

6. Time travel and reset

Renewals, overdue invoices, suspension and termination take weeks in production. In the sandbox you jump to them.

bash
curl -X POST https://api.solumecore.com/sandbox/v1/time/advance \
  -H "Authorization: Bearer $SOLUME_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "days": 27 }'
json
{
  "advanced_days": 27,
  "virtual_now": "2026-12-02T12:00:11.000Z",
  "events_generated": 1
}

days is an integer from 1 to 400 (anything else is 400 invalid_request). The clock moves one day at a time and the lifecycle runs on each step, like the daily billing job. It needs no Idempotency-Key. You can call it 30 times per hour per key; beyond that it answers 429 rate_limited with Retry-After (in seconds).

The lifecycle uses the same rules as production, read from the platform billing settings. These settings are configurable by SolumeCore staff, so the numbers below are the defaults and can change:

Setting Meaning Default
dunning.diasAvisoPrevio Days before the renewal date when the renewal invoice is created 3
dunning.reintentosHoras Hours to wait before each automatic retry after a failed charge [24, 72, 168]
dunning.diasGraciaCancelacion Days a service stays suspended before it is terminated 15

Read the real dates of a service from its billing object (GET /services/:id) instead of hard-coding the defaults.

What happens to a monthly service (defaults, auto_renew: off) bought on day 0:

When Events Service status
Day 0 invoice.created, invoice.paid, order.paid for the order; then service.provisioned provisioning, then active
Renewal date minus 3 days invoice.created (renewal invoice) active
Renewal date, invoice unpaid invoice.overdue, service.suspended (reason: non_payment) suspended, powered off
Suspension plus 15 days service.terminated (reason: non_payment); the open invoice is cancelled cancelled

The reason values are the ones production uses: non_payment (unpaid), cancelled_at_period_end (the period ended after you scheduled the cancellation) and other.

Variations:

  • auto_renew: balance, enough balance: when the renewal invoice is created it is charged at once, and you get invoice.created, invoice.paid and service.renewed. card charges the virtual card; balance_and_card tries the balance first, then the card. The two card modes need the card and the mandate (section 3), and if you remove the card later the charge cannot be made and the invoice goes overdue.
  • A charge fails because of a scenario (card_declined, requires_action or insufficient_balance sent on the POST /time/advance request, in a mode that would use that method): the invoice becomes overdue at once and invoice.overdue is emitted once, with failed_attempts in its data. The next attempt is scheduled with the next value of reintentosHoras. The service is suspended on the renewal date, or earlier if the list of retries runs out.
  • Not enough balance, no scenario (auto_renew: balance with a balance that does not cover the invoice): nothing is charged and the invoice stays open until the renewal date, so you still have time to pay by hand. Then it becomes overdue and the service is suspended, as with auto_renew: off.
  • You pay while suspended (/renew or /invoices/:id/pay): invoice.paid, service.renewed, service.unsuspended.
  • You renew too early (POST /services/:id/renew before the renewal window opens): nothing is charged. It answers 200 with already_paid: true and the last paid invoice (or null), as in production. Inside the window it creates the renewal invoice if needed and pays it.
  • POST /services/:id/cancel with at: period_end: service.cancel_scheduled; no renewal invoice; at the end of the period the service is terminated (service.terminated with reason: cancelled_at_period_end). POST /services/:id/cancel/withdraw cancels the schedule and counts in the cancel bucket (section 8).
  • Credentials you never read expire after 24 virtual hours.
  • Caps use the virtual clock. Advancing one day empties the daily and hourly spending windows. Rate limits use real time and are not affected.

Work that comes due is processed on the next request to the account, or by a background timer when it is due within about ten minutes, so webhooks arrive even if you make no requests. A scenario header applies to the request that carries it: when you test failed renewals, send it on POST /time/advance itself.

Reset

bash
curl -X POST https://api.solumecore.com/sandbox/v1/reset \
  -H "Authorization: Bearer $SOLUME_KEY"
json
{
  "reset": true,
  "balance": { "amount_cents": 100000, "currency": "USD" },
  "virtual_now": "2026-11-05T12:00:00.000Z"
}

It wipes the virtual account: services, orders, invoices, tasks, events, SSH keys, the virtual card and mandate, idempotency keys and the per-service and per-bucket rate-limit counters (orders, power, reinstall...). The balance goes back to 1000.00 USD and the clock back to real time. Your key and your webhooks are kept. The general, time/advance and reset counters are not cleared. You can reset 20 times per hour; beyond that it answers 429 rate_limited with Retry-After (in seconds).

7. Webhooks and events

The sandbox uses the webhooks you already registered in the client panel (API and webhooks, Webhooks tab). There is nothing sandbox-specific to configure, only make sure the webhook is active and subscribed to the event names you need: the real names (the panel offers those and *, all events) and, if you want only sandbox deliveries, the test.<event> names: in the webhook editor they are the Sandbox (test events only) group.

Sandbox deliveries can never be mistaken for production ones:

  • Event name. Every event travels as test.<event> (test.order.paid). A webhook subscribed to order.paid, to test.order.paid or to * receives it. A webhook subscribed only to test.<event> names receives sandbox deliveries and never a production one. The name is in the envelope (evento) and in the Solume-Event header.
  • Headers. Solume-Livemode: false (production sends true), plus Solume-Signature and Solume-Delivery as usual.
  • Payload. "livemode": false inside datos.
  • Signature with a derived secret. The signature is built like production (Solume-Signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of <t>.<raw body>), but keyed with a secret derived from your webhook secret, so it does not verify with your real whsec_ secret. This keeps a sandbox key (which is created without staff approval) from forging a production event. Derive it like this and use it as the secret in your usual verification:
js
import crypto from 'node:crypto';

// whsec_... is the signing secret of the webhook, the whole string.
export const sandboxSecret = (whsec) =>
  crypto.createHmac('sha256', whsec).update('solume-sandbox-webhook').digest('hex');
  • No retries. A sandbox delivery is tried once. It never counts towards, nor resets, the consecutive failures that pause a real webhook.
  • At most 50 events per operation. If one request or one POST /time/advance generates more, only the last 50 are delivered, followed by one summary event, test.sandbox.events_summarized. It reaches only webhooks subscribed to all events (*) and it is not stored in GET /events.
  • Same webhook, both environments. One webhook receives live and sandbox events. Check Solume-Livemode or livemode to keep test events away from your production logic.
  • Contents. Ids and statuses, never credentials.

Envelope and headers of a sandbox delivery:

http
POST /your-endpoint
Solume-Event: test.order.paid
Solume-Livemode: false
Solume-Delivery: cm0entrega0ejemplo
Solume-Signature: t=1793880000,v1=...
json
{
  "id": "cm0entrega0ejemplo",
  "evento": "test.order.paid",
  "creado": "2026-11-05T12:00:00.123Z",
  "datos": {
    "order_id": "ord_2de517a19ba075fc18",
    "status": "provisioning",
    "service_id": "K3F8QX2P",
    "total": { "amount_cents": 500, "currency": "USD" },
    "payment_method": "balance",
    "servicio": { "codigo": "K3F8QX2P", "nombre": "web-1", "tipo": "VPS" },
    "livemode": false
  }
}

id is the delivery id (also in Solume-Delivery): use it to ignore duplicates. creado is the real time of the delivery, not the virtual clock. datos holds the data of the event (the same data as data in GET /events), plus livemode, and plus servicio (codigo, nombre, tipo) on the events about a service. The summary event has this datos:

json
{ "events_generated": 400, "events_delivered": 50, "events_omitted": 350, "livemode": false }

Event types, all emitted by the sandbox:

Event When Production emits it today
order.paid Order accepted and charged Yes
order.failed Provisioning failed (provision_failed); the amount is refunded Yes
service.provisioned / service.provision_failed Provisioning finished / failed Yes
service.suspended / service.unsuspended Unpaid at the due date / paid again Yes
service.terminated Unpaid after the grace days, or end of period (not for cancel with at: "now", as in production) Yes
service.renewed A renewal invoice was paid Yes
service.cancel_scheduled Cancellation at the end of the period requested Yes
invoice.created / invoice.paid / invoice.overdue Invoice lifecycle Yes
task.succeeded / task.failed An asynchronous action finished Yes
balance.low A balance payment of a renewal or an invoice left less than that payment (once a day) Yes

Production emits the same events with the same data (without the test. prefix, livemode and servicio), except where the table says otherwise. Some sandbox payloads carry extra fields (for example failed_attempts in invoice.overdue). A service event looks like this:

json
{
  "id": "cm0entrega0ejemplo2",
  "evento": "test.service.provisioned",
  "creado": "2026-11-05T12:00:11.456Z",
  "datos": {
    "service_id": "K3F8QX2P",
    "order_id": "ord_2de517a19ba075fc18",
    "status": "active",
    "servicio": { "codigo": "K3F8QX2P", "nombre": "web-1", "tipo": "VPS" },
    "livemode": false
  }
}

The other new events carry: service.provision_failed { service_id, order_id, status: "error", error: { code: "provision_failed", message } }, task.succeeded and task.failed { task_id, type, status, service_id } (plus error: { code } when failed) and service.cancel_scheduled { service_id, cancel_at }.

If you prefer polling, GET /sandbox/v1/events returns the same events oldest first, each one as { id, event, created_at, service, actor, data } with the plain event name (the same item as GET /v1/events; it is not the webhook envelope shown above). It takes since (an ISO-8601 date only; an event id is 400 invalid_request), type (one exact event name, without the test. prefix), limit (1 to 100, default 25) and cursor (the next_cursor of the previous page). Unlike production, events are stored even if you have no webhook. The sandbox keeps the latest 150 events, and only those of the last 30 virtual days.

bash
curl "https://api.solumecore.com/sandbox/v1/events?limit=50" \
  -H "Authorization: Bearer $SOLUME_KEY"
json
{
  "data": [
    {
      "id": "evt_647af1d9afa36e9923",
      "event": "order.paid",
      "created_at": "2026-11-05T12:00:00.000Z",
      "service": { "id": "K3F8QX2P", "name": "web-1", "type": "VPS" },
      "actor": null,
      "data": {
        "order_id": "ord_2de517a19ba075fc18",
        "status": "provisioning",
        "service_id": "K3F8QX2P",
        "total": { "amount_cents": 500, "currency": "USD" },
        "payment_method": "balance"
      }
    },
    {
      "id": "evt_6382c394bacab494b8",
      "event": "service.provisioned",
      "created_at": "2026-11-05T12:00:11.000Z",
      "service": { "id": "K3F8QX2P", "name": "web-1", "type": "VPS" },
      "actor": null,
      "data": { "service_id": "K3F8QX2P" }
    }
  ],
  "next_cursor": null
}

Sandbox activity never alerts SolumeCore staff.

8. Limits

Rate limits are counted per sandbox key and use fixed windows. The values come from the same platform settings as production (rateLimit.clientApi.*, the same table /v1 uses), so they follow the production configuration, and GET /me lists them. The X-RateLimit-* headers (general bucket) and 429 rate_limited with Retry-After behave as in production.

Bucket Default
General (all requests) 120 per minute per key and IP; in the sandbox also 5 times that per key across all IPs
Failed authentications 20 per minute per IP (a missing, invalid or revoked key, or a live key); beyond that 429 rate_limited before the key is read
Quotes 30 per minute per key
Orders (attempts) 30 per hour per key, counting rejected attempts too; the purchase cap of 10 orders per hour usually stops you first
Power 6 per minute and 40 per hour per service; 60 per minute per key
Reinstall 1 per 10 minutes and 5 per day per service; 20 per day per key
Reset password 3 per hour per service; 20 per hour per key
Renew and pay invoice 5 per hour per service
Hostname, rDNS, SSH keys, firewall changes, auto-renew 10 per hour per service
Service reads 60 per minute per service and 300 per minute per key, for GET /services/:id, /status, /metrics, GET /firewall and the gsk firewall reads
Cancel and cancel withdraw 3 per day per service
Read credentials 10 per hour per service
POST /time/advance (sandbox only) 30 per hour per key
POST /reset (sandbox only) 20 per hour per key

A rejected request answers 429 rate_limited with details.bucket, details.scope (service or key) and details.retry_after. Production uses sliding windows and the sandbox fixed windows, so the exact moment a limit frees up can differ slightly.

Order and account limits:

Limit Value Error
Orders provisioning at once 3 429 too_many_in_flight (details.retry_after: 15)
Spending caps (virtual clock) 1000.00 per order, 3000.00 per rolling 24 h, 10 orders per rolling hour 403 spending_cap_exceeded, with details.cap
Orders per virtual account 1000 429 rate_limited (details.limit: sandbox_orders); call /reset
Live (not terminated) services 50 409 conflict
SSH keys 30 per account and 30 per service 409 conflict
Top-up amount 5.00 to 10000.00 USD each; balance up to 100000.00 400 invalid_request / 409 conflict
Retained history 100 finished services, 150 orders, 200 invoices, 100 tasks, 150 events (the oldest finished ones are dropped)
Lifetime The account is deleted after 7 days without use; each use renews the term
Active accounts on the platform At most 500 sandbox accounts at once. When it is full, the first request that would create yours answers 429 rate_limited with Retry-After: 3600; existing accounts keep working 429 rate_limited (details.retry_after: 3600)
Concurrent requests Requests of one account run one at a time; one that waits more than 5 seconds answers 409 request_in_progress, retry it 409 request_in_progress
Request body 32 KB 413

Idempotency works as in production: Idempotency-Key (8 to 64 characters of A-Z a-z 0-9 _ -) is required on orders, renewals, reinstalls, password resets, cancellations, invoice payments and top-ups. The same key with the same body within 24 hours replays the stored answer (Idempotent-Replay: true); with a different body it answers 422 idempotency_key_reused; while the first request is still running it answers 409 request_in_progress. A request that ends in an error is not stored, so you can retry it with the same key (production behaves the same way).

9. Differences from production

The sandbox reproduces the API contract and the billing rules, not the infrastructure. These are the things it does not simulate:

Topic Production Sandbox
IP allowlist Staff registers the IPs; others get 403 ip_not_allowed None; ip_not_allowed never happens
Key approval, scopes and caps Granted and edited by staff Self-service, all scopes, fixed caps
Card payments Card processor, charged off-session with the mandate 2026-11-v2 you accepted on the website; real 3-D Secure Virtual Visa 4242 and mandate that you add with POST /payment-method/setup (none by default); card_declined and 3-D Secure (requires_action) only through scenarios
PayPal and crypto Hosted checkout; the balance is credited when the payment confirms Top-up credited instantly; checkout_url is a placeholder
Provisioning Real servers, nodes and times A timer (10 s by default); no machine exists
Stock Real; an unavailable plan cannot be bought The flag is shown but never blocks; use out_of_stock
Taxes May apply Always 0.00
Anti-fraud and abuse checks May reject an order with 403 order_rejected Never rejects
Node maintenance 503 maintenance Never
Reverse DNS Verifies the name resolves to the IP; applied asynchronously by staff (status: pending) pending, then applied after one virtual minute; no DNS check
Network and servers Reachable IPs; real metrics and firewall filtering Documentation IPs, synthetic metrics, rules stored but no traffic filtered
Billing job Runs once a day on the real clock Runs one virtual day at a time on time/advance (and on any request once something is due)
Rate limits Sliding windows; every value editable Same buckets and values, fixed windows, plus a per-key general cap of 5 times the per-key-and-IP one
Webhooks Real event names, your real secret, up to 8 attempts, paused after 20 consecutive failures test. names, derived secret, Solume-Livemode: false, no retries, at most 50 events per operation
Webhook payloads Data of the event The same data plus livemode: false and, for service events, servicio
GET /events Built from the deliveries of your webhooks: empty until you register one Stored even without a webhook; last 150 events
Notifications Emails and staff alerts None
Data lifetime Permanent 7 days after last use

Everything else behaves like production, and an automated test compares the error codes of both. That includes the rules that used to differ: cancel with at: "now" only for an active service, GET /services/:id/credentials with the manage permission and no HEAD, the reason values of the suspension and termination events, ip_not_allowed for a blocked IP, 402 requires_action for a missing card or mandate and 503 with Retry-After when the platform's Redis does not answer. Two details are still not identical: a paused service cannot exist in the sandbox (nothing pauses it), so you cannot try that 409 service_suspended case of cancel, and only the Redis outage is simulated: other platform dependencies that production can lose (database, payment processor, hypervisor) never fail here.

Treat any 5xx as retryable with the same Idempotency-Key, in both environments.

Do not use the sandbox for load testing: its limits are meant to be small.

10. Go-live checklist

  1. Switch the base URL to https://api.solumecore.com/v1 and the key to your sk_live_ key. Keep both in configuration, never in code.
  2. Create your live key in the panel (API keys tab, up to 10 active live keys) and open a ticket asking for the permissions and spending caps you need (purchase requires caps) and the public IPs your servers call from: a live key only works from allowed IPs.
  3. Save a card and accept the automatic-charge mandate, version 2026-11-v2, on the website. It cannot be done through the API. Check GET /payment-method on your live key: you need chargeable_off_session: true. The sandbox simulates the same check with its virtual card (section 3), but only the live key tells you the truth about the real card; without it card orders and payments fail with 402 requires_action (no_card or no_mandate) and auto_renew: card with 409 conflict.
  4. Top up the real balance if you plan to pay with balance.
  5. Re-test the error paths (card_declined, requires_action, insufficient_balance, out_of_stock, spending_cap_exceeded, rate_limited). Some only exist in production: order_rejected, ip_not_allowed for a key used from an IP that is not on its allowlist, maintenance, card_declined with details.reason insufficient_funds or stripe_disabled, and 503 when a dependency other than Redis is down.
  6. Idempotency. Generate one Idempotency-Key per business operation and reuse it on retries after timeouts and 5xx.
  7. Asynchronous work. Handle 202 tasks and long provisioning times. Real provisioning is slower than 10 seconds, so do not assume it.
  8. Webhooks. In production events have their real names (no test. prefix), are signed with your real whsec_ secret (not the derived one) and are retried up to 8 times. Verify the signature on the raw body, answer fast, store the delivery id to ignore duplicates, and keep your guard that ignores livemode: false in production logic. Register at least one webhook before relying on GET /events. service.provisioned, task.succeeded and task.failed also exist in production, but keep polling GET /services/:id and GET /tasks/:id as the fallback if a delivery is missed.
  9. Back off on 429 using Retry-After, and stay below the production limits.
  10. Credentials. Store the initial password the first time you read it; it can only be read once. The read needs the manage permission; there is no HEAD route.
  11. Renewals. Decide your auto_renew mode, and read billing for the real dates instead of hard-coding the defaults.
  12. Start small: a first live order with a cheap plan, then scale.

Everything else, including the full endpoint reference, error codes and code examples, is in the full guide.

FAQ

Is the reseller API sandbox free?

Yes. It uses a virtual account: nothing is charged, no server is provisioned and no real order, invoice or card is touched.

How do I get a sandbox key?

Create it yourself in the client panel under API and webhooks, in the Sandbox keys section. You can have up to 3 active sandbox keys; they need no staff approval and no IP addresses.

How do I test a declined card or a suspended service?

Send the X-Sandbox-Scenario header (for example card_declined) to force an error on a request, and call POST /sandbox/v1/time/advance to move the virtual clock so renewals, suspension and termination run.

What does the sandbox not simulate?

Staff approval and the IP allowlist, real card processing and 3-D Secure, PayPal and crypto, real provisioning times, anti-fraud checks and real stock. Read the differences section before going live.

Was this article helpful?
Reseller API Sandbox: Test Your Integration for Free