Iniciar sesiónCrear servidor
API

Sandbox de la API de revendedor: prueba sin gastar dinero

Prueba tu integración con la API de SolumeCore en un entorno gratis: llaves sk_test_, saldo virtual, escenarios de error, viaje en el tiempo y webhooks.

32 min de lectura#api sandbox#reseller api#test api keys#sk_test#time travel#api scenarios#vps api

El sandbox es una copia de pruebas de la API de revendedor. Con él construyes y pruebas toda tu integración (órdenes, renovaciones, suspensiones, webhooks, manejo de errores) sin gastar dinero y sin tocar ningún servidor real.

Esta guía cubre lo que es propio del sandbox. Los endpoints, los cuerpos de las peticiones y la forma de las respuestas son los de producción: consulta la guía completa y el archivo OpenAPI.

Sandbox Producción
URL base https://api.solumecore.com/sandbox/v1 https://api.solumecore.com/v1
Prefijo de la llave sk_test_ sk_live_
Dinero Virtual Real
Servidores Registros simulados VPS y VDS reales

1. Qué es y para qué sirve

  • La misma API. El sandbox expone las mismas rutas, cuerpos, errores y cabeceras que /v1. Pasar a producción es cambiar la URL base y la llave, nada más.
  • Cuenta virtual privada. Cada llave del sandbox tiene su propia cuenta: un saldo virtual, una tarjeta virtual y sus propios servicios, órdenes, facturas y tareas. Dos llaves nunca ven los datos de la otra.
  • Herramientas solo del sandbox. Cabeceras para forzar fallos, POST /time/advance para saltar en el tiempo, POST /reset para empezar de cero, y POST /payment-method/setup / DELETE /payment-method para agregar o quitar la tarjeta virtual.
  • Sin efectos reales. Una petición al sandbox nunca lee ni escribe un servicio, orden, factura, saldo, tarjeta o pago reales.

Primera petición:

bash
export SOLUME_KEY=sk_test_xxxxxxxx

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

Devuelve el saldo virtual. Una cuenta nueva empieza con 1000.00 USD.

2. Llaves del sandbox (sk_test_)

Las creas tú mismo en el panel del cliente:

  1. Entra en API y webhooks y abre la pestaña Claves de API.
  2. En la sección Llaves de pruebas (sandbox), pulsa Nueva llave de pruebas y ponle un nombre (por ejemplo Pruebas de CI). Igual que con una llave real, te pide confirmar tu identidad otra vez, y no se puede crear desde una sesión de soporte.
  3. Copia el secreto (sk_test_...). Se muestra una sola vez. Solo guardamos una huella, así que una llave perdida no se puede recuperar: revócala y crea otra.
Propiedad Llave del sandbox
Aprobación Ninguna. Se crea al instante.
Lista de IPs permitidas Ninguna. Funciona desde cualquier IP.
Llaves activas por cuenta Hasta 3 del sandbox (y, aparte, hasta 10 reales)
Permisos Todos: read, manage, purchase, billing
Topes de gasto virtuales 1000.00 USD por orden, 3000.00 USD por 24 horas móviles, 10 órdenes por hora móvil
Descuento y recargo de tarjeta 0% y 0%

Los topes existen para que puedas probar spending_cap_exceeded. GET /sandbox/v1/me muestra los topes, tu uso y todos los niveles de límite de tasa. Revocar o borrar una llave también borra su cuenta virtual.

Las llaves y los entornos no se mezclan:

bash
# Una llave del sandbox en producción
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_..." } }

Una llave real (sk_live_) en /sandbox/v1 también responde 403 wrong_environment, y cuenta como una autenticación fallida (ver la sección 8).

3. Qué es real y qué es virtual

Elemento En el sandbox
Catálogo (planes, ubicaciones, plantillas de SO, opciones, precios) Real, de solo lectura. Las cotizaciones usan precios reales y el descuento de tu llave.
Saldo Virtual. Empieza en 1000.00 USD.
Tarjeta Virtual: una Visa terminada en 4242 que agregas tú; una cuenta nueva no tiene ninguna (ver abajo).
Servicios, órdenes, facturas, tareas Registros virtuales en tu cuenta privada.
Credenciales Falsas, aleatorias, se leen una sola vez.
Direcciones IP Rangos de documentación (192.0.2.x, 198.51.100.x, 203.0.113.x, 2001:db8::/32). No son enrutables.
Impuestos Ninguno: tax siempre es 0.00.
Cobro, aprovisionamiento No se cobra nada y no se aprovisiona nada. No hay ninguna máquina a la que conectarse.

Tarjeta y mandato: una cuenta nueva no tiene ninguno, como un cliente nuevo. En producción un pago con tarjeta exige una tarjeta predeterminada guardada con el mandato de cobro automático versión 2026-11-v2 aceptado, que el titular configura en la web. El sandbox aplica las mismas reglas a una tarjeta virtual:

  • GET /payment-method responde { "payment_method": null } y una cotización lista solo balance en payable_with.
  • payment: "card" responde 402 requires_action con details.reason no_card (sin tarjeta) o no_mandate (tarjeta sin el mandato). payment: "auto" paga con el saldo cuando alcanza para el total; si no, responde 402 insufficient_balance con details.card igual al mismo motivo.
  • auto_renew: "card" o "balance_and_card" (en una orden o con PUT /services/:id/auto-renew) responde 409 conflict con details.reason no_default_card o mandate_not_accepted.
  • El cobro automático con tarjeta durante POST /time/advance solo usa una tarjeta con el mandato aceptado; sin él la factura queda vencida.

En el sandbox no hay web, así que lo hacen dos endpoints que existen solo en /sandbox/v1 (ambos piden el permiso billing):

bash
# Agrega la Visa virtual 4242 y acepta el mandato (el cuerpo es opcional; accept_mandate vale true por defecto)
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 }'

# Deja la tarjeta pero sin el mandato, para probar no_mandate / mandate_not_accepted
#   -d '{ "accept_mandate": false }'

# Quita la tarjeta y el mandato
curl -X DELETE https://api.solumecore.com/sandbox/v1/payment-method \
  -H "Authorization: Bearer $SOLUME_KEY"

Los dos responden como GET /payment-method ({ "payment_method": { "brand": "visa", "last4": "4242", "mandate_accepted": true, "chargeable_off_session": true, ... } }, o null tras el borrado). Volver a llamar a setup solo actualiza el mandato de la misma tarjeta, y POST /reset quita la tarjeta. Con tarjeta y mandato, los pagos con tarjeta salen bien salvo que envíes el escenario card_declined o requires_action (sección 5), que mandan sobre la falta de tarjeta; el error de 3-D Secure lleva details.reason: "authentication_required" como en producción. Comprueba el mandato real con GET /payment-method en tu llave real antes de pasar a producción (sección 10).

Como el catálogo es real, un plan agotado en producción también muestra available: false en el sandbox, pero no bloquea una orden de prueba (usa el escenario out_of_stock para probarlo).

Para añadir dinero virtual llama a POST /sandbox/v1/balance/top-ups (method: card, paypal o crypto). Funciona al instante y el saldo se acredita. La checkout_url que devuelve es un marcador: no hay nada que pagar.

4. Ciclo de vida simulado

Una orden cobra el saldo o la tarjeta virtual y crea un servicio en provisioning.

Paso Tiempo
Orden aceptada, servicio provisioning Inmediato (201)
El servicio pasa a active y se emite service.provisioned Unos 10 segundos. Cámbialo por orden con X-Sandbox-Provision-Seconds (0 a 120).
Acción de energía (start, stop, restart, shutdown) queued 1 s, lista en 4 a 5 s
Restablecer contraseña Lista en unos 4 s
Reinstalar Lista en unos 9 s
Petición de DNS inverso (PTR) pending, y un minuto virtual después pasa a applied
Cancelar ahora Inmediato: 200 con status: cancelled (no es una tarea)

Energía, reinstalar y restablecer contraseña devuelven 202 con una tarea. Consulta GET /sandbox/v1/tasks/:id o espera el webhook task.succeeded o task.failed.

Las credenciales funcionan como en producción: la contraseña inicial de un servicio nuevo (y la que produce un restablecimiento o una reinstalación) es aleatoria, vale 24 horas y se lee una sola vez en GET /services/:id/credentials. Una segunda lectura responde 410 credentials_consumed; pasadas 24 horas virtuales responde 404 credentials_not_available. Pide una nueva con POST /services/:id/reset-password. Igual que en producción, esa lectura exige el permiso manage (si no, 403 insufficient_scope, sin gastar la lectura) y no existe la ruta HEAD (responde 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"
  }'

Toma los ids de GET /catalog/plans, /catalog/locations y /catalog/os-templates. El sandbox solo vende VPS y VDS, igual que producción (si no, product_not_resellable).

El tiempo aquí es tiempo virtual: el reloj de la cuenta es el reloj real más lo que hayas adelantado (sección 6).

5. Control de escenarios

Envía estas cabeceras para hacer fallar el sandbox a propósito. /v1 las ignora. Un valor desconocido responde 400 invalid_request con los valores permitidos en details.

X-Sandbox-Scenario

El escenario se aplica solo a la petición que lo lleva.

Valor Dónde se aplica Qué ocurre
card_declined Todo pago que usaría la tarjeta: POST /orders, /services/:id/renew, /invoices/:id/pay con payment card o auto; POST /balance/top-ups con method: card; el cobro automático con tarjeta de POST /time/advance 402 card_declined. No se crea nada. payment: balance no se afecta.
requires_action Las mismas rutas que card_declined 402 requires_action (la tarjeta necesita 3-D Secure). No se crea nada.
insufficient_balance Todo pago que usaría el saldo (balance o auto) y el cobro automático con saldo de POST /time/advance 402 insufficient_balance aunque el saldo alcance, con required_cents en details. payment: card no se afecta.
out_of_stock POST /orders (una cotización no lleva disponibilidad, así que POST /quotes no se afecta) 409 out_of_stock. No se crea nada.
provision_failed POST /orders La orden se acepta (201). Al terminar el aprovisionamiento el servicio pasa a error, la orden a failed, el importe se devuelve al saldo virtual y se emiten service.provision_failed y order.failed.
task_failed Peticiones que crean una tarea: energía, reinstalar, restablecer contraseña 202 como siempre; luego la tarea termina failed (error.code: task_failed) y se emite task.failed. El servicio queda como estaba.
slow Cualquier petición Añade 3 segundos de latencia. Después la petición se ejecuta con normalidad.
rate_limited Cualquier petición 429 rate_limited inmediato con Retry-After: 30. No se ejecuta nada más.
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" }
  }
}

Los demás errores de escenario tienen la misma forma:

Código Estado details
requires_action 402 { "reason": "authentication_required" }
insufficient_balance 402 { "required_cents": 500 }
out_of_stock 409 ninguno

Una tarjeta o un mandato que faltan no son un escenario: es el estado real de tu cuenta virtual (sección 3) y responde 402 requires_action con details.reason no_card o no_mandate. Los escenarios mandan sobre eso. Producción también puede responder card_declined con details.reason insufficient_funds o stripe_disabled, que el sandbox nunca produce.

El escenario rate_limited responde:

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 }
  }
}

Hostnames mágicos

En POST /orders, un hostname que empieza con una de estas palabras tiene el mismo efecto que el escenario. No distingue mayúsculas y puede ir seguido de - y cualquier cosa (fail-card-2). Si la cabecera y el hostname no coinciden, gana la cabecera (slow y rate_limited nunca tocan el pago, así que el hostname sigue valiendo).

Hostname Equivale a
fail-card card_declined
fail-stock out_of_stock
fail-provision provision_failed

Otras cabeceras de petición

Cabecera Valores Efecto
X-Sandbox-Provision-Seconds Entero de 0 a 120 (por defecto 10) Cuánto tiempo permanecen en provisioning los servicios que crea esa petición. Cualquier otro valor es 400 invalid_request.
X-Sandbox-Firewall-Engine shield (por defecto), gsk, none Motor de firewall de los servicios que crea esa orden, como el nodo de un servicio real. Llamar a una función del otro motor responde 422 feature_not_supported; con none, 409 firewall_unavailable.

Cabeceras de respuesta

Cabecera Significado
X-Sandbox: true En todas las respuestas del sandbox, también en los errores.
X-Sandbox-Virtual-Time El reloj virtual de la cuenta (ISO-8601), en las respuestas que leen la cuenta.
X-Request-Id, X-RateLimit-*, Retry-After, Idempotent-Replay Igual que en producción.

6. Viaje en el tiempo y reinicio

Las renovaciones, las facturas vencidas, la suspensión y la terminación tardan semanas en producción. En el sandbox saltas hasta ellas.

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 es un entero de 1 a 400 (cualquier otro valor es 400 invalid_request). El reloj avanza de a un día y en cada paso corre el ciclo de vida, como el trabajo diario de facturación. No necesita Idempotency-Key. Puedes llamarlo 30 veces por hora por llave; pasado eso responde 429 rate_limited con Retry-After (en segundos).

El ciclo de vida usa las mismas reglas que producción, leídas de la configuración de facturación de la plataforma. Esa configuración la puede cambiar el personal de SolumeCore, así que los números de abajo son los valores por defecto y pueden cambiar:

Ajuste Significado Por defecto
dunning.diasAvisoPrevio Días antes de la fecha de renovación en que se crea la factura de renovación 3
dunning.reintentosHoras Horas de espera antes de cada reintento automático tras un cobro fallido [24, 72, 168]
dunning.diasGraciaCancelacion Días que un servicio permanece suspendido antes de terminarse 15

Lee las fechas reales de un servicio en su objeto billing (GET /services/:id) en lugar de fijar los valores por defecto en tu código.

Qué le pasa a un servicio mensual (valores por defecto, auto_renew: off) comprado el día 0:

Cuándo Eventos Estado del servicio
Día 0 invoice.created, invoice.paid, order.paid de la orden; luego service.provisioned provisioning, luego active
Fecha de renovación menos 3 días invoice.created (factura de renovación) active
Fecha de renovación, factura sin pagar invoice.overdue, service.suspended (reason: non_payment) suspended, apagado
Suspensión más 15 días service.terminated (reason: non_payment); la factura abierta se cancela cancelled

Los valores de reason son los que usa producción: non_payment (sin pagar), cancelled_at_period_end (terminó el periodo tras programar la cancelación) y other.

Variaciones:

  • auto_renew: balance, con saldo suficiente: al crearse la factura de renovación se cobra de inmediato y recibes invoice.created, invoice.paid y service.renewed. card cobra la tarjeta virtual; balance_and_card prueba primero el saldo y luego la tarjeta. Los dos modos con tarjeta exigen la tarjeta y el mandato (sección 3); si quitas la tarjeta después, el cobro no puede hacerse y la factura queda vencida.
  • Un cobro falla por un escenario (card_declined, requires_action o insufficient_balance enviado en la propia petición POST /time/advance, en un modo que usaría ese método): la factura pasa a overdue de inmediato y se emite invoice.overdue una vez, con failed_attempts en sus datos. El siguiente intento se programa con el siguiente valor de reintentosHoras. El servicio se suspende en la fecha de renovación, o antes si se acaba la lista de reintentos.
  • Saldo insuficiente, sin escenario (auto_renew: balance con un saldo que no cubre la factura): no se cobra nada y la factura sigue open hasta la fecha de renovación, así que aún tienes tiempo de pagarla a mano. Después pasa a overdue y el servicio se suspende, igual que con auto_renew: off.
  • Pagas estando suspendido (/renew o /invoices/:id/pay): invoice.paid, service.renewed, service.unsuspended.
  • Renuevas demasiado pronto (POST /services/:id/renew antes de que abra la ventana de renovación): no se cobra nada. Responde 200 con already_paid: true y la última factura pagada (o null), como en producción. Dentro de la ventana crea la factura de renovación si hace falta y la paga.
  • POST /services/:id/cancel con at: period_end: service.cancel_scheduled; no hay factura de renovación; al final del periodo el servicio se termina (service.terminated con reason: cancelled_at_period_end). POST /services/:id/cancel/withdraw anula la programación y cuenta en la cubeta cancel (sección 8).
  • Las credenciales que nunca leíste caducan a las 24 horas virtuales.
  • Los topes usan el reloj virtual. Avanzar un día vacía las ventanas de gasto diaria y por hora. Los límites de tasa usan tiempo real y no se afectan.

El trabajo que vence se procesa en la siguiente petición a la cuenta, o con un temporizador en segundo plano cuando vence en unos diez minutos, así que los webhooks llegan aunque no hagas peticiones. La cabecera de escenario se aplica a la petición que la lleva: para probar renovaciones fallidas, envíala en el propio POST /time/advance.

Reinicio

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"
}

Borra la cuenta virtual: servicios, órdenes, facturas, tareas, eventos, llaves SSH, la tarjeta virtual y el mandato, claves de idempotencia y los contadores de límites por servicio y por cubeta (órdenes, energía, reinstalar...). El saldo vuelve a 1000.00 USD y el reloj al tiempo real. Tu llave y tus webhooks se conservan. Los contadores general, de time/advance y de reinicio no se limpian. Puedes reiniciar 20 veces por hora; pasado eso responde 429 rate_limited con Retry-After (en segundos).

7. Webhooks y eventos

El sandbox usa los webhooks que ya registraste en el panel del cliente (API y webhooks, pestaña Webhooks). No hay nada específico del sandbox que configurar; solo asegúrate de que el webhook esté activo y suscrito a los nombres de evento que necesitas: los nombres reales (el panel ofrece esos y *, todos los eventos) y, si quieres solo las entregas del sandbox, los nombres test.<evento>: en el editor del webhook son el grupo Sandbox (solo eventos de prueba).

Las entregas del sandbox nunca se pueden confundir con las de producción:

  • Nombre del evento. Todo evento viaja como test.<evento> (test.order.paid). Lo recibe un webhook suscrito a order.paid, a test.order.paid o a *. Un webhook suscrito solo a nombres test.<evento> recibe entregas del sandbox y nunca una de producción. El nombre va en el sobre (evento) y en la cabecera Solume-Event.
  • Cabeceras. Solume-Livemode: false (producción envía true), además de Solume-Signature y Solume-Delivery como siempre.
  • Payload. "livemode": false dentro de datos.
  • Firma con un secreto derivado. La firma se arma como en producción (Solume-Signature: t=<segundos unix>,v1=<hex>, donde v1 es el HMAC-SHA256 de <t>.<cuerpo crudo>), pero con un secreto derivado del secreto de tu webhook, así que no se verifica con tu secreto real whsec_. Así una llave del sandbox (que se crea sin aprobación del personal) no puede falsificar un evento de producción. Derívalo así y úsalo como secreto en tu verificación de siempre:
js
import crypto from 'node:crypto';

// whsec_... es el secreto de firma del webhook, la cadena completa.
export const sandboxSecret = (whsec) =>
  crypto.createHmac('sha256', whsec).update('solume-sandbox-webhook').digest('hex');
  • Sin reintentos. Una entrega del sandbox se intenta una sola vez. Nunca suma ni reinicia los fallos consecutivos que pausan un webhook real.
  • Como máximo 50 eventos por operación. Si una petición o un POST /time/advance genera más, solo se entregan los últimos 50, seguidos de un evento resumen, test.sandbox.events_summarized. Solo llega a los webhooks suscritos a todos los eventos (*) y no se guarda en GET /events.
  • El mismo webhook, los dos entornos. Un webhook recibe eventos reales y del sandbox. Revisa Solume-Livemode o livemode para mantener los eventos de prueba lejos de tu lógica de producción.
  • Contenido. Ids y estados, nunca credenciales.

Sobre y cabeceras de una entrega del sandbox:

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 es el id de la entrega (también va en Solume-Delivery): úsalo para ignorar duplicados. creado es la hora real de la entrega, no el reloj virtual. datos lleva los datos del evento (los mismos de data en GET /events), más livemode, y más servicio (codigo, nombre, tipo) en los eventos de un servicio. El evento resumen lleva estos datos:

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

Tipos de evento, todos los emite el sandbox:

Evento Cuándo ¿Producción lo emite hoy?
order.paid Orden aceptada y cobrada Sí
order.failed Falló el aprovisionamiento (provision_failed); se devuelve el importe Sí
service.provisioned / service.provision_failed El aprovisionamiento terminó / falló Sí
service.suspended / service.unsuspended Sin pagar en la fecha de vencimiento / pagado de nuevo Sí
service.terminated Sin pagar tras los días de gracia, o fin de periodo (no con cancel y at: "now", como en producción) Sí
service.renewed Se pagó una factura de renovación Sí
service.cancel_scheduled Se pidió la cancelación al final del periodo Sí
invoice.created / invoice.paid / invoice.overdue Ciclo de vida de la factura Sí
task.succeeded / task.failed Terminó una acción asíncrona Sí
balance.low Un pago con saldo de una renovación o factura dejó menos que ese pago (una vez al día) Sí

Producción emite los mismos eventos con el mismo data (sin el prefijo test., livemode ni servicio), salvo donde la tabla diga otra cosa. Algunos payloads del sandbox llevan campos extra (por ejemplo failed_attempts en invoice.overdue). Un evento de servicio se ve así:

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
  }
}

Los demás eventos nuevos llevan: service.provision_failed { service_id, order_id, status: "error", error: { code: "provision_failed", message } }, task.succeeded y task.failed { task_id, type, status, service_id } (más error: { code } si falló) y service.cancel_scheduled { service_id, cancel_at }.

Si prefieres consultar, GET /sandbox/v1/events devuelve los mismos eventos del más viejo al más nuevo, cada uno como { id, event, created_at, service, actor, data } con el nombre de evento sin prefijo (el mismo elemento que GET /v1/events; no es el sobre de webhook de arriba). Acepta since (solo una fecha ISO-8601; un id de evento es 400 invalid_request), type (un nombre exacto de evento, sin el prefijo test.), limit (1 a 100, por defecto 25) y cursor (el next_cursor de la página anterior). A diferencia de producción, los eventos se guardan aunque no tengas ningún webhook. El sandbox conserva los últimos 150 eventos, y solo los de los últimos 30 días virtuales.

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
}

La actividad del sandbox nunca avisa al personal de SolumeCore.

8. Límites

Los límites de tasa se cuentan por llave del sandbox y usan ventanas fijas. Los valores salen de los mismos ajustes de la plataforma que producción (rateLimit.clientApi.*, la misma tabla que usa /v1), así que siguen la configuración de producción, y GET /me los lista. Las cabeceras X-RateLimit-* (cubeta general) y el 429 rate_limited con Retry-After se comportan como en producción.

Cubeta Por defecto
General (todas las peticiones) 120 por minuto por llave e IP; en el sandbox además 5 veces eso por llave sumando todas las IPs
Autenticaciones fallidas 20 por minuto por IP (llave ausente, inválida o revocada, o una llave real); pasado eso 429 rate_limited antes de mirar la llave
Cotizaciones 30 por minuto por llave
Órdenes (intentos) 30 por hora por llave, contando también los intentos rechazados; el tope de compra de 10 órdenes por hora suele pararte antes
Energía 6 por minuto y 40 por hora por servicio; 60 por minuto por llave
Reinstalar 1 cada 10 minutos y 5 por día por servicio; 20 por día por llave
Restablecer contraseña 3 por hora por servicio; 20 por hora por llave
Renovar y pagar factura 5 por hora por servicio
Hostname, rDNS, llaves SSH, cambios de firewall, auto-renew 10 por hora por servicio
Lecturas de servicio 60 por minuto por servicio y 300 por minuto por llave, para GET /services/:id, /status, /metrics, GET /firewall y las lecturas de firewall gsk
Cancelar y retirar la cancelación 3 por día por servicio
Leer credenciales 10 por hora por servicio
POST /time/advance (solo sandbox) 30 por hora por llave
POST /reset (solo sandbox) 20 por hora por llave

Una petición rechazada responde 429 rate_limited con details.bucket, details.scope (service o key) y details.retry_after. Producción usa ventanas deslizantes y el sandbox ventanas fijas, así que el instante exacto en que se libera un límite puede diferir un poco.

Límites de órdenes y de la cuenta:

Límite Valor Error
Órdenes aprovisionándose a la vez 3 429 too_many_in_flight (details.retry_after: 15)
Topes de gasto (reloj virtual) 1000.00 por orden, 3000.00 por 24 h móviles, 10 órdenes por hora móvil 403 spending_cap_exceeded, con details.cap
Órdenes por cuenta virtual 1000 429 rate_limited (details.limit: sandbox_orders); llama a /reset
Servicios vivos (no terminados) 50 409 conflict
Llaves SSH 30 por cuenta y 30 por servicio 409 conflict
Monto de recarga 5.00 a 10000.00 USD cada una; saldo hasta 100000.00 400 invalid_request / 409 conflict
Historial conservado 100 servicios terminados, 150 órdenes, 200 facturas, 100 tareas, 150 eventos (se descartan los más viejos ya terminados)
Vigencia La cuenta se borra tras 7 días sin uso; cada uso renueva el plazo
Cuentas activas en la plataforma Como máximo 500 cuentas del sandbox a la vez. Si está llena, la primera petición que crearía la tuya responde 429 rate_limited con Retry-After: 3600; las cuentas que ya existen siguen funcionando 429 rate_limited (details.retry_after: 3600)
Peticiones simultáneas Las peticiones de una cuenta se ejecutan de una en una; una que espera más de 5 segundos responde 409 request_in_progress; reinténtala 409 request_in_progress
Cuerpo de la petición 32 KB 413

La idempotencia funciona como en producción: Idempotency-Key (8 a 64 caracteres de A-Z a-z 0-9 _ -) es obligatoria en órdenes, renovaciones, reinstalaciones, restablecimientos de contraseña, cancelaciones, pagos de factura y recargas. La misma clave con el mismo cuerpo dentro de 24 horas repite la respuesta guardada (Idempotent-Replay: true); con otro cuerpo responde 422 idempotency_key_reused; mientras la primera petición sigue en curso responde 409 request_in_progress. Una petición que termina en error no se guarda, así que puedes reintentarla con la misma clave (producción se comporta igual).

9. Diferencias con producción

El sandbox reproduce el contrato de la API y las reglas de facturación, no la infraestructura. Esto es lo que no simula:

Tema Producción Sandbox
IPs permitidas El personal registra las IPs; las demás reciben 403 ip_not_allowed Ninguna; ip_not_allowed nunca ocurre
Aprobación de la llave, permisos y topes Los concede y edita el personal Autoservicio, todos los permisos, topes fijos
Pagos con tarjeta Procesador de tarjetas, cobro fuera de sesión con el mandato 2026-11-v2 que aceptaste en la web; 3-D Secure real Visa virtual 4242 y mandato que agregas con POST /payment-method/setup (ninguno por defecto); card_declined y 3-D Secure (requires_action) solo con escenarios
PayPal y cripto Pasarela alojada; el saldo se acredita al confirmarse el pago Recarga acreditada al instante; checkout_url es un marcador
Aprovisionamiento Servidores, nodos y tiempos reales Un temporizador (10 s por defecto); no existe ninguna máquina
Stock Real; un plan no disponible no se puede comprar La marca se muestra pero nunca bloquea; usa out_of_stock
Impuestos Pueden aplicarse Siempre 0.00
Antifraude y abuso Puede rechazar una orden con 403 order_rejected Nunca rechaza
Mantenimiento de nodo 503 maintenance Nunca
DNS inverso Verifica que el nombre resuelva a la IP; lo aplica el personal de forma asíncrona (status: pending) pending y, un minuto virtual después, applied; sin comprobar el DNS
Red y servidores IPs alcanzables; métricas reales y filtrado real del firewall IPs de documentación, métricas sintéticas, reglas guardadas pero sin filtrar tráfico
Trabajo de facturación Corre una vez al día con el reloj real Corre un día virtual cada vez en time/advance (y en cualquier petición cuando algo vence)
Límites de tasa Ventanas deslizantes; todos los valores editables Mismas cubetas y valores, ventanas fijas, y además un tope general por llave de 5 veces el de llave e IP
Webhooks Nombres reales, tu secreto real, hasta 8 intentos, pausa tras 20 fallos consecutivos Nombres test., secreto derivado, Solume-Livemode: false, sin reintentos, 50 eventos por operación como máximo
Payloads de webhook Los datos del evento Los mismos datos más livemode: false y, en eventos de servicio, servicio
GET /events Se arma con las entregas de tus webhooks: vacío hasta que registres uno Se guarda aunque no haya webhook; últimos 150 eventos
Notificaciones Correos y alertas al personal Ninguna
Vigencia de los datos Permanentes 7 días tras el último uso

Todo lo demás se comporta como producción, y una prueba automática compara los códigos de error de ambos. Eso incluye las reglas que antes diferían: cancel con at: "now" solo para un servicio active, GET /services/:id/credentials con el permiso manage y sin HEAD, los valores de reason de los eventos de suspensión y baja, ip_not_allowed para una IP bloqueada, 402 requires_action por falta de tarjeta o mandato y 503 con Retry-After cuando el Redis de la plataforma no responde. Quedan dos diferencias: un servicio pausado no puede existir en el sandbox (nada lo pausa), así que no puedes probar ese caso del 409 service_suspended de cancel; y solo se simula la caída de Redis: otras dependencias que producción puede perder (base de datos, procesador de pagos, hipervisor) nunca fallan aquí.

Trata cualquier 5xx como reintentable con la misma Idempotency-Key, en ambos entornos.

No uses el sandbox para pruebas de carga: sus límites son pequeños a propósito.

10. Lista de comprobación para pasar a producción

  1. Cambia la URL base a https://api.solumecore.com/v1 y la llave a tu sk_live_. Mantén ambas en configuración, nunca en el código.
  2. Crea tu llave real en el panel (pestaña Claves de API, hasta 10 llaves reales activas) y abre un ticket pidiendo los permisos y topes de gasto que necesitas (purchase exige topes) y las IPs públicas desde las que llaman tus servidores: una llave real solo funciona desde IPs autorizadas.
  3. Guarda una tarjeta y acepta el mandato de cobro automático, versión 2026-11-v2, en la web. No se puede hacer por la API. Revisa GET /payment-method en tu llave real: necesitas chargeable_off_session: true. El sandbox simula la misma comprobación con su tarjeta virtual (sección 3), pero solo la llave real te dice la verdad sobre la tarjeta real; sin ella las órdenes y pagos con tarjeta fallan con 402 requires_action (no_card o no_mandate) y auto_renew: card con 409 conflict.
  4. Recarga el saldo real si piensas pagar con saldo.
  5. Vuelve a probar los caminos de error (card_declined, requires_action, insufficient_balance, out_of_stock, spending_cap_exceeded, rate_limited). Algunos solo existen en producción: order_rejected, ip_not_allowed para una llave usada desde una IP que no está en su lista, maintenance, card_declined con details.reason insufficient_funds o stripe_disabled, y el 503 cuando cae una dependencia distinta de Redis.
  6. Idempotencia. Genera una Idempotency-Key por operación de negocio y reutilízala en los reintentos tras un timeout o un 5xx.
  7. Trabajo asíncrono. Maneja las tareas 202 y los tiempos de aprovisionamiento largos. El aprovisionamiento real es más lento que 10 segundos, no lo des por hecho.
  8. Webhooks. En producción los eventos tienen su nombre real (sin prefijo test.), se firman con tu secreto real whsec_ (no el derivado) y se reintentan hasta 8 veces. Verifica la firma sobre el cuerpo crudo, responde rápido, guarda el id de entrega para ignorar duplicados y mantén tu guarda para ignorar livemode: false en la lógica de producción. Registra al menos un webhook antes de depender de GET /events. service.provisioned, task.succeeded y task.failed también existen en producción, pero sigue consultando GET /services/:id y GET /tasks/:id como respaldo por si se pierde una entrega.
  9. Retrocede ante un 429 usando Retry-After, y mantente por debajo de los límites de producción.
  10. Credenciales. Guarda la contraseña inicial la primera vez que la lees; solo se puede leer una vez. La lectura exige el permiso manage; no existe la ruta HEAD.
  11. Renovaciones. Decide tu modo de auto_renew y lee billing para las fechas reales en lugar de fijar los valores por defecto.
  12. Empieza poco a poco: una primera orden real con un plan barato y luego escala.

Todo lo demás, incluida la referencia completa de endpoints, los códigos de error y los ejemplos de código, está en la guía completa.

Artículos relacionados

Preguntas frecuentes

¿El sandbox de la API de revendedor es gratis?

Sí. Usa una cuenta virtual: no se cobra nada, no se aprovisiona ningún servidor y no se toca ninguna orden, factura ni tarjeta reales.

¿Cómo obtengo una llave del sandbox?

La creas tú mismo en el panel del cliente, en API y webhooks, sección de llaves de prueba (sandbox). Puedes tener hasta 3 llaves de sandbox activas; no necesitan aprobación del equipo ni direcciones IP.

¿Cómo pruebo una tarjeta rechazada o un servicio suspendido?

Envía la cabecera X-Sandbox-Scenario (por ejemplo card_declined) para forzar un error en una petición, y llama a POST /sandbox/v1/time/advance para mover el reloj virtual y que corran las renovaciones, la suspensión y la baja.

¿Qué no simula el sandbox?

La aprobación del equipo y la lista de IPs permitidas, el procesamiento real de tarjetas y el 3-D Secure, PayPal y cripto, los tiempos reales de aprovisionamiento, las comprobaciones antifraude y el stock real. Lee la sección de diferencias antes de pasar a producción.

¿Te fue útil este artículo?
Sandbox de la API de revendedor: prueba sin gastar dinero