API de revendedor de SolumeCore: guía, referencia y ejemplos
Compra, opera y renueva servidores VPS y VDS por API: quickstart con curl, referencia de endpoints, errores, límites, facturación, webhooks y OpenAPI.
Revende servidores VPS y VDS desde tus propios sistemas. Con la API de revendedor consultas el catálogo, cotizas y compras servidores, los operas (energía, reinstalación, contraseñas, red, llaves SSH, firewall), pagas renovaciones, lees facturas y recibes eventos, todo por HTTPS y JSON.
Esta es la guía completa: conceptos, guía rápida, una referencia de cada endpoint, errores, límites, reglas de facturación, webhooks, buenas prácticas y el registro de cambios. El contrato legible por máquinas es el archivo OpenAPI (producción) y el archivo OpenAPI del sandbox. Una guía más corta del entorno de pruebas gratuito está en la guía del sandbox.
Resumen
- Para quién es. Revendedores de hosting, paneles, sistemas de facturación y automatizaciones que venden o administran servidores VPS y VDS. Los servidores de juegos, el hosting web compartido y las aplicaciones no se pueden revender con esta API: un plan de otro tipo se rechaza con
product_not_resellable. - Usarla no cuesta nada. No hay cargo por la API, por las llamadas ni por las llaves. Pagas el precio normal de cada servicio que compras o renuevas, menos el descuento acordado para tu llave, más el recargo de tarjeta acordado para tu llave cuando se cobra una tarjeta. Ambos valen 0% por defecto y se acuerdan con cada revendedor; cada cotización y cada factura muestra cada línea.
- URL base.
| Entorno | URL base | Prefijo de la llave |
|---|---|---|
| Producción | https://api.solumecore.com/v1 |
sk_live_ |
| Sandbox | https://api.solumecore.com/sandbox/v1 |
sk_test_ |
- La misma API en ambos. El sandbox tiene las mismas rutas, cuerpos, errores y cabeceras sobre una cuenta virtual privada (consulta Sandbox). Pasar a producción es cambiar la URL base y la llave.
- Legible por máquinas. Toda respuesta es JSON en UTF-8 con nombres de campo en inglés y
snake_case. El dinero siempre va en centavos enteros.
Primeros pasos
Crea una llave. En el panel del cliente entra en API y webhooks, abre Claves de API y pulsa Nueva clave. El secreto (
sk_live_...) se muestra una sola vez; guárdalo en un gestor de secretos. Una llave nueva solo puede leer y administrar servicios hasta que el personal conceda más permisos. Puedes tener hasta 10 llaves reales activas; revoca una para crear otra.Pide el acceso que necesitas. Abre un ticket desde el panel (el botón Pedir que autoricen IPs lo prepara) e indica al equipo:
- las direcciones IP públicas desde las que llamará tu código. Una llave solo funciona desde las direcciones IP que el personal registró para ella; cualquier otra dirección recibe
403 ip_not_allowed; - si quieres comprar y renovar por API (permiso
purchase) y con qué topes de gasto: máximo por orden, máximo por 24 horas móviles y órdenes por hora móvil; - si quieres administrar el saldo y la renovación automática por API (permiso
billing); - el descuento y el recargo de tarjeta que has acordado.
Solo el personal puede conceder permisos, topes e IPs. Puedes ver lo que se concedió a tu llave en el panel (Qué pueden hacer tus llaves) y en
GET /v1/me.- las direcciones IP públicas desde las que llamará tu código. Una llave solo funciona desde las direcciones IP que el personal registró para ella; cualquier otra dirección recibe
Configura el pago en el sitio web. La API cobra primero tu saldo y después tu tarjeta guardada predeterminada, fuera de sesión. Guarda la tarjeta y acepta el mandato de cobro automático vigente (versión 2026-11-v2) en Facturación, dentro del panel; no se puede hacer por la API. Recarga el saldo en el panel o con
POST /v1/balance/top-ups. La lista completa está en Requisitos antes de comprar por API.Prueba todo primero en el sandbox. Crea una llave de pruebas (
sk_test_...) en la misma página; no necesita aprobación ni IPs. Consulta Sandbox.Haz tu primera llamada.
export SOLUME_KEY=sk_live_xxxxxxxx
curl https://api.solumecore.com/v1/me -H "Authorization: Bearer $SOLUME_KEY"Permisos (scopes)
Cada llave tiene un conjunto de permisos concedidos por el personal.
| Scope | Permite |
|---|---|
read |
Catálogo, cotizaciones, servicios, tareas, métricas, facturas, órdenes, eventos |
manage |
Energía, reinstalación, restablecer contraseña, leer las credenciales de un solo uso, hostname, rDNS, llaves SSH, firewall, cancelar y retirar la cancelación |
purchase |
Todo lo que gasta dinero: crear órdenes, renovar, pagar facturas |
billing |
Saldo, recargas, información del método de pago, modo de renovación automática |
Una llamada sin el permiso necesario responde 403 insufficient_scope con details.required_scope, antes de que ocurra cualquier otra cosa. Crear una orden con auto_renew distinto de off también necesita billing.
Topes de gasto
purchase solo se concede junto con topes. Están en centavos de la moneda de la cuenta y se comprueban antes de cobrar (el importe se reserva y se libera si el pago falla, de modo que las llamadas en paralelo no pueden pasarse del tope):
| Tope | Significado | Error al excederlo |
|---|---|---|
per_order_cents |
Máximo por una orden, renovación o pago | 403 spending_cap_exceeded, details.cap = "per_order" |
daily_cents |
Máximo en 24 horas móviles | 403 spending_cap_exceeded, details.cap = "daily" |
orders_per_hour |
Órdenes en una hora móvil (10 si no se define) | 403 spending_cap_exceeded, details.cap = "hourly_orders" |
Una llave con purchase y sin topes cargados no puede comprar: 403 spending_caps_not_set. Como máximo puede haber 3 órdenes en curso por llave; si hay más, responde 429 too_many_in_flight. GET /v1/me muestra los topes y tu consumo (spent_today_cents, orders_last_hour), y POST /v1/quotes te indica si una orden excedería un tope (would_exceed_cap).
Requisitos antes de comprar por API
Comprar, renovar o pagar una factura por API exige todo lo siguiente. Revisa cada punto antes de salir a producción; GET /v1/me y GET /v1/payment-method muestran casi todos.
| Requisito | Quién lo hace | Cómo comprobarlo | Qué recibes si falta |
|---|---|---|---|
| Una dirección IP autorizada para la llave | El personal de Solume, tras tu ticket | GET /me: allowed_ips_count mayor que 0 |
403 ip_not_allowed en cada llamada |
El permiso purchase (y billing para cambiar la renovación automática o recargar) |
El personal de Solume | GET /me: scopes |
403 insufficient_scope |
| Topes de gasto cargados: por orden y por día (las órdenes por hora son 10 por defecto) | El personal de Solume | GET /me: can_purchase: true y caps |
403 spending_caps_not_set o 403 spending_cap_exceeded |
| Una tarjeta predeterminada guardada con el mandato de cobro automático, versión 2026-11-v2, aceptado para esa tarjeta | Tú, en el panel (Facturación) | GET /payment-method: chargeable_off_session: true |
402 requires_action con details.reason no_card o no_mandate |
Saldo para el total, si pagas con payment: "balance" o quieres evitar la tarjeta |
Tú, en el panel o con POST /balance/top-ups |
GET /balance |
402 insufficient_balance |
| La orden pasa las comprobaciones de confianza y antifraude del sitio web | Automático | n/a | 403 order_rejected |
Sobre el mandato: es la autorización que da el titular de la cuenta, en el sitio web, para cobrar una tarjeta guardada sin estar presente. La versión 2026-11-v2 cubre renovaciones, pagos de facturas y compras iniciadas por tus llaves de API, dentro de los topes; un mandato anterior (versión 2026-10) solo cubría renovaciones y recargas automáticas, así que una tarjeta guardada con él no se puede cobrar en compras por API hasta que el titular acepte el texto nuevo. mandate_accepted en GET /payment-method indica que hay aceptado, para esa tarjeta, un mandato de una versión que cubre las compras por API (2026-11-v2; uno anterior no cuenta aunque exista); chargeable_off_session indica que la API puede usarlo de verdad (esa versión, cobros no pausados por el titular y cobros automáticos habilitados en toda la plataforma). Los dos reflejan lo que exigen POST /orders, POST /invoices/{id}/pay y POST /services/{id}/renew con payment: "card" o "auto". Una diferencia: auto_renew: "card" / "balance_and_card" solo pide un mandato válido para renovaciones (cualquier versión aceptada), así que una cuenta que aún tiene el mandato anterior puede activarlo aunque mandate_accepted sea false; esas renovaciones las cobra el proceso de renovación automática, no una compra por API. Con solo saldo no necesitas la tarjeta, pero toda orden con payment: "auto" que el saldo no cubra fallará con 402.
Dos casos más: auto_renew: "card" o "balance_and_card" (en POST /orders o PUT /services/{id}/auto-renew) también comprueba que existan una tarjeta predeterminada y un mandato aceptado, y responde 409 conflict con details.reason no_default_card o mandate_not_accepted; y payment: "card" mientras los pagos con tarjeta están desactivados responde 402 card_declined con details.reason: "stripe_disabled".
Autenticación y entornos
Envía la llave en la cabecera Authorization en cada petición:
Authorization: Bearer sk_live_xxxxxxxx- Las llaves reales (
sk_live_) funcionan en/v1solo desde IPs registradas. Las llaves del sandbox (sk_test_) funcionan solo en/sandbox/v1, desde cualquier IP. - Una llave en el entorno equivocado responde
403 wrong_environment. - Una llave desconocida o revocada responde
401 unauthorized(ambos casos se ven igual, a propósito). Una cuenta suspendida o cerrada responde403 unauthorized. - Las autenticaciones fallidas se limitan por IP (20 por minuto por defecto) antes incluso de buscar la llave; pasado ese límite recibes
429 rate_limited. - Las llaves son solo para el titular de la cuenta; no hay delegación. Una llave solo ve los servicios, órdenes y facturas de su propia cuenta: un id de servicio que pertenece a otra persona responde el mismo
404 not_foundque uno que no existe.
Convenciones
| Tema | Regla |
|---|---|
| Formato | JSON de entrada y de salida, UTF-8. Content-Type: application/json en POST y PUT con cuerpo. Cuerpo máximo de 32 KB. |
| Entrada estricta | Los campos desconocidos en un cuerpo o en la cadena de consulta se rechazan con 400 invalid_request. |
| Ids | El id de un servicio es su código público (por ejemplo K3F8QX2P). Las órdenes, facturas, tareas, eventos y llaves SSH usan ids con prefijo: ord_..., inv_..., tsk_..., evt_..., key_.... Los ids internos nunca se exponen. |
| Dinero | { "amount_cents": 720, "currency": "USD" }: un entero en unidades menores, nunca un decimal. |
| Fechas | ISO 8601 en UTC, por ejemplo 2026-11-05T12:00:00.000Z. Una fecha que no aplica es null. |
| Paginación | ?limit= (1 a 100, por defecto 25) y ?cursor=. Las listas responden { "data": [...], "next_cursor": "..." | null }. Los cursores son opacos: devuelve lo que recibiste. Un cursor null es la última página. |
| Id de la petición | Toda respuesta lleva la cabecera X-Request-Id (también en error.request_id). Regístralo y cítalo al contactar con soporte. |
| Cabeceras de límite | Toda respuesta incluye X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset (segundos Unix) de la cubeta general. Un 429 incluye además Retry-After (segundos). |
| Trabajo asíncrono | Todo lo que se ejecuta en la plataforma de servidores responde 202 con { "task": { "id": "tsk_...", "status": "queued" } }. Consulta GET /v1/tasks/{id}. |
| Idempotencia | Mira abajo. |
Idempotencia
Envía Idempotency-Key (de 8 a 64 caracteres: letras, dígitos, _ o -) en toda llamada que mueva dinero o sea destructiva. Es obligatoria en:
POST /orders, POST /services/{id}/renew, POST /services/{id}/reinstall, POST /services/{id}/reset-password, POST /services/{id}/cancel, POST /invoices/{id}/pay y POST /balance/top-ups.
Cómo funciona:
- Misma clave y misma petición dentro de 24 horas: se repite la respuesta guardada con la cabecera
Idempotent-Replay: truey no se ejecuta nada de nuevo. - Misma clave con una petición distinta:
422 idempotency_key_reused. Esto también vale cuando la primera orden se recuperó tras una llamada interrumpida: la clave sigue ligada a la petición original. - Misma clave mientras la primera llamada sigue ejecutándose:
409 request_in_progress(reintenta en breve). - Clave ausente:
400 idempotency_key_required. Formato incorrecto:400 invalid_request. - Las claves son por llave de API. Genera una por intención (por ejemplo
order-<id-de-tu-orden>), reutilízala al reintentar y nunca la reutilices para otra compra. - Si la llamada falla con un error (por ejemplo
card_declined) o con un5xx, la clave se libera, de modo que puedes reintentar con la misma clave tras corregir la causa. - Las respuestas guardadas nunca contienen contraseñas ni otros secretos.
- Las rutas que devuelven contraseñas (
GET /credentials) no usan idempotencia.
Errores
Toda respuesta que no sea 2xx tiene esta forma. code es estable y está documentado; message es texto para personas y puede cambiar, así que nunca tomes decisiones según él.
{
"error": {
"code": "insufficient_scope",
"message": "This API key does not have the \"purchase\" scope.",
"request_id": "req_5b1c0e7d2a9f4c3b8e16",
"details": { "required_scope": "purchase" }
}
}Códigos de error
| Código | HTTP | Causa | Qué hacer |
|---|---|---|---|
invalid_request |
400 (también 413, 4xx de validación) | Un campo mal formado o desconocido, un cursor incorrecto, un cuerpo de más de 32 KB, un formato incorrecto de Idempotency-Key, un valor erróneo. details puede listar los problemas. |
Corrige la petición. No la reintentes sin cambios. |
idempotency_key_required |
400 | Se llamó a una ruta que necesita Idempotency-Key sin ella. |
Añade la cabecera. |
unauthorized |
401 (403 para una cuenta inactiva) | Llave ausente, desconocida o revocada; o la cuenta está suspendida o cerrada. | Revisa la llave; contacta con soporte si la cuenta está inactiva. |
ip_not_allowed |
403 | La IP que llama no está registrada para esta llave (details.ip), o la IP está bloqueada. |
Pide a soporte que autorice la IP. Comprueba que no llamas a través de un proxy o desde otra IP de salida. |
wrong_environment |
403 | Una llave del sandbox en /v1, o una llave real en /sandbox/v1. |
Usa la URL base que corresponde. |
insufficient_scope |
403 | A la llave le falta un permiso (details.required_scope). Leer las credenciales necesita manage, no read. |
Pide el permiso a soporte. |
spending_caps_not_set |
403 | La llave tiene purchase pero no tiene topes cargados. |
Pide a soporte que configure los topes. |
spending_cap_exceeded |
403 | Se excedió una orden, el total de 24 h móviles, el número de órdenes por hora o el límite mensual que el titular de la cuenta fijó en ella (details.cap: per_order, daily, hourly_orders, account_monthly; y remaining_cents donde aplica). |
Espera a que pase la ventana, compra menos o pide topes más altos. |
product_not_resellable |
403 | El plan no es un plan VPS/VDS. | Usa ids de plan de GET /catalog/plans. |
order_rejected |
403 | La orden no pasó las comprobaciones de confianza, antifraude y antiabuso que también se aplican en el sitio web. El mensaje es genérico a propósito. | Contacta con soporte; no reintentes en bucle. |
not_found |
404 | El recurso no existe o no es tuyo; también un plan, ubicación o plantilla desconocidos. | Revisa el id. |
credentials_not_available |
404 | Esta llave no compró ni restableció el servicio, o pasó la ventana de 24 h (details.reason). |
Llama a reset-password para obtener una contraseña nueva. |
conflict |
409 | La petición entra en conflicto con el estado actual. details.reason indica el motivo (consulta abajo). |
Lee details.reason. |
request_in_progress |
409 | La misma Idempotency-Key sigue ejecutándose, o una orden asociada a ella aún se está procesando. |
Espera y reintenta con la misma clave; para órdenes, consulta GET /orders/{id} si details.order_id está presente. |
out_of_stock |
409 | No hay stock en esa ubicación, o la orden se pagó pero no se pudo entregar y se reembolsó (details.refunded_to). |
Elige otra ubicación u otro plan. |
service_not_ready |
409 | El servicio todavía se está aprovisionando, o una reinstalación no ha terminado. Cancelar con at: "now" en ese estado también lo responde. |
Consulta el servicio y reintenta cuando esté active. |
service_suspended |
409 | El servicio está suspendido. Cancelar con at: "now" un servicio suspendido, en pausa o con error también lo responde: usa at: "period_end" o contacta con soporte. |
Paga la factura abierta (POST /services/{id}/renew). |
firewall_unavailable |
409 / 503 | 409: el servicio no tiene firewall (motor none), no tiene IP, o es una entrega simulada. 503: el proveedor del firewall falló (una lectura fallida nunca se devuelve como lista vacía), está caído o sin configurar. |
409: no hay nada que hacer. 503: reintenta más tarde. |
idempotency_key_reused |
422 | La clave se usó con una petición distinta. | Usa una clave nueva para una petición nueva. |
invalid_option |
422 | Una opción desconocida, una opción repetida, o una opción sin precio para ese ciclo. | Revisa GET /catalog/options?plan=. |
feature_not_supported |
422 | La ruta pertenece a otro motor de firewall (details.engine es el motor del servicio). |
Lee primero GET /services/{id}/firewall. |
credentials_consumed |
410 | La contraseña de un solo uso ya se leyó. | Llama a POST /services/{id}/reset-password. |
requires_action |
402 | La tarjeta necesita al cliente. details.reason: authentication_required (3-D Secure), no_card (no hay tarjeta predeterminada guardada) o no_mandate (el mandato versión 2026-11-v2 no está aceptado para esa tarjeta, o los cobros automáticos están pausados o desactivados). No se creó ni se cobró nada. |
Pide al cliente que lo corrija en el panel (Facturación: guardar la tarjeta, aceptar el mandato) o que se autentique, o recarga el saldo y usa payment: "balance". |
card_declined |
402 | La tarjeta fue rechazada (details.reason: declined o insufficient_funds), o los pagos con tarjeta están desactivados (stripe_disabled). No se creó ni se cobró nada. |
Pide al cliente que actualice la tarjeta; reintenta con balance. |
insufficient_balance |
402 | El saldo no cubre el total (details.required_cents). Con payment: "auto" también aparece cuando el saldo no alcanza y no se puede usar ninguna tarjeta (details.card indica el motivo). |
Recarga el saldo. |
rate_limited |
429 | Una cubeta está llena (details.bucket, details.retry_after). |
Espera Retry-After segundos y reintenta. Usa retroceso exponencial. |
too_many_in_flight |
429 | Tres órdenes de la llave todavía se están creando. | Espera a que terminen (Retry-After: 15). |
maintenance |
503 | El nodo bloquea ahora mismo las acciones del panel. | Reintenta más tarde. |
internal_error |
500 (502 al leer la tarjeta; 503 cuando la plataforma no está disponible temporalmente) | Un error inesperado. Tras un cobro con tarjeta en una renovación o un pago, puede significar que no se pudo confirmar el resultado. Un 503 (mensaje "The service is temporarily unavailable") llega con Retry-After: 2 cuando una dependencia de la plataforma no responde. |
Cita el request_id. Ante un 503 reintenta tras Retry-After (conserva la misma Idempotency-Key). Antes de reintentar un pago tras un 500, lee la factura (GET /invoices/{id}) o la orden. |
409 conflict incluye details.reason en los casos que no tienen código propio: service_cancelled, service_error, service_paused, cancellation_scheduled, no_billing_cycle, grouped_invoice (págala con POST /invoices/{id}/pay), invoice_not_payable, payment_in_progress (hay un cobro automático con tarjeta o un pago sin confirmar de esa factura pendiente; reintenta más tarde), no_default_card, mandate_not_accepted. Un 409 al cancelar significa que el servicio ya está cancelado, es un subservidor, o es un servicio permanente o de cortesía (con at: "now").
Límites de tasa
Los límites usan una ventana deslizante y se aplican además del límite general. Todos los valores los define Solume (y Solume puede cambiar los límites general y de energía para una llave concreta); GET /v1/me devuelve en rate_limits los límites exactos que se aplican a tu llave. Valores por defecto:
| Cubeta | Por defecto | Se cuenta por |
|---|---|---|
| general (todas las llamadas) | 120 / minuto | llave e IP |
cotizaciones (POST /quotes) |
30 / minuto | llave |
órdenes (intentos de POST /orders) |
30 / hora | llave |
| energía | 6 / minuto y 40 / hora | servicio, más 60 / minuto por llave |
| reinstalación | 1 / 10 minutos y 5 / día | servicio, más 20 / día por llave |
| reset-password | 3 / hora | servicio, más 20 / hora por llave |
| renovar y pagar factura | 5 / hora | servicio |
| ajustes del servicio (hostname, rDNS, llaves SSH, cambios de firewall) | 10 / hora | servicio |
| cancelar (y retirar la cancelación) | 3 / día | servicio |
| credenciales | 10 / hora | servicio |
service-reads (GET /services/{id}, /status, /metrics, GET /firewall y toda lectura de gsk) |
60 / minuto | servicio, más 300 / minuto por llave |
| autenticaciones fallidas | 20 / minuto | IP de origen |
Los límites por servicio cuentan todas las llaves e IPs de tu cuenta sobre ese servicio, así que añadir IPs no los aumenta. La cubeta service-reads protege los servidores y al proveedor del firewall del sondeo constante; se cuenta además de la cubeta general, y esas llamadas también caducan a los 8 segundos. GET /me todavía no lista service-reads. Las compras por hora también están limitadas por el tope orders_per_hour de la llave (10 por defecto). Superar un límite responde 429 rate_limited con Retry-After.
Guía rápida
Toda la vida de un servidor con curl. Sustituye los ids por los de tu propio catálogo. Los ejemplos están recortados; tienen las mismas formas que documenta el archivo OpenAPI, y cada ejemplo de ese archivo se valida contra el esquema de la respuesta real mediante una prueba automatizada. Puedes ejecutar el mismo flujo en el sandbox cambiando la URL base y la llave.
export API=https://api.solumecore.com/v1
export SOLUME_KEY=sk_live_xxxxxxxx
alias solume='curl -sS -H "Authorization: Bearer $SOLUME_KEY" -H "Content-Type: application/json"'1. Comprueba tu llave, tu IP y tu método de pago
solume $API/me
solume $API/payment-method
solume $API/balance/me muestra scopes, caps, usage, discount_percent y card_fee_percent. /payment-method debe mostrar "chargeable_off_session": true si quieres pagar con tarjeta:
{
"payment_method": {
"brand": "visa",
"last4": "4242",
"exp_month": 12,
"exp_year": 2029,
"mandate_accepted": true,
"chargeable_off_session": true
}
}Si es null o mandate_accepted es false, guarda la tarjeta y acepta los cobros automáticos en Facturación, dentro del panel.
2. Consulta el catálogo
solume "$API/catalog/plans?type=vps"
solume $API/catalog/locations
solume "$API/catalog/os-templates?plan=cm3x7k2p00001vps2gb"
solume "$API/catalog/options?plan=cm3x7k2p00001vps2gb"Un plan muestra sus specs, los prices por ciclo (monthly, quarterly, semiannual, annual) con list_price (sitio web) y your_price (con tu descuento), y available.
3. Cotiza
Una cotización no tiene efectos secundarios y usa el mismo cuerpo que la orden:
solume -X POST $API/quotes -d '{
"plan_id": "cm3x7k2p00001vps2gb",
"location_id": "us-mia-1",
"billing_cycle": "monthly",
"os_template_id": "cm3x7k2p00002ubuntu2404",
"hostname": "web-1",
"options": [{ "slug": "anti-ddos", "answer": "yes" }]
}'{
"quote": {
"lines": [
{
"kind": "plan",
"description": "VPS Small - monthly",
"amount": { "amount_cents": 800, "currency": "USD" }
},
{
"kind": "option",
"slug": "anti-ddos",
"description": "Anti-DDoS",
"amount": { "amount_cents": 300, "currency": "USD" }
},
{
"kind": "reseller_discount",
"description": "Reseller discount (10%)",
"amount": { "amount_cents": -110, "currency": "USD" }
}
],
"total": { "amount_cents": 990, "currency": "USD" },
"card_fee": { "percent": 3, "amount": { "amount_cents": 30, "currency": "USD" } },
"total_with_card": { "amount_cents": 1020, "currency": "USD" },
"payable_with": ["balance", "card"],
"would_exceed_cap": false
}
}(Otros campos: plan, location, hostname, billing_cycle, currency, list_total, reseller_discount, subtotal, tax, cap, cap_remaining_cents.) total es el precio pagando con saldo; el recargo de tarjeta es una línea aparte que se añade solo cuando el pago termina en la tarjeta.
4. Compra
solume -X POST $API/orders -H "Idempotency-Key: order-acme-42-001" -d '{
"plan_id": "cm3x7k2p00001vps2gb",
"location_id": "us-mia-1",
"billing_cycle": "monthly",
"os_template_id": "cm3x7k2p00002ubuntu2404",
"hostname": "web-1",
"ssh_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl me@laptop",
"options": [{ "slug": "anti-ddos", "answer": "yes" }],
"payment": "auto",
"metadata": { "customer": "acme-42" }
}'201 responde la orden y el servicio nuevo en provisioning:
{
"order": {
"id": "ord_9173eb967115608b2f",
"status": "provisioning",
"total": { "amount_cents": 720, "currency": "USD" },
"payment_method": "balance",
"service_id": "K3F8QX2P",
"auto_renew": "off"
},
"service": {
"id": "K3F8QX2P",
"hostname": "web-1",
"status": "provisioning",
"power_state": null,
"ips": []
}
}(Recortado. Las órdenes también incluyen lines, subtotal, tax, billing_cycle, invoice_number, invoice_id (el id inv_… que usan GET /invoices/:id y POST /invoices/:id/pay), metadata...) La respuesta nunca contiene credenciales. Si reintentas con la misma Idempotency-Key obtienes la misma respuesta con Idempotent-Replay: true, y no se cobra dos veces. metadata (hasta 10 claves, 200 caracteres por valor) se guarda en la orden y en el servicio y se devuelve en todas partes, de modo que puedes asociarla a tus propios registros.
Los errores de pago no crean nada: 402 requires_action, 402 card_declined, 402 insufficient_balance.
5. Espera a que el servicio esté active
solume $API/services/K3F8QX2PConsulta cada pocos segundos (espaciando hasta 10 a 30 segundos) hasta que status sea active (error significa que el aprovisionamiento falló y la orden se reembolsa; revisa GET /orders/{id}). Una vez activo, power_state es running e ips contiene las direcciones.
6. Lee las credenciales una sola vez
solume $API/services/K3F8QX2P/credentials{ "username": "root", "password": "wm^j&Xt&=L!bkZEK&M2A", "expires_at": "2026-11-06T12:00:11.000Z" }La contraseña está disponible durante 24 horas tras el aprovisionamiento y se consume con la primera lectura correcta: una segunda lectura responde 410 credentials_consumed. Guárdala de inmediato en tu almacén de secretos y no la registres en logs. La respuesta lleva Cache-Control: no-store. Solo la llave que compró el servicio (o que pidió un restablecimiento o una reinstalación) puede leerla, y necesita el permiso manage (una llave solo con read recibe 403 insufficient_scope). Esta ruta no tiene versión HEAD, así que nada puede consumir la contraseña por accidente. Los servidores Windows usan el usuario Administrator.
7. Opera el servidor
# reiniciar: 202 + tarea
solume -X POST $API/services/K3F8QX2P/power/restart
solume $API/tasks/tsk_883e0ff8c42d09b420
# estado en vivo y métricas
solume $API/services/K3F8QX2P/status
solume "$API/services/K3F8QX2P/metrics?range=24h"Una tarea pasa por queued, running y luego succeeded o failed (con error.code).
8. Renueva
solume -X POST $API/services/K3F8QX2P/renew -H "Idempotency-Key: renew-K3F8QX2P-2026-12" -d '{"payment":"auto"}'Paga la factura de renovación abierta (creándola si la ventana de renovación ya abrió) con los mismos topes y el mismo orden de pago que las órdenes. Consulta Facturación y ciclo de vida.
9. Reinstala o cambia el sistema operativo
solume -X POST $API/services/K3F8QX2P/reinstall -H "Idempotency-Key: reinstall-K3F8QX2P-001" \
-d '{"os_template_id":"cm3x7k2p00002ubuntu2404","confirm":true}'Esto borra el disco, por lo que se exige confirm: true. Devuelve una tarea; cuando termine con éxito, lee la contraseña nueva con GET /credentials (una reinstalación genera una nueva).
10. Restablece la contraseña
solume -X POST $API/services/K3F8QX2P/reset-password -H "Idempotency-Key: reset-K3F8QX2P-001"
# cuando la tarea termine con éxito:
solume $API/services/K3F8QX2P/credentials11. Firewall
solume $API/services/K3F8QX2P/firewall
solume -X POST $API/services/K3F8QX2P/firewall/geoblock -d '{"country":"CN"}'Las operaciones disponibles dependen del motor de firewall del servicio; consulta Firewall.
12. Cancela
# al final del periodo pagado (se puede retirar)
solume -X POST $API/services/K3F8QX2P/cancel -H "Idempotency-Key: cancel-K3F8QX2P-001" -d '{"at":"period_end","confirm":true}'
solume -X POST $API/services/K3F8QX2P/cancel/withdrawat: "now" es destructivo y no se puede retirar, y solo funciona en un servicio active: mientras se aprovisiona o se reinstala recibes 409 service_not_ready, y con un servicio suspendido, en pausa o con error 409 service_suspended (una suspensión por impago o del personal no se evita cancelando). at: "period_end" siempre es posible. confirm: true es obligatorio en ambos casos.
El mismo flujo en Node.js y Python
Compra, espera a que esté active y lee las credenciales una sola vez.
const API = 'https://api.solumecore.com/v1';
const KEY = process.env.SOLUME_KEY; // sk_live_... (o sk_test_... con /sandbox/v1)
async function call(method, path, body, idempotencyKey) {
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch(API + path, {
method,
headers: {
Authorization: `Bearer ${KEY}`,
...(body ? { 'Content-Type': 'application/json' } : {}),
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
if (res.status === 429 || res.status >= 500) {
const wait = Number(res.headers.get('retry-after')) || 2 ** attempt;
await new Promise((r) => setTimeout(r, wait * 1000));
continue; // seguro: misma Idempotency-Key
}
const json = await res.json();
if (!res.ok)
throw Object.assign(new Error(json.error.code), {
response: json,
requestId: res.headers.get('x-request-id'),
});
return json;
}
throw new Error('too many retries');
}
const { service } = await call(
'POST',
'/orders',
{
plan_id: 'cm3x7k2p00001vps2gb',
location_id: 'us-mia-1',
billing_cycle: 'monthly',
os_template_id: 'cm3x7k2p00002ubuntu2404',
hostname: 'web-1',
payment: 'auto',
},
'order-acme-42-001',
);
let current = service;
while (current.status === 'provisioning') {
await new Promise((r) => setTimeout(r, 5000));
current = await call('GET', `/services/${service.id}`);
}
if (current.status !== 'active') throw new Error(`provisioning ended as ${current.status}`);
const { username, password } = await call('GET', `/services/${service.id}/credentials`);
// Guarda la contraseña en tu gestor de secretos AHORA: una segunda lectura responde 410.
console.log(username, current.ips[0].address);import os, time, requests
API = "https://api.solumecore.com/v1"
KEY = os.environ["SOLUME_KEY"] # sk_live_... (o sk_test_... con /sandbox/v1)
def call(method, path, body=None, idempotency_key=None):
headers = {"Authorization": f"Bearer {KEY}"}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
for attempt in range(5):
res = requests.request(method, API + path, json=body, headers=headers, timeout=30)
if res.status_code == 429 or res.status_code >= 500:
time.sleep(int(res.headers.get("Retry-After", 2 ** attempt)))
continue # seguro: misma Idempotency-Key
if not res.ok:
err = res.json()["error"]
raise RuntimeError(f'{err["code"]} (request {err["request_id"]})')
return res.json()
raise RuntimeError("too many retries")
service = call("POST", "/orders", {
"plan_id": "cm3x7k2p00001vps2gb",
"location_id": "us-mia-1",
"billing_cycle": "monthly",
"os_template_id": "cm3x7k2p00002ubuntu2404",
"hostname": "web-1",
"payment": "auto",
}, "order-acme-42-001")["service"]
while service["status"] == "provisioning":
time.sleep(5)
service = call("GET", f'/services/{service["id"]}')
if service["status"] != "active":
raise RuntimeError(f'provisioning ended as {service["status"]}')
creds = call("GET", f'/services/{service["id"]}/credentials')
# Guarda creds["password"] en tu gestor de secretos AHORA: una segunda lectura responde 410.
print(creds["username"], service["ips"][0]["address"])Referencia de endpoints
R lectura, M administración, P compra, B facturación. "Idem" significa que Idempotency-Key es obligatoria. Toda ruta también puede responder 400, 401, 403, 429 y 500 como se describe en Errores; una ruta con {id} responde 404 not_found para un servicio que no es tuyo o que no existe. La columna de límite es la cubeta que cuenta además de la general (consulta Límites de tasa).
Cuenta
| Método y ruta | Scope | Límite | Notas |
|---|---|---|---|
GET /me |
any | La llave (name, key_prefix, tag), scopes, can_purchase, caps, usage, discount_percent, card_fee_percent, currency, allowed_ips_count y los rate_limits que se aplican. |
|
GET /balance |
B | { "balance": { "amount_cents": 2500, "currency": "USD" } } |
|
POST /balance/top-ups |
B, Idem | Cuerpo { "amount_cents": 5000, "method": "card" | "paypal" | "crypto" }. 201 con una checkout_url alojada y expires_at; el cliente paga allí. Mismo mínimo, máximo y límites que la recarga del sitio web (un 400 los muestra). 409 conflict cuando ese método o las recargas no están disponibles. |
|
GET /payment-method |
B | La tarjeta predeterminada y si se puede cobrar fuera de sesión: { "payment_method": { brand, last4, exp_month, exp_year, mandate_accepted, chargeable_off_session } | null }. Solo lectura. |
|
GET /events?since=&type=&limit=&cursor= |
R | Alternativa de consulta a los webhooks. Consulta Webhooks y eventos. |
Catálogo (solo VPS y VDS)
| Método y ruta | Scope | Límite | Notas |
|---|---|---|---|
GET /catalog/plans?type=vps|vds&location= |
R | Planes con specs, prices por ciclo (list_price, your_price), available y availability (in_stock, out_of_stock, coming_soon). |
|
GET /catalog/locations |
R | id (úsalo como location_id), name, country, city (null por ahora), in_stock, coming_soon. |
|
GET /catalog/os-templates?plan= |
R | Plantillas válidas para ese plan: id, name, family (linux o windows), distro, version, min_ram_mb, default_user. plan es obligatorio. |
|
GET /catalog/options?plan= |
R | Opciones de compra (por ejemplo Anti-DDoS) con sus precios por ciclo: recurring, one_time y first_invoice, o null para un ciclo sin precio. |
|
POST /quotes |
R | cotizaciones | Mismo cuerpo que POST /orders. Devuelve el desglose completo sin efectos secundarios. Errores: 403 product_not_resellable, 404, 422 invalid_option. |
Órdenes
| Método y ruta | Scope | Límite | Notas |
|---|---|---|---|
POST /orders |
P, Idem | órdenes | Consulta el cuerpo más abajo. 201 { order, service }. |
GET /orders/{id} |
R | { order, service }; el servicio es null hasta que existe. |
|
GET /orders?limit=&cursor= |
R | Tus órdenes por API, las más nuevas primero. |
Cuerpo de POST /orders y POST /quotes:
| Campo | Tipo | Notas |
|---|---|---|
plan_id |
string | De GET /catalog/plans. |
location_id |
string | El código de la ubicación, de GET /catalog/locations. Debe corresponder al plan. |
billing_cycle |
monthly, quarterly, semiannual, annual |
|
os_template_id |
string | De GET /catalog/os-templates?plan=. |
hostname |
string, 3 a 63 | Letras minúsculas, dígitos y guiones; no puede empezar ni terminar con -. |
ssh_public_key |
string, opcional | Una línea de llave pública. No junto con ssh_key_ids. |
ssh_key_ids |
array, opcional | Hasta 5 ids de llaves SSH que ya estén en tu cuenta. |
options |
array, opcional | [{ "slug": "anti-ddos", "answer": "yes" | "no" }], hasta 20. |
payment |
auto (por defecto), balance, card |
Consulta Pagos. |
auto_renew |
off (por defecto), balance, balance_and_card, card |
Cualquier valor distinto de off necesita billing. |
metadata |
object, opcional | Hasta 10 claves (letras, dígitos, _, ., -), valores de hasta 200 caracteres. Se devuelve tal cual. |
El status de una orden es pending, paid, provisioning, completed, failed o refunded. Errores de POST /orders: 402 (requires_action, card_declined, insufficient_balance), 403 (spending_cap_exceeded, spending_caps_not_set, order_rejected, product_not_resellable), 409 (out_of_stock, request_in_progress), 422 invalid_option, 429 too_many_in_flight.
Servicios
El objeto de servicio: id, hostname, status (provisioning, active, suspended, paused, cancelled, error), power_state (running, stopped, paused, unknown), plan (id, name, cores, ram_mb, disk_gb), location (id, name, country), os, ips (address, version, primary, rdns), created_at, origin (api o web), metadata y billing (consulta Facturación y ciclo de vida). Los elementos de las listas omiten power_state y billing.
| Método y ruta | Scope | Límite | Notas |
|---|---|---|---|
GET /services?status=&limit=&cursor= |
R | Tus servicios VPS y VDS. | |
GET /services/{id} |
R | service-reads | El objeto completo. |
GET /services/{id}/status |
R | service-reads | Instantánea en vivo y barata: power_state, uptime_seconds, last_seen, cpu_percent, ram (used_bytes, total_bytes), network (in_bytes, out_bytes). |
GET /services/{id}/metrics?range=1h|24h|7d |
R | service-reads | { range, available, points: [{ time, cpu_percent, ram_percent, disk_read_bytes_per_sec, ... }] }. |
GET /services/{id}/credentials |
M | credenciales | { username, password, expires_at } de un solo uso. Consulta la guía rápida. Errores: 404 credentials_not_available, 409 service_not_ready, 410 credentials_consumed. |
POST /services/{id}/power/{action} |
M | energía | start, stop (apagado forzado), restart (reinicio ordenado) o shutdown (apagado ordenado). Tarea 202. Errores: 409 service_suspended o service_not_ready, 503 maintenance. |
POST /services/{id}/reinstall |
M, Idem | reinstalación | { "os_template_id": "...", "ssh_key_ids": [...]?, "confirm": true }. Borra el disco. Tarea 202. |
POST /services/{id}/reset-password |
M, Idem | reset-password | Tarea 202; lee la contraseña nueva con GET /credentials. |
GET /tasks/{id} |
R | { id, type, status, service_id, created_at, started_at, finished_at, error }. type: power.start, power.stop, power.restart, power.shutdown, reinstall, reset_password. error.code: hypervisor_error, guest_agent_error, reinstall_failed o timeout. Las tareas se conservan 7 días y las puede leer cualquier llave de la cuenta. |
|
PUT /services/{id}/hostname |
M | ajustes del servicio | { "hostname": "web-prod-1" } cambia el hostname real mediante el agente invitado. Responde { hostname, requires_reboot } (Windows lo aplica al reiniciar). |
GET /services/{id}/network |
R | ips con su rdns (ptr, status: pending, applied, rejected, forward_confirmed) y rdns_available. |
|
PUT /services/{id}/network/ips/{ip}/rdns |
M | ajustes del servicio | { "ptr": "mail.example.com" }. Responde { ip, ptr, status: "pending" }: el registro lo aplica el personal. |
GET /services/{id}/ssh-keys |
R | { data: [{ id, type, fingerprint, comment, public_key }] }. Solo llaves públicas. |
|
POST /services/{id}/ssh-keys |
M | ajustes del servicio | { "public_key": "...", "name": "laptop"? }. 201 con la llave. La llave también se guarda en tu cuenta (hasta 30). Los ids (key_...) son los mismos que usa ssh_key_ids. |
DELETE /services/{id}/ssh-keys/{keyId} |
M | ajustes del servicio | 204. |
POST /services/{id}/cancel |
M, Idem | cancelar | { "at": "period_end" | "now", "confirm": true }. Responde { status: "scheduled" | "requested" | "cancelled", cancel_at, cancel_at_period_end }. |
POST /services/{id}/cancel/withdraw |
M | cancelar | Deshace una cancelación period_end. { "cancel_at_period_end": false }. No es posible tras una cancelación inmediata. |
Las acciones de energía, hostname, llaves SSH y reinstalación exigen que el servicio esté active; de lo contrario responden 409 service_not_ready o service_suspended. El hostname, las llaves SSH y el rDNS se ejecutan de forma síncrona (200, 201, 204); solo la energía, la reinstalación y el restablecimiento de contraseña devuelven tareas.
Facturación
| Método y ruta | Scope | Límite | Notas |
|---|---|---|---|
POST /services/{id}/renew |
P, Idem | renovar | { "payment": "auto" | "balance" | "card", "cycles": 1? } (cuerpo opcional). 200 { invoice, service, already_paid }. |
PUT /services/{id}/auto-renew |
B | ajustes del servicio | { "mode": "off" | "balance" | "balance_and_card" | "card" }. Responde la facturación del servicio. |
GET /invoices?status=&service_id=&limit=&cursor= |
R | status: open, overdue, paid, cancelled, disputed. Factura: id, number, status, total, created_at, due_at, paid_at, lines (description, type, service_id, quantity, unit_price, total). type de línea: plan, storage, ips, recurring, option, discount, fee, other. |
|
GET /invoices/{id} |
R | Una factura. | |
POST /invoices/{id}/pay |
P, Idem | renovar | { "payment": ... } (opcional). 200 { invoice, already_paid }. Úsalo para una factura de renovación agrupada. |
Solo las facturas cuyos servicios son todos VPS o VDS son visibles y se pueden pagar por la API (sin recargas, borradores ni facturas de migración de nodo); su id es inv_ más el id de la factura.
Firewall
El firewall de un servicio depende del nodo en el que se ejecuta, igual que en el panel. La IP siempre es la IP principal del servicio (nunca la envías). GET /services/{id}/firewall te indica qué motor tienes; llámalo antes de usar las demás rutas.
engine |
Qué ofrece | Rutas |
|---|---|---|
shield |
Funciones por IP: bloqueo de países, lista de orígenes permitidos, bloqueo de ASN, compuertas de puertos, filtros, lista negra | GET /firewall, POST y DELETE /firewall/{feature} |
gsk |
Perfiles y reglas de mitigación, ataques, métricas de tráfico, puertos de caché | GET/PUT /firewall/rules, GET /firewall/profiles, GET /firewall/attacks, GET /firewall/metrics, rutas de cache-ports |
none |
Nada: capabilities es [] y state es null |
Solo GET /firewall |
curl $API/services/K3F8QX2P/firewall -H "Authorization: Bearer $SOLUME_KEY"{
"engine": "shield",
"capabilities": ["geoblock", "srcwl", "asn-block", "..."],
"state": {
"ip": "203.0.113.10",
"geoblock": ["CN"],
"geo_policy": "blacklist",
"srcwl": [],
"asn_block": [],
"blacklist": []
}
}shield. capabilities lista las funciones que puedes cambiar. state tiene ip, geoblock (códigos de país), geo_policy (whitelist o blacklist), srcwl (orígenes permitidos), asn_block y blacklist ({ ip, manual }). Cambia una función con POST /firewall/{feature} (añadir) o DELETE /firewall/{feature} (quitar) y un cuerpo JSON que depende de la función; ambos responden { "ok": true }:
| Función | Cuerpo |
|---|---|
geoblock |
{ "country": "CN" } |
srcwl |
{ "cidr": "198.51.100.0/24" } |
asn-block |
{ "asn": "4837" } |
tcp-allow-perdst, udp-allow-perdst, ports-fivem, ports-rdp, ports-ssh, syn-gate |
{ "port": 22 } |
blacklist |
{ "src_addr": "192.0.2.9" } |
geo-rate |
{ "country": "CN", "proto": "tcp" | "udp", "pps": 1000 }; DELETE recibe country y proto |
fivem-cache |
{} |
rdp-filter, ssh-filter, udp-strict |
{ "enabled": true } (un ajuste: envía un valor nuevo, DELETE es un 400) |
geoblock-policy |
{ "policy": 0 | 1 } (ajuste) |
filter-mode |
{ "mode": "filter" | "drop" } (ajuste) |
Una función desconocida es 404 not_found. La validación exacta (puertos de 1 a 65535, etcétera) sale de las mismas reglas que el panel.
gsk.
| Ruta | Notas |
|---|---|
GET /firewall/rules, PUT /firewall/rules |
PUT { "rules": [{ "profile", "source", "port", "protocol": "tcp" | "udp" }] } reemplaza todas las reglas (hasta 200); [] las borra. |
GET /firewall/profiles |
{ profiles: [{ id, name, description }] } |
GET /firewall/attacks?page= |
{ attacks: [{ id, address, date, peak_pps, peak_bps, duration_seconds }] } para esta IP. |
GET /firewall/metrics |
{ samples: [{ time, pass_bps, drop_bps, pass_pps, drop_pps }] } |
GET /firewall/cache-ports |
{ cache: [{ type, ports }] }. Tipos: steam-a2s, raknet, sampp, fivem, minecraft-java, source, teamspeak. |
POST /firewall/cache-ports/{type} |
{ "ports": [1 to 65535] } (de 1 a 50 puertos) responde { ports }. |
DELETE /firewall/cache-ports/{type}/ports |
{ "ports": [...] } quita algunos puertos. DELETE /firewall/cache-ports/{type} borra un tipo; DELETE /firewall/cache-ports borra todo. |
Errores. Llamar a una ruta del otro motor responde 422 feature_not_supported con details.engine. Un servicio en un nodo sin firewall (o sin IP) responde 409 firewall_unavailable (mensaje "This service has no firewall." para none), sin llamar a ningún proveedor. Si el proveedor no está configurado o está caído, recibes 503 firewall_unavailable con un mensaje genérico (el texto y las credenciales del proveedor nunca se devuelven); una respuesta del proveedor que significa que nuestras propias credenciales son incorrectas (401, 403, 404 o 422) también es un 503, y solo un 400 o 409 del proveedor se convierte en un 400 invalid_request genérico; una limitación del proveedor es 429 rate_limited. Los cuerpos de shield son estrictos, y el profile de una regla de gsk debe existir en el proveedor (details.valid_profiles los lista). Los resultados de shield siempre se limitan a la IP del servicio, y los ataques de gsk se devuelven solo para esa IP. Las lecturas cuentan contra service-reads (60 por minuto por servicio, 300 por minuto por llave) además del límite general; los cambios cuentan contra service-settings (10 por hora por servicio).
Facturación y ciclo de vida
Precios y descuentos
your_price = list_price x (1 - discount/100) por ciclo y por opción. El descuento de tu llave se aplica a las compras y a las renovaciones: un servicio comprado por la API recuerda la llave que lo compró, y cada factura de renovación incluye una línea negativa Reseller discount igual al porcentaje de descuento actual de la llave. Cambiar el porcentaje afecta a las facturas que se emitan después, nunca a las ya emitidas. Un servicio comprado en el sitio web no tiene descuento. En un pago con tarjeta, el Card fee (un porcentaje de tu precio) es una línea aparte, que se añade solo cuando se cobra la tarjeta y no se reembolsa. billing.price muestra el importe de la próxima renovación.
El objeto billing
cycle, price (lo que te cuesta la próxima renovación), current_period_end, next_invoice_at, open_invoice_id, due_at, grace_ends_at, suspends_at, terminates_at, auto_renew, cancel_at_period_end y renewable. Las fechas que no aplican son null. Úsalos para mostrar a tus clientes cuándo pagar.
Cómo funcionan las renovaciones
- Una factura de renovación la genera una ejecución diaria, unos días antes de la fecha de renovación (
dunning.diasAvisoPrevio, 3 días por defecto). Su fecha de vencimiento es la fecha de renovación. Recibesinvoice.created(consulta Webhooks). - La pagas con
POST /services/{id}/renew(oPOST /invoices/{id}/pay), o automáticamente, según el modo deauto_renew. - Si la factura no está pagada cuando vence, el servicio se suspende por impago en ese momento. No hay periodo de gracia antes de la suspensión;
grace_ends_ates igual asuspends_at. - Mientras está suspendido, los reintentos con tarjeta siguen el calendario
dunning.reintentosHoras(24, 72 y 168 horas por defecto) tras un cobro fallido. Pagar la factura reactiva el servicio (service.unsuspended). - Un servicio suspendido durante
dunning.diasGraciaCancelaciondías (15 por defecto) se termina: el servidor se detiene y se corta su red (service.terminated, estadocancelled). La máquina virtual nunca se elimina automáticamente.
Estos números son ajustes de la plataforma y pueden cambiar; las fechas de cada servicio siempre están en su objeto billing, así que léelas en lugar de fijar los valores en tu código.
POST /services/{id}/renew es seguro de llamar más de una vez: una factura pagada responde 200 con already_paid: true, y preguntar antes de que abra la ventana de renovación no cobra nada ni prepaga nada (already_paid: true, con la última factura pagada). Un servicio cancelado, con error o en pausa responde 409 conflict con details.reason.
Modos de renovación automática
Los servicios comprados por API empiezan con auto_renew: "off". Cámbialo con PUT /services/{id}/auto-renew (necesita billing) o defínelo al hacer la orden.
| Modo | Comportamiento |
|---|---|
off |
Nunca se cobra nada automáticamente. Renuevas con /renew. La factura, los recordatorios, la suspensión y la terminación siguen igualmente el calendario de arriba. |
balance |
Una pasada que corre cada 10 minutos paga con el saldo la factura de renovación pendiente o vencida cuando el saldo cubre el total completo (sin pagos parciales). Si no lo cubre, no se cobra nada. |
balance_and_card |
Saldo cuando cubre el total completo (la misma pasada de 10 minutos); si no, se cobra la tarjeta predeterminada según el calendario de la tarjeta de abajo. |
card |
La tarjeta predeterminada. El primer intento se hace en el más temprano de estos momentos: 2 días después de generarse la factura (comercio.cobroAutomatico.diasAvisoPrevio, un ajuste) y 24 horas antes del vencimiento; de inmediato si quedan menos de 12 horas. Necesita una tarjeta y el mandato aceptado (de lo contrario 409 no_default_card / mandate_not_accepted). |
Si el saldo queda bajo tras un pago, recibes balance.low.
Pagos
- Orden de pago con
payment: "auto": primero el saldo de la cuenta; si no cubre el total, se cobra la tarjeta guardada predeterminada fuera de sesión a través del procesador de tarjetas, con el mandato que el cliente aceptó en el sitio web.payment: "balance"o"card"fuerzan un solo método. Un pago nunca se divide. - PayPal y cripto no se cobran en segundo plano. Solo pueden recargar el saldo (
POST /balance/top-upsdevuelve una URL de checkout alojado para el cliente), tras lo cual pagas con el saldo. - Tarjeta y mandato: los pagos con tarjeta por API (
payment: "card", o"auto"cuando el saldo no alcanza) necesitan una tarjeta predeterminada guardada con el mandato versión 2026-11-v2 aceptado. Sin él, la llamada responde402 requires_actioncondetails.reasonno_cardono_mandateantes de crear nada. Consulta Requisitos antes de comprar por API. - Cuando la tarjeta necesita al cliente (3-D Secure) la llamada responde
402 requires_action(details.reason: "authentication_required"); si se rechaza,402 card_declined. No se crea ni se aprovisiona nada, el gasto reservado se libera y se puede reintentar con la mismaIdempotency-Keytras corregir la causa.402 insufficient_balancesignifica que el saldo no cubre el total (details.required_cents). - Lo que hay que hacer en el sitio web: guardar la tarjeta, aceptar el mandato de cobro automático, recargar el saldo por PayPal o cripto si lo deseas, y que el personal conceda
purchase,billing, las IPs y los topes. El panel muestra el estado de tu tarjeta en Tarjeta para compras por API. - Un
500 internal_errortras un cobro con tarjeta en una renovación o un pago significa que no se pudo confirmar el resultado: el gasto sigue contado, la factura no se cancela y no se acepta ningún otro pago de ella durante 2 horas. LeeGET /invoices/{id}. - Si una orden se paga pero no se puede entregar, el importe se devuelve al saldo o a la tarjeta y recibes
409 out_of_stockcondetails.order_id.
Webhooks y eventos
Registra un endpoint HTTPS en API y webhooks, dentro del panel (o lee el artículo sobre webhooks). Cada entrega es un POST firmado con HMAC-SHA256 y se reintenta automáticamente. Cada webhook elige sus eventos, o * para todos.
Formato de la entrega
{
"id": "cm0entrega0ejemplo",
"evento": "invoice.paid",
"creado": "2026-12-02T06:01:12.000Z",
"datos": {
"invoice_id": "inv_cm3z4c9s00007renew02",
"number": "INV-2026-000412",
"status": "paid",
"total": { "amount_cents": 720, "currency": "USD" },
"due_at": "2026-12-05T06:00:00.000Z",
"paid_at": "2026-12-02T06:01:12.000Z",
"service_ids": ["K3F8QX2P"]
}
}Las claves del sobre (id, evento, creado, datos) son el sobre de webhooks de la plataforma y se mantienen tal cual; el contenido de datos de los eventos de revendedor está en inglés. Cabeceras: Solume-Signature: t=<unix seconds>,v1=<hex>, Solume-Event, Solume-Delivery (igual que id) y User-Agent: SolumeCore-Webhooks/1.0.
Verifica la firma: v1 es el HMAC-SHA256 en hexadecimal de <t>.<raw body> con tu secreto de firma whsec_ como clave (la cadena completa). Usa los bytes crudos antes de interpretarlos, compara en tiempo constante y rechaza un t que se aleje más de cinco minutos de tu reloj.
import crypto from 'node:crypto';
export function isValidSignature(header, rawBody, secret, tolerance = 300) {
const parts = Object.fromEntries((header ?? '').split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!Number.isInteger(t) || !parts.v1 || Math.abs(Date.now() / 1000 - t) > tolerance)
return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest();
const received = Buffer.from(parts.v1, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}Reintentos: se da por entregada cualquier respuesta 2xx; responde en menos de 10 segundos. Los errores de red, los tiempos de espera agotados, 408, 429 y 5xx se reintentan hasta 8 veces con esperas crecientes (desde unos 30 segundos hasta 4 horas); las redirecciones y los demás 4xx fallan sin reintentos. Tras 20 entregas fallidas consecutivas el webhook se pausa. Los reintentos reutilizan el mismo id, así que deduplica con él. No se garantiza el orden; usa creado.
Eventos
Estos eventos de revendedor existen y se emiten hoy:
| Evento | Cuándo | datos |
|---|---|---|
order.paid |
Se pagó una orden (justo después de POST /orders) |
order_id, status, service_id, total, payment_method |
order.failed |
Una orden pagada no se pudo entregar y se reembolsó | order_id, status |
invoice.created |
Se generó una factura de renovación | invoice_id, number, status, total, due_at, paid_at, service_ids |
invoice.paid |
Se pagó una factura, por cualquier vía | igual que invoice.created |
invoice.overdue |
Una factura pasó su fecha de vencimiento sin pagarse | igual que invoice.created |
service.renewed |
Se pagó una renovación | service_id, invoice_id, current_period_end |
service.suspended |
Se suspendió un servicio | service_id, reason (non_payment u other) |
service.unsuspended |
Un servicio suspendido se reactivó por un pago | service_id, reason (payment_received) |
service.terminated |
Un servicio suspendido llegó al final de los días de gracia, o una cancelación al final del periodo entró en vigor | service_id, reason (non_payment o cancelled_at_period_end) |
balance.low |
Un pago con saldo dejó menos saldo que el de ese pago | balance |
service.provisioned |
Tu VPS/VDS comprado por la API se entregó y ya está active |
service_id, order_id, status: "active" |
service.provision_failed |
El aprovisionamiento agotó sus intentos y el servicio pasó a error (aún puede llegar después un service.provisioned si nuestros reintentos tienen éxito) |
service_id, order_id, status: "error", error: { code: "provision_failed", message } (un mensaje genérico fijo) |
task.succeeded |
Una tarea terminó bien (energía, reset de contraseña, reinstalación); GET /tasks/{id} dice lo mismo |
task_id, type, status: "succeeded", service_id |
task.failed |
Una tarea falló o se dio por perdida como timeout (10 minutos en energía y reset de contraseña, 3 horas en una reinstalación), aunque nadie la consulte |
task_id, type, status: "failed", service_id, error: { code } |
service.cancel_scheduled |
POST /services/{id}/cancel con at: "period_end" (o la misma petición desde el panel) |
service_id, cancel_at (null cuando el personal procesa la baja porque no hay ciclo automático) |
Solo se envían para cuentas con una llave de API activa, una vez por objeto (deduplicados durante 24 horas), y nunca incluyen credenciales, datos del nodo, hostnames ni direcciones IP. service.provisioned y service.provision_failed solo se envían para servidores comprados por la API, no para compras hechas en la web. Una tarea produce un evento de cierre: task.succeeded o task.failed, nunca los dos. service.cancel_scheduled se envía una vez por petición: repetir la petición, at: "now" y cancel/withdraw no emiten nada. Tu webhook también puede suscribirse a los demás eventos de tu cuenta (energía, copias de seguridad, avisos de facturación... consulta el artículo sobre webhooks).
No tienes que esperar eventos: consultar GET /services/{id} (hasta que status sea active) y GET /tasks/{id} siempre funciona y es el respaldo seguro si se pierde una entrega. El sandbox emite los mismos eventos como test.<evento>.
GET /events: consultar en lugar de recibir
GET /v1/events?since=&type=&limit=&cursor= devuelve los eventos de los últimos 30 días, del más antiguo al más nuevo, construidos a partir de las entregas de tus webhooks:
{
"data": [
{
"id": "evt_cm3z0a1b20001xyz",
"event": "invoice.paid",
"created_at": "2026-12-02T06:01:12.000Z",
"service": null,
"actor": null,
"data": {
"invoice_id": "inv_cm3z4c9s00007renew02",
"status": "paid",
"service_ids": ["K3F8QX2P"]
}
}
],
"next_cursor": null
}since es una fecha ISO 8601 y type filtra un solo evento. Una cuenta sin ningún webhook registrado todavía no tiene eventos: registra un webhook (aunque apunte a un sumidero) para alimentar este endpoint. Usa id para deduplicar.
Sandbox
El sandbox es una copia de pruebas gratuita en https://api.solumecore.com/sandbox/v1, con sus propias llaves sk_test_. Resumen (la guía del sandbox tiene todos los detalles):
- Llaves: creas hasta 3 tú mismo en el panel (API y webhooks, Claves de API, Llaves de pruebas (sandbox)); sin aprobación, sin IPs, con todos los permisos.
- Real: el catálogo (planes, ubicaciones, plantillas, opciones). Virtual: un saldo de 1000.00 USD, una tarjeta (una cuenta nueva no tiene, como un cliente nuevo: agrégala con
POST /sandbox/v1/payment-method/setup, que también acepta el mandato), servicios, órdenes, facturas, tareas y credenciales falsas de un solo uso. No se cobra ni se aprovisiona nada. - Ciclo de vida: un servicio nuevo pasa a
activetras unos 10 segundos (X-Sandbox-Provision-Seconds: 0a120). - Escenarios: envía
X-Sandbox-Scenarioconcard_declined,requires_action,insufficient_balance,out_of_stock,provision_failed,task_failed,sloworate_limitedpara probar el manejo de errores.X-Sandbox-Firewall-Engine: shield|gsk|noneelige el motor de firewall de un servicio nuevo. Los hostnames mágicosfail-card,fail-stock,fail-provisionhacen lo mismo en una orden. - Viaje en el tiempo:
POST /sandbox/v1/time/advance {"days": 30}ejecuta las reglas de renovación, suspensión y terminación;POST /sandbox/v1/resetborra la cuenta (y su tarjeta).time/advancetiene su propio límite (30 por hora por llave). Una cuenta de sandbox se conserva 7 días después de su último uso. - Webhooks: se entregan a tus endpoints registrados, pero nunca se pueden confundir con los de producción: el nombre del evento lleva prefijo (
test.order.paid; un webhook suscrito aorder.paid, atest.order.paido a*lo recibe), se envía la cabeceraSolume-Livemode: false, el payload lleva"livemode": falsey la firma usa un secreto derivado, así que no se verifica con tu secretowhsec_real. Las entregas del sandbox nunca se reintentan. Una operación entrega como máximo 50 eventos más un resumentest.sandbox.events_summarized. - No se simula: la lista de IPs permitidas, la aprobación del personal, procesamiento de tarjetas, 3-D Secure real, PayPal y cripto reales, el tiempo real de aprovisionamiento, las comprobaciones antifraude y el stock real.
- Toda respuesta lleva
X-Sandbox: true. Una llave real en el sandbox, o al revés, responde403 wrong_environment.
Lista de comprobación para pasar a producción
- Cambia la URL base a
https://api.solumecore.com/v1y la llave ask_live_. - Pide a soporte tus IPs, los permisos
purchaseybillingy tus topes de gasto. - Guarda la tarjeta y acepta el mandato de cobro automático en el panel; comprueba
GET /payment-method. - Revisa
GET /me: scopes, topes, descuento, recargo de tarjeta y límites de tasa. - Prueba cómo manejas
402,403,409,429y5xx, y lasIdempotency-Keyrepetidas. - Registra un endpoint de webhook y verifica las firmas.
- Empieza con una orden real pequeña y una renovación antes de abrir el servicio a tus clientes.
Buenas prácticas
- Idempotencia siempre. Deriva la
Idempotency-Keydel id de tu propia orden, envíala en cada reintento y trataIdempotent-Replay: truecomo un éxito. - Reintenta con retroceso. Reintenta
429(respetandoRetry-After),503y5xxcon retroceso exponencial y jitter. No reintentes sin cambios los4xxdistintos de408/409 request_in_progress/429. - Registra
X-Request-Idde cada llamada (yerror.request_id) y cítalo en los tickets. - Nunca guardes en caché ni registres credenciales. Pasa la contraseña a tu almacén de secretos en la primera lectura. Si la pierdes, usa
reset-password. - Maneja el
402.requires_actionycard_declinednecesitan al cliente; recurre apayment: "balance"y mantén el saldo con fondos. - Vigila tus topes y límites. Usa
would_exceed_capen las cotizaciones,GET /me(usage) y las cabecerasX-RateLimit-*para frenar antes de llegar al429. - Prefiere webhooks y consulta con criterio. Usa webhooks o
GET /eventspara la facturación y el ciclo de vida, y consultaGET /services/{id}cada 5 a 30 segundos solo mientras esperas el estadoactive. UsaGET /services/{id}/statuspara comprobaciones en vivo baratas. - Pagina. Sigue
next_cursorhasta que seanull; no construyas cursores tú mismo. - Concilia. Guarda en
metadatatus propios ids al hacer la orden y compara periódicamenteGET /servicesyGET /invoicescon tus registros. - Haz explícitas las llamadas destructivas. La reinstalación y la cancelación necesitan
confirm: true; pide confirmación también en tu propia interfaz.
Seguridad
- Lista de IPs permitidas. Mantén al mínimo la lista de IPs registradas y pide a soporte que quite las direcciones que ya no uses. Llama desde IPs de salida fijas.
- Mínimo privilegio. Pide solo los scopes que necesitas: por ejemplo, un trabajo de monitoreo necesita
read; el código que compra necesitapurchasey nada más. Usa varias llaves (una por sistema) para poder revocar una sin detener las demás. - Los topes son una red de seguridad. Mantén los topes por orden y diarios cerca de lo que realmente gastas.
- Llaves y registros. Una cuenta puede tener hasta 10 llaves reales activas. Borrar una llave real es un borrado lógico (deja de funcionar de inmediato y se conserva su historial) y te pide confirmar tu identidad. Toda llamada hecha con una llave, incluidas las rechazadas, se registra y se conserva 90 días.
- Rota las llaves. Crea la llave nueva, despliégala y luego revoca la anterior en el panel. Crear o revocar llaves te pide confirmar tu identidad y envía notificaciones. Rota de inmediato si una llave pudo filtrarse.
- Mantén los secretos fuera del código y de los logs. Usa variables de entorno o un gestor de secretos; nunca pongas una llave en un navegador, una aplicación móvil o un repositorio público. Nunca registres las cabeceras de las peticiones ni las credenciales.
- Verifica los webhooks. Comprueba la firma con el cuerpo crudo y una tolerancia de tiempo, y deduplica por
id. - No compartas llaves entre clientes, no envíes
Authorizationa terceros, no montes un proxy público que exponga la API sin tu propia autenticación y límites, ni desactives la verificación de certificados. - El personal recibe alertas por
429eip_not_allowedrepetidos; un gasto inusual también choca con tus topes.
Registro de cambios
v1.1 (2026-11)
Cambios posteriores a la auditoría de seguridad.
GET /services/{id}/credentialsahora necesita el permisomanage(antes eraread);HEADno se atiende.- Nueva cubeta de límite
service-reads: 60 por minuto por servicio y 300 por minuto por llave paraGET /services/{id},/status,/metrics,GET /firewally las lecturas de gsk. POST /services/{id}/cancelconat: "now"solo funciona en un servicioactive(en otro caso,409 service_suspendedoservice_not_ready).- Las compras y los pagos por API con tarjeta necesitan el mandato de cobro automático versión 2026-11-v2: sin él,
402 requires_actioncondetails.reason: "no_mandate".auto_renewcon tarjeta comprueba la tarjeta y el mandato (409 conflict). - Las órdenes devuelven
invoice_id(inv_...); unaIdempotency-Keyreutilizada con otro cuerpo es422también después de que una orden se recuperó. - Firewall: un fallo del proveedor es
503 firewall_unavailable; las lecturas nunca devuelven una lista vacía ante un fallo. - Una dependencia de la plataforma que no responde da
503conRetry-Afteren/v1. - Sandbox: los webhooks viajan como
test.<event>conSolume-Livemode: falsey un secreto de firma derivado, sin reintentos;time/advancetiene su propio límite y entrega como máximo 50 eventos más un resumen; las cuentas se conservan 7 días. - Hasta 3 llaves de sandbox y 10 llaves reales por cuenta; los registros de uso se conservan 90 días.
v1.0 (2026-11)
Primera versión completa de la API de revendedor, que reemplaza a la API de cliente anterior (/v1/services solo con energía y firewall), la cual no tenía usuarios activos.
- Catálogo (
/catalog/*), cotizaciones y órdenes para VPS y VDS, con descuento y recargo de tarjeta por llave. - Servicios: estado, métricas, credenciales de un solo uso, energía, reinstalación, restablecimiento de contraseña, hostname, rDNS, llaves SSH, red, cancelar y retirar la cancelación, y tareas asíncronas.
- Firewall con tres motores (
shield,gsk,none). - Facturación: renovaciones, modos de renovación automática, facturas, saldo, recargas e información del método de pago.
- Permisos (
read,manage,purchase,billing), topes de gasto, lista de IPs permitidas, idempotencia, límites de tasa por cubeta y un único formato de error. - Webhooks y eventos para órdenes, facturas y el ciclo de vida del servicio, además de
GET /events. - Sandbox con cuenta virtual, escenarios, viaje en el tiempo y webhooks con
livemode: false. - Archivos OpenAPI para producción y para el sandbox.
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
¿Usar la API de revendedor de SolumeCore tiene algún costo?
No. La API, las llamadas y las llaves no tienen cargo. Pagas el precio normal de cada VPS o VDS que compras o renuevas, menos el descuento acordado para tu llave y más el recargo de tarjeta acordado cuando se cobra con tarjeta. Ambos son 0% por defecto.
¿Puedo revender servidores de juegos o hosting web con la API?
No. La API de revendedor cubre solo servidores VPS y VDS. Un plan de otro tipo se rechaza con el error product_not_resellable.
¿Por qué mi llave de API responde ip_not_allowed?
Una llave live solo funciona desde las direcciones IP que el equipo de Solume registró para ella. Abre un ticket desde el panel con las IP públicas desde las que llama tu código. Las llaves del sandbox no necesitan IP.
¿Cómo pruebo sin gastar dinero?
Usa el sandbox en https://api.solumecore.com/sandbox/v1 con una llave sk_test_ creada en el panel del cliente. Tiene los mismos endpoints y errores que producción sobre una cuenta virtual con un saldo virtual de 1000.00 USD.
¿Cómo se paga una compra cuando uso la API?
Con payment en auto se usa primero el saldo de la cuenta y, si no alcanza, se cobra la tarjeta guardada predeterminada fuera de sesión con el mandato de cobro automático aceptado en la web. PayPal y cripto solo sirven para recargar el saldo.