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.
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/advanceto jump forward in time,POST /resetto start over, andPOST /payment-method/setup/DELETE /payment-methodto 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:
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:
- Go to API and webhooks and open the API keys tab.
- 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. - 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:
# 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-methodanswers{ "payment_method": null }and a quote lists onlybalanceinpayable_with.payment: "card"answers402 requires_actionwithdetails.reasonno_card(no card) orno_mandate(card without the mandate).payment: "auto"pays with the balance when it covers the total; otherwise it answers402 insufficient_balancewithdetails.cardset to the same reason.auto_renew: "card"or"balance_and_card"(on an order or withPUT /services/:id/auto-renew) answers409 conflictwithdetails.reasonno_default_cardormandate_not_accepted.- The automatic card charge during
POST /time/advanceonly 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):
# 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).
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. |
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" }'{
"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/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Remaining: 0
X-Sandbox: true{
"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.
curl -X POST https://api.solumecore.com/sandbox/v1/time/advance \
-H "Authorization: Bearer $SOLUME_KEY" \
-H "Content-Type: application/json" \
-d '{ "days": 27 }'{
"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 getinvoice.created,invoice.paidandservice.renewed.cardcharges the virtual card;balance_and_cardtries 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_actionorinsufficient_balancesent on thePOST /time/advancerequest, in a mode that would use that method): the invoice becomesoverdueat once andinvoice.overdueis emitted once, withfailed_attemptsin its data. The next attempt is scheduled with the next value ofreintentosHoras. The service is suspended on the renewal date, or earlier if the list of retries runs out. - Not enough balance, no scenario (
auto_renew: balancewith a balance that does not cover the invoice): nothing is charged and the invoice staysopenuntil the renewal date, so you still have time to pay by hand. Then it becomesoverdueand the service is suspended, as withauto_renew: off. - You pay while suspended (
/renewor/invoices/:id/pay):invoice.paid,service.renewed,service.unsuspended. - You renew too early (
POST /services/:id/renewbefore the renewal window opens): nothing is charged. It answers200withalready_paid: trueand the last paid invoice (ornull), as in production. Inside the window it creates the renewal invoice if needed and pays it. POST /services/:id/cancelwithat: period_end:service.cancel_scheduled; no renewal invoice; at the end of the period the service is terminated (service.terminatedwithreason: cancelled_at_period_end).POST /services/:id/cancel/withdrawcancels the schedule and counts in thecancelbucket (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
curl -X POST https://api.solumecore.com/sandbox/v1/reset \
-H "Authorization: Bearer $SOLUME_KEY"{
"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 toorder.paid, totest.order.paidor to*receives it. A webhook subscribed only totest.<event>names receives sandbox deliveries and never a production one. The name is in the envelope (evento) and in theSolume-Eventheader. - Headers.
Solume-Livemode: false(production sendstrue), plusSolume-SignatureandSolume-Deliveryas usual. - Payload.
"livemode": falseinsidedatos. - Signature with a derived secret. The signature is built like production (
Solume-Signature: t=<unix seconds>,v1=<hex>, wherev1is the HMAC-SHA256 of<t>.<raw body>), but keyed with a secret derived from your webhook secret, so it does not verify with your realwhsec_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:
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/advancegenerates 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 inGET /events. - Same webhook, both environments. One webhook receives live and sandbox events. Check
Solume-Livemodeorlivemodeto keep test events away from your production logic. - Contents. Ids and statuses, never credentials.
Envelope and headers of a sandbox delivery:
POST /your-endpoint
Solume-Event: test.order.paid
Solume-Livemode: false
Solume-Delivery: cm0entrega0ejemplo
Solume-Signature: t=1793880000,v1=...{
"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:
{ "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:
{
"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.
curl "https://api.solumecore.com/sandbox/v1/events?limit=50" \
-H "Authorization: Bearer $SOLUME_KEY"{
"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
- Switch the base URL to
https://api.solumecore.com/v1and the key to yoursk_live_key. Keep both in configuration, never in code. - 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 (
purchaserequires caps) and the public IPs your servers call from: a live key only works from allowed IPs. - 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-methodon your live key: you needchargeable_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 with402 requires_action(no_cardorno_mandate) andauto_renew: cardwith409 conflict. - Top up the real balance if you plan to pay with balance.
- 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_allowedfor a key used from an IP that is not on its allowlist,maintenance,card_declinedwithdetails.reasoninsufficient_fundsorstripe_disabled, and503when a dependency other than Redis is down. - Idempotency. Generate one
Idempotency-Keyper business operation and reuse it on retries after timeouts and 5xx. - Asynchronous work. Handle
202tasks and long provisioning times. Real provisioning is slower than 10 seconds, so do not assume it. - Webhooks. In production events have their real names (no
test.prefix), are signed with your realwhsec_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 ignoreslivemode: falsein production logic. Register at least one webhook before relying onGET /events.service.provisioned,task.succeededandtask.failedalso exist in production, but keep pollingGET /services/:idandGET /tasks/:idas the fallback if a delivery is missed. - Back off on
429usingRetry-After, and stay below the production limits. - Credentials. Store the initial password the first time you read it; it can only be read once. The read needs the
managepermission; there is noHEADroute. - Renewals. Decide your
auto_renewmode, and readbillingfor the real dates instead of hard-coding the defaults. - 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.
Related articles
- Reseller API guide
- Reseller API sandbox
- Account API keys
- Webhooks and account events
- Which SolumeCore API to use
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.