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.
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/advancepara saltar en el tiempo,POST /resetpara empezar de cero, yPOST /payment-method/setup/DELETE /payment-methodpara 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:
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:
- Entra en API y webhooks y abre la pestaña Claves de API.
- 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. - 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:
# 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-methodresponde{ "payment_method": null }y una cotización lista solobalanceenpayable_with.payment: "card"responde402 requires_actioncondetails.reasonno_card(sin tarjeta) ono_mandate(tarjeta sin el mandato).payment: "auto"paga con el saldo cuando alcanza para el total; si no, responde402 insufficient_balancecondetails.cardigual al mismo motivo.auto_renew: "card"o"balance_and_card"(en una orden o conPUT /services/:id/auto-renew) responde409 conflictcondetails.reasonno_default_cardomandate_not_accepted.- El cobro automático con tarjeta durante
POST /time/advancesolo 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):
# 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).
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. |
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" }
}
}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/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 }
}
}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.
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 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 recibesinvoice.created,invoice.paidyservice.renewed.cardcobra la tarjeta virtual;balance_and_cardprueba 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_actionoinsufficient_balanceenviado en la propia peticiónPOST /time/advance, en un modo que usaría ese método): la factura pasa aoverduede inmediato y se emiteinvoice.overdueuna vez, confailed_attemptsen sus datos. El siguiente intento se programa con el siguiente valor dereintentosHoras. 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: balancecon un saldo que no cubre la factura): no se cobra nada y la factura sigueopenhasta la fecha de renovación, así que aún tienes tiempo de pagarla a mano. Después pasa aoverduey el servicio se suspende, igual que conauto_renew: off. - Pagas estando suspendido (
/renewo/invoices/:id/pay):invoice.paid,service.renewed,service.unsuspended. - Renuevas demasiado pronto (
POST /services/:id/renewantes de que abra la ventana de renovación): no se cobra nada. Responde200conalready_paid: truey la última factura pagada (onull), como en producción. Dentro de la ventana crea la factura de renovación si hace falta y la paga. POST /services/:id/cancelconat: period_end:service.cancel_scheduled; no hay factura de renovación; al final del periodo el servicio se termina (service.terminatedconreason: cancelled_at_period_end).POST /services/:id/cancel/withdrawanula la programación y cuenta en la cubetacancel(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
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"
}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 aorder.paid, atest.order.paido a*. Un webhook suscrito solo a nombrestest.<evento>recibe entregas del sandbox y nunca una de producción. El nombre va en el sobre (evento) y en la cabeceraSolume-Event. - Cabeceras.
Solume-Livemode: false(producción envíatrue), además deSolume-SignatureySolume-Deliverycomo siempre. - Payload.
"livemode": falsedentro dedatos. - Firma con un secreto derivado. La firma se arma como en producción (
Solume-Signature: t=<segundos unix>,v1=<hex>, dondev1es 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 realwhsec_. 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:
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/advancegenera 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 enGET /events. - El mismo webhook, los dos entornos. Un webhook recibe eventos reales y del sandbox. Revisa
Solume-Livemodeolivemodepara 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:
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 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:
{ "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í:
{
"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.
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
}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
- Cambia la URL base a
https://api.solumecore.com/v1y la llave a tusk_live_. Mantén ambas en configuración, nunca en el código. - 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 (
purchaseexige topes) y las IPs públicas desde las que llaman tus servidores: una llave real solo funciona desde IPs autorizadas. - 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-methoden tu llave real: necesitaschargeable_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 con402 requires_action(no_cardono_mandate) yauto_renew: cardcon409 conflict. - Recarga el saldo real si piensas pagar con saldo.
- 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_allowedpara una llave usada desde una IP que no está en su lista,maintenance,card_declinedcondetails.reasoninsufficient_fundsostripe_disabled, y el503cuando cae una dependencia distinta de Redis. - Idempotencia. Genera una
Idempotency-Keypor operación de negocio y reutilízala en los reintentos tras un timeout o un 5xx. - Trabajo asíncrono. Maneja las tareas
202y los tiempos de aprovisionamiento largos. El aprovisionamiento real es más lento que 10 segundos, no lo des por hecho. - Webhooks. En producción los eventos tienen su nombre real (sin prefijo
test.), se firman con tu secreto realwhsec_(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 ignorarlivemode: falseen la lógica de producción. Registra al menos un webhook antes de depender deGET /events.service.provisioned,task.succeededytask.failedtambién existen en producción, pero sigue consultandoGET /services/:idyGET /tasks/:idcomo respaldo por si se pierde una entrega. - Retrocede ante un
429usandoRetry-After, y mantente por debajo de los límites de producción. - 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 rutaHEAD. - Renovaciones. Decide tu modo de
auto_renewy leebillingpara las fechas reales en lugar de fijar los valores por defecto. - 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
- Guía de la API de revendedor
- Sandbox de la API de revendedor
- Claves de API de la cuenta
- Webhooks y eventos de la cuenta
- Qué API de SolumeCore usar
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.