API v1 · Operativa

Recarga.do API

Recargas móviles, paquetes de datos y pago de servicios en República Dominicana. Una sola integración REST, con ambiente de pruebas incluido.

Basehttps://api.recarga.do
Versiónv1
MonedaDOP
FormatoJSON

Introducción

La API de Recarga.do permite a su negocio vender recargas y cobrar servicios desde su propio sistema: punto de venta, aplicación móvil, portal web o bot de WhatsApp.

ConceptoDetalle
ModeloPrepago. Usted mantiene un saldo y cada operación lo descuenta.
ProtocoloREST sobre HTTPS. Peticiones y respuestas en JSON UTF-8.
AutenticaciónPar api_key / api_secret por header.
MonedaPeso dominicano (DOP). Montos con hasta 2 decimales.
Zona horariaAmerica/Santo_Domingo (UTC−4).
ServiciosTelefonía, paquetes de datos, electricidad, agua, internet y TV, entretenimiento y más.
Diseñada para no cobrar dos veces

Cada operación financiera exige una clave de idempotencia, y el importe se reserva antes de enviarse al proveedor. Si algo falla, el dinero vuelve a su saldo automáticamente. Si el resultado queda en duda, se retiene en lugar de cobrarse. Lea Idempotencia antes de integrar.

Guía rápida

De cero a su primera recarga de prueba en cinco minutos.

1
Verifique su cuenta Consulte GET /v1/account y confirme si su token está en development (pruebas) o production (dinero real).
2
Consulte su saldo GET /v1/balance le indica cuánto puede gastar ahora mismo.
3
Obtenga el catálogo GET /v1/services devuelve el service_id y el rango de montos de cada servicio.
4
Ejecute la recarga POST /v1/recharges con un Idempotency-Key único.
5
Guarde el resultado Almacene el transaction_id y el request_id. Son su referencia ante cualquier consulta.

Todo el flujo en una sola sesión de terminal

# 1 · Credenciales del ambiente de pruebas
KEY="rk_sbx_8c4cacb52c6b8f65bf389480ce3f8891"
SECRET="sk_sbx_a8e4de1dbd5e135a4be191cfd8901f386e541279a6550b1d30af9fbeaa65bbd0"
AUTH="Authorization: Bearer $KEY.$SECRET"

# 2 · ¿En qué ambiente estoy?
curl -s https://api.recarga.do/v1/account -H "$AUTH"

# 3 · ¿Cuánto saldo tengo?
curl -s https://api.recarga.do/v1/balance -H "$AUTH"

# 4 · ¿Qué servicios puedo vender?
curl -s https://api.recarga.do/v1/services -H "$AUTH"

# 5 · Recargar (la clave debe ser única por operación)
curl -s -X POST https://api.recarga.do/v1/recharges \
  -H "$AUTH" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"service_id":23,"identifier":"8095551235","amount":100}'

# 6 · Consultar la transacción
curl -s https://api.recarga.do/v1/transactions/48849 -H "$AUTH"

Ambiente de pruebas

Las cuentas con ambiente development operan contra un simulador. No se genera ninguna recarga real ni ningún cargo real, pero el saldo, las transacciones y todos los estados se comportan exactamente igual que en producción. Es el lugar correcto para desarrollar y para probar sus manejos de error.

Credenciales de prueba abiertas

Puede usarlas ahora mismo, sin registro. Son compartidas y su saldo se restablece periódicamente: úselas para explorar, y solicite las suyas propias para desarrollar en serio.

api_key    rk_sbx_8c4cacb52c6b8f65bf389480ce3f8891
api_secret sk_sbx_a8e4de1dbd5e135a4be191cfd8901f386e541279a6550b1d30af9fbeaa65bbd0

Escenarios controlados

En el sandbox, el último dígito del campo identifier decide el resultado. Así puede probar todos los caminos de forma determinista, incluidos los que en producción son difíciles de reproducir.

Identifier termina enHTTPCódigoQué ocurre con su saldo
0 — ej. 8095550000502INVALID_PHONE Se reserva y se devuelve completo
1 — ej. 8095550001502TRANSACTION_REJECTED Se reserva y se devuelve completo
2 — ej. 8095550002202TRANSACTION_PENDING Queda retenido — ni cobrado ni disponible
cualquier otro — ej. 8095551235201SUCCESS Se cobra
Pruebe el escenario 202 antes de salir a producción

Es el caso que rompe la mayoría de las integraciones. Un 202 no significa que la recarga falló: significa que aún no se sabe. Su código nunca debe reintentar con una clave nueva ante un 202. Use el identificador terminado en 2 para verificar que lo maneja bien.

Pasar a producción

  1. Solicite sus credenciales de producción a su ejecutivo de cuenta.
  2. Confirme con GET /v1/account que devuelve "sandbox": false.
  3. Deposite saldo. Verifique con GET /v1/balance.
  4. La URL base y todos los endpoints son idénticos: solo cambian las credenciales.

Consola interactiva

Ejecute peticiones reales contra la API desde este navegador. Viene precargada con las credenciales de prueba, así que funciona sin configurar nada.

Probar la API

La respuesta aparecerá aquí.
Reintente con la misma clave, no con una nueva

Envíe la misma recarga dos veces sin tocar el campo Idempotency-Key: obtendrá el mismo transaction_id y el header Idempotent-Replay: true. El saldo baja una sola vez. Pulse «Nueva clave» y verá que sí se ejecuta una segunda recarga: esa es exactamente la diferencia que evita cobrar dos veces a su cliente.

Autenticación

Cada comercio recibe un par api_key / api_secret. Se admiten dos formas equivalentes; elija la que le resulte más cómoda.

Opción A — Bearer (recomendada)

Authorization: Bearer <api_key>.<api_secret>

La clave y el secreto van separados por un punto, en un solo header.

Opción B — Headers separados

X-Api-Key: <api_key>
X-Api-Secret: <api_secret>
Su api_secret es una credencial de pago

Quien lo posea puede gastar su saldo. No lo incluya en aplicaciones móviles, código JavaScript de navegador ni repositorios. Las llamadas deben salir siempre desde su servidor. Si sospecha de una filtración, solicite su rotación de inmediato.

Su empresa la determina el token

No envíe empresa_id en el cuerpo de sus peticiones. La API deriva la identidad exclusivamente de su token. Si envía un empresa_id que no le corresponde, la petición se rechaza con 403 y el intento queda registrado.

Motivos de rechazo

SituaciónHTTPCódigo
Falta el header o el formato es incorrecto401UNAUTHENTICATED
La clave o el secreto no coinciden401UNAUTHENTICATED
El token expiró401UNAUTHENTICATED
Token suspendido o revocado403FORBIDDEN
Empresa sin API habilitada o suspendida403FORBIDDEN
empresa_id ajeno en el cuerpo403FORBIDDEN

Formato de respuesta

Todas las respuestas comparten la misma estructura. Puede escribir un único manejador para toda la API.

{
  "success": true,
  "data": { },
  "message": "Operación realizada correctamente",
  "request_id": "req_9f2c1ab34de5f6a7b8c9d0e1"
}
{
  "success": false,
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Balance insuficiente para procesar la operación.",
    "details": { "saldo_disponible": 120.00, "monto_requerido": 500.00 }
  },
  "request_id": "req_9f2c1ab34de5f6a7b8c9d0e1"
}
Guarde siempre el request_id

Viaja además en el header X-Request-Id y aparece en nuestros registros. Con él, cualquier incidencia se localiza en segundos. Regístrelo junto a cada transacción en su sistema.

Cómo interpretar el resultado

  • Decida por el código HTTP y por error.code, nunca por el texto de message: los mensajes pueden cambiar.
  • 2xx no siempre es "cobrado": el 202 significa resultado pendiente.
  • 5xx y 502 no siempre son "no ocurrió": ante la duda, consulte la transacción.

Idempotencia

Es la parte más importante de esta documentación. Léala completa antes de escribir su integración.

POST /v1/recharges exige el header Idempotency-Key: una cadena única por operación, de hasta 128 caracteres. Se recomienda un UUID v4.

Idempotency-Key: 8fe9d681-a99e-4fda-b610-3c0a1f7e42bd

Qué garantiza

SituaciónRespuestaEfecto sobre el saldo
Primera vez con esa clave201 con la transacciónSe cobra una vez
Misma clave, mismo cuerpo, ya completada 201 + header Idempotent-Replay: true Ninguno — es la respuesta original
Misma clave, operación aún en curso409 IDEMPOTENT_REQUEST_IN_PROGRESS Ninguno — la original sigue su curso
Misma clave, cuerpo distinto422 VALIDATION_ERRORNinguno
Clave nueva201Se cobra otra vez — es otra operación

Las claves se conservan 24 horas. El ámbito es por comercio: dos clientes distintos pueden usar la misma cadena sin interferir.

Ante un timeout: reintente con la MISMA clave

Nunca genere una clave nueva para el mismo cobro. Una clave nueva es una operación nueva y su cliente recibirá dos recargas pagando una. Si su sistema genera la clave dentro del bucle de reintentos, tiene un error grave: la clave debe generarse una sola vez, antes del primer intento, y persistirse junto a la orden.

Patrón correcto

// CORRECTO — la clave se genera una vez y se guarda con la orden
orden.idempotency_key = orden.idempotency_key ?? uuidv4();
guardar(orden);

for (intento = 1; intento <= 3; intento++) {
    r = post('/v1/recharges', cuerpo, orden.idempotency_key);   // misma clave siempre
    if (r.status === 201) return exito(r);
    if (r.status === 409) { esperar(2000); continue; }           // sigue en curso
    if (r.status === 202) return consultarEstado(r);             // pendiente: NO reintentar
    if (r.status === 502) return fallo(r);                       // rechazo: saldo devuelto
}

// INCORRECTO — genera una clave por intento: cobra varias veces
for (intento = 1; intento <= 3; intento++) {
    post('/v1/recharges', cuerpo, uuidv4());                     // ✗ NUNCA
}

Manejo del saldo

Su saldo tiene tres componentes. Entenderlos evita la mayoría de las dudas de conciliación.

CampoSignificado
saldo_disponibleLo que puede gastar ahora mismo.
saldo_retenidoComprometido por operaciones en curso: ni cobrado ni gastable.
saldo_actualLa suma de ambos. Es el dinero que aún le pertenece.

Ciclo de una recarga

1
Reserva Antes de contactar al proveedor, el importe pasa de disponible a retenido. Si no alcanza, la operación se rechaza y nunca llega al proveedor.
2
Envío al proveedor Su dinero está protegido: comprometido pero no cobrado.
3
Liquidación Éxito → sale de retenido y de actual: cobrado.
Rechazo → vuelve a disponible: no se cobró nada.
Desconocido → permanece en retenido hasta resolverse.
Nunca se entrega una recarga sin respaldo de saldo

Y nunca se cobra una recarga que el proveedor rechazó. Si ve saldo en retenido, corresponde a operaciones cuyo resultado se está confirmando; se resuelven automáticamente.

Límites y rate limit

Límites de consumo

Su cuenta puede tener topes por período, visibles en GET /v1/balance. El valor 0 significa «sin límite».

PeríodoSe cuenta desde
DiarioLas 00:00 del día en curso
SemanalEl lunes de la semana en curso
MensualEl día 1 del mes en curso

Al superarlos recibirá 422 LIMIT_EXCEEDED, con el límite y lo consumido en details.

Rate limit

Por defecto 120 peticiones por minuto por token. Cada respuesta incluye:

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 1786568460

Al excederlo: 429 RATE_LIMIT_EXCEEDED con retry_after en segundos. Aplique espera exponencial; si reintenta una recarga, conserve la misma clave de idempotencia.

Endpoints · Sistema

GET/v1/health Público
Estado del servicio. Úselo para monitoreo; no requiere credenciales.
curl -s https://api.recarga.do/v1/health

{"success":true,"data":{"status":"ok"},"message":"Servicio operativo","request_id":"req_..."}
GET/v1/info Público
Versión y entorno de la API.
{
  "success": true,
  "data": {
    "application": "Recarga.do API",
    "api_version": "v1",
    "environment": "production",
    "documentation": "https://doc.recarga.do",
    "currency": "DOP",
    "timezone": "America/Santo_Domingo"
  },
  "message": "Información de la API",
  "request_id": "req_..."
}
GET/v1/health/deep Autenticado
Comprueba las dependencias internas. Devuelve 503 si alguna está degradada.

Endpoints · Cuenta y saldo

GET/v1/account Autenticado
Datos de su empresa y del token. Aquí confirma si está en pruebas o en producción.
{
  "success": true,
  "data": {
    "empresa_id": 370,
    "razon_social": "Comercial Ejemplo SRL",
    "codigo_empresa": "EMP-00370",
    "ambiente": "development",
    "sandbox": true,
    "token": { "nombre": "integracion-pos", "permisos": [] }
  },
  "message": "Cuenta consultada",
  "request_id": "req_..."
}

Compruebe sandbox al arrancar su aplicación y muéstrelo en su interfaz. Es la forma más simple de evitar que alguien crea que está probando cuando está gastando dinero real.

GET/v1/balance Autenticado
Saldo, retenciones, límites y consumo por período.
{
  "success": true,
  "data": {
    "saldo_actual": 15400.00,
    "saldo_disponible": 15150.00,
    "saldo_retenido": 250.00,
    "tipo_credito": "prepago",
    "alerta_saldo_bajo": 5000.00,
    "limites":         { "diario": 50000.00, "semanal": 250000.00, "mensual": 1000000.00 },
    "consumo_periodo": { "diario": 3200.00,  "semanal": 18400.00,  "mensual": 61200.00 },
    "saldo_bajo": false,
    "moneda": "DOP",
    "actualizado_en": "2026-08-12 20:31:04"
  },
  "message": "Balance consultado",
  "request_id": "req_..."
}

saldo_bajo se activa cuando el disponible cae por debajo de alerta_saldo_bajo: úselo para avisar a su equipo antes de quedarse sin fondos.

Endpoints · Recargas

POST/v1/recharges AutenticadoIdempotency-Key
Ejecuta una recarga y descuenta el importe de su saldo.

Headers

HeaderObligatorioDescripción
AuthorizationBearer <api_key>.<api_secret>
Idempotency-KeyCadena única por operación, máx. 128 caracteres. UUID v4 recomendado.
Content-Typeapplication/json

Cuerpo

CampoTipoObligatorioDescripción
service_identeroObtenido de GET /v1/services.
identifierstringTeléfono, contrato o NIC según el servicio. 3–100 caracteres.
amountdecimal> 0, máx. 2 decimales, dentro del rango del servicio.
terminal_idstringNoReferencia de caja o terminal. Máx. 50 caracteres.

No envíe empresa_id: lo determina su token.

POST /v1/recharges HTTP/1.1
Host: api.recarga.do
Authorization: Bearer rk_live_xxx.sk_live_xxx
Idempotency-Key: 8fe9d681-a99e-4fda-b610-3c0a1f7e42bd
Content-Type: application/json

{
  "service_id": 12,
  "identifier": "8095551234",
  "amount": 100,
  "terminal_id": "CAJA-01"
}
HTTP/1.1 201 Created

{
  "success": true,
  "data": {
    "transaction_id": 48849,
    "status": "SUCCESS",
    "xid": "RD-b1d02d05e291c41716ff89bf",
    "auth_code": "884213",
    "service": { "id": 12, "nombre": "Claro Recarga" },
    "identifier": "8095551234",
    "amount": 100.00,
    "currency": "DOP",
    "provider_message": "Recarga procesada exitosamente",
    "processed_at": "2026-08-12T21:11:26-04:00"
  },
  "message": "Recarga procesada exitosamente",
  "request_id": "req_eb4e618bfd13949169702e52"
}

El importe fue cobrado. Entregue el comprobante al cliente y guarde transaction_id y auth_code.

HTTP/1.1 202 Accepted

{
  "success": false,
  "error": {
    "code": "TRANSACTION_PENDING",
    "message": "La operación quedó en proceso. Consulte su estado antes de reintentar; el importe permanece retenido.",
    "details": {
      "transaction_id": 48861,
      "xid": "RD-b4812b7c6a84b3c2a2aed260",
      "status": "PENDING",
      "consultar_en": "/v1/transactions/48861"
    }
  },
  "request_id": "req_6dca78bb8032673599c5e47a"
}
No lo trate como un fallo

La recarga pudo haberse entregado. El importe queda retenido, no cobrado. No reintente ni con la misma clave ni con una nueva: consulte GET /v1/transactions/{id} hasta que pase a SUCCESS o FAILED. En su caja, muéstrelo como «verificando», nunca como «fallida».

HTTP/1.1 502 Bad Gateway

{
  "success": false,
  "error": { "code": "INVALID_PHONE", "message": "Número inválido." },
  "request_id": "req_2fe16c3c610ef66635a798ac"
}

El proveedor rechazó la operación de forma definitiva. El importe ya fue devuelto a su saldo. Puede corregir los datos y volver a intentar con una clave nueva.

HTTP/1.1 409 Conflict

{
  "success": false,
  "error": {
    "code": "IDEMPOTENT_REQUEST_IN_PROGRESS",
    "message": "Ya hay una operación en curso con esta Idempotency-Key. Consulte su estado antes de reintentar."
  },
  "request_id": "req_..."
}

Otra petición con la misma clave se está ejecutando. Espere unos segundos y reintente con la misma clave, o consulte el historial por identifier.

Endpoints · Transacciones

GET/v1/transactions Autenticado
Historial paginado. Solo devuelve transacciones de su empresa.
ParámetroPor defectoDescripción
page1Número de página.
per_page50Entre 1 y 200.
statusSUCCESS, FAILED, REVERSED, PENDING, PROCESSING
date_fromFecha YYYY-MM-DD, inclusive.
date_toFecha YYYY-MM-DD, inclusive.
identifierCoincidencia exacta. Su mejor herramienta tras un timeout.
curl -s "https://api.recarga.do/v1/transactions?identifier=8095551234&date_from=2026-08-12" -H "$AUTH"

{
  "success": true,
  "data": {
    "items": [ { "transaction_id": 48849, "status": "SUCCESS", "amount": 100.00, ... } ],
    "pagination": { "page": 1, "per_page": 50, "total": 1284, "total_pages": 26 }
  },
  "message": "1284 transacciones encontradas",
  "request_id": "req_..."
}
GET/v1/transactions/{id} Autenticado
Detalle de una transacción. Una transacción ajena devuelve 404.
{
  "transaction_id": 48849,
  "xid": "RD-b1d02d05e291c41716ff89bf",
  "status": "SUCCESS",
  "service": { "id": 12, "nombre": "Claro Recarga" },
  "identifier": "8095551234",
  "amount": 100.00,
  "currency": "DOP",
  "auth_code": "884213",
  "reference": "RD-b1d02d05e291c41716ff89bf",
  "terminal_id": "CAJA-01",
  "message": "Recarga procesada exitosamente",
  "created_at": "2026-08-12 21:11:25",
  "processed_at": "2026-08-12 21:11:26"
}

Estados posibles

statusSignificadoSu saldo
SUCCESSEntregada y cobradaCobrado
FAILEDRechazada por el proveedorDevuelto
PENDINGResultado aún desconocidoRetenido
PROCESSINGEn curso hacia el proveedorRetenido
REVERSEDAnulada tras completarseDevuelto
GET/v1/transactions/{id}/receipt Autenticado
Datos del recibo en JSON, para imprimir en su formato. Solo transacciones SUCCESS.
{
  "transaction_id": 48849,
  "xid": "RD-b1d02d05e291c41716ff89bf",
  "auth_code": "884213",
  "emisor": "Recarga.do",
  "comercio": "Comercial Ejemplo SRL",
  "servicio": "Claro Recarga",
  "identifier": "8095551234",
  "amount": 100.00,
  "currency": "DOP",
  "processed_at": "2026-08-12 21:11:26",
  "mensaje": "Recarga procesada exitosamente"
}

Códigos de error

Decida siempre por error.code, no por el texto del mensaje.

CódigoHTTPSignificadoQué hacer
UNAUTHENTICATED401Credenciales ausentes o inválidasRevise el header. No reintente en bucle.
FORBIDDEN403Token o empresa sin permisoContacte a soporte.
NOT_FOUND404Recurso inexistente o de otra empresaVerifique el identificador.
VALIDATION_ERROR422Parámetros inválidosRevise details. No reintente sin corregir.
INSUFFICIENT_BALANCE422Saldo disponible insuficienteDeposite. details trae disponible y requerido.
LIMIT_EXCEEDED422Límite del período superadoEspere al siguiente período o solicite ampliación.
IDEMPOTENT_REQUEST_IN_PROGRESS409Operación en curso con la misma claveEspere y reintente con la misma clave.
IDEMPOTENCY_CONFLICT409Clave irresolubleConsulte el historial; use una clave nueva solo si confirma que no se ejecutó.
RATE_LIMIT_EXCEEDED429Demasiadas peticionesEspera exponencial. Conserve la clave de idempotencia.
TRANSACTION_PENDING202Resultado desconocidoConsulte la transacción. No reintente.
INVALID_PHONE502Número inválidoCorrija y use una clave nueva. Saldo devuelto.
INVALID_PRODUCT502Servicio no válidoRefresque el catálogo.
INVALID_AMOUNT502Monto no admitidoRespete el rango del servicio.
DUPLICATE_TRANSACTION502El proveedor la considera duplicadaConsulte el historial antes de reintentar.
TRANSACTION_REJECTED502Rechazo genéricoSaldo devuelto. Vea el mensaje del proveedor.
PROVIDER_TIMEOUT502El proveedor no respondióConsulte el estado antes de decidir.
PROVIDER_UNAVAILABLE502Sin comunicación con el proveedorReintente con espera. Misma clave.
INTERNAL_ERROR500Error internoConsulte el estado y reporte el request_id.

Flujos recomendados

Venta en punto de venta

Cajero introduce número y monto
        │
        ├─ Validar contra el catálogo cacheado (rango de monto)
        ├─ Generar Idempotency-Key UUID v4  ← UNA sola vez
        ├─ Persistir la orden con esa clave  ← ANTES de llamar
        │
        ▼
POST /v1/recharges
        │
        ├─ 201 ─→ Imprimir recibo. Guardar transaction_id y auth_code.
        ├─ 202 ─→ Mostrar "VERIFICANDO". Consultar cada 10 s hasta 2 min.
        ├─ 409 ─→ Esperar 2 s y reintentar con LA MISMA clave.
        ├─ 502 ─→ Mostrar el motivo. El saldo ya volvió. Nueva clave si corrige.
        └─ timeout de red ─→ Reintentar con LA MISMA clave.

Resolver una operación en duda

¿Timeout, 202 o 500?
        │
        ├─ 1. Reintentar POST con LA MISMA Idempotency-Key
        │       └─ Si ya se ejecutó → 201 con Idempotent-Replay: true
        │
        ├─ 2. Si devuelve 409 → la original sigue en curso: esperar
        │
        └─ 3. Consultar GET /v1/transactions?identifier=8095551234&date_from=HOY
                └─ Localiza la transacción aunque haya perdido el transaction_id
Nunca quedará en duda si persiste la clave

Guardar la Idempotency-Key junto a la orden, antes de la primera llamada, resuelve por sí solo casi todos los escenarios de red. Es la única pieza de estado que su sistema necesita conservar.

Conciliación diaria

  1. Al cierre, consulte GET /v1/transactions?date_from=HOY&date_to=HOY paginando.
  2. Compare contra sus ventas registradas por transaction_id.
  3. Toda transacción en PENDING debe revisarse al día siguiente.
  4. El total de SUCCESS debe coincidir con lo descontado de su saldo.

Ejemplos de código

Implementaciones completas de una recarga, con manejo de los cuatro desenlaces posibles.

#!/usr/bin/env bash
set -euo pipefail

KEY="rk_sbx_8c4cacb52c6b8f65bf389480ce3f8891"
SECRET="sk_sbx_a8e4de1dbd5e135a4be191cfd8901f386e541279a6550b1d30af9fbeaa65bbd0"
AUTH="Authorization: Bearer $KEY.$SECRET"

# La clave se genera UNA vez y se guarda junto a la orden
IDEM=$(uuidgen)
echo "$IDEM" > /var/orders/orden-1234.key

RESP=$(curl -s -w '\n%{http_code}' -X POST https://api.recarga.do/v1/recharges \
  -H "$AUTH" -H "Idempotency-Key: $IDEM" -H "Content-Type: application/json" \
  -d '{"service_id":23,"identifier":"8095551235","amount":100}')

CODE=$(echo "$RESP" | tail -1)
BODY=$(echo "$RESP" | head -n -1)

case "$CODE" in
  201) echo "COBRADA: $(echo "$BODY" | jq -r .data.transaction_id)" ;;
  202) echo "PENDIENTE: consultar $(echo "$BODY" | jq -r .error.details.consultar_en)" ;;
  409) echo "EN CURSO: reintentar con LA MISMA clave $IDEM" ;;
  502) echo "RECHAZADA: $(echo "$BODY" | jq -r .error.code) — saldo devuelto" ;;
  *)   echo "REVISAR: $CODE / $(echo "$BODY" | jq -r .request_id)" ;;
esac
<?php

class RecargaDo
{
    public function __construct(
        private string $apiKey,
        private string $apiSecret,
        private string $base = 'https://api.recarga.do'
    ) {}

    public function recargar(int $servicioId, string $numero, float $monto, string $idemKey): array
    {
        $ch = curl_init($this->base . '/v1/recharges');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_HTTPHEADER     => [
                'Authorization: Bearer ' . $this->apiKey . '.' . $this->apiSecret,
                'Idempotency-Key: ' . $idemKey,
                'Content-Type: application/json',
            ],
            CURLOPT_POSTFIELDS => json_encode([
                'service_id' => $servicioId,
                'identifier' => $numero,
                'amount'     => $monto,
            ]),
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_TIMEOUT        => 40,
        ]);

        $body   = curl_exec($ch);
        $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        return ['status' => $status, 'body' => json_decode((string) $body, true) ?? []];
    }
}

// La clave se genera UNA vez y se persiste con la orden.
$orden->idem_key ??= sprintf('%s-%s', $orden->id, bin2hex(random_bytes(16)));
$orden->save();

$api = new RecargaDo(getenv('RECARGA_KEY'), getenv('RECARGA_SECRET'));
$r   = $api->recargar(23, '8095551235', 100.00, $orden->idem_key);

switch ($r['status']) {
    case 201:
        $orden->marcarCobrada($r['body']['data']['transaction_id']);
        break;

    case 202: // Desconocido: el importe está retenido, NO reintentar
        $orden->marcarVerificando($r['body']['error']['details']['transaction_id']);
        break;

    case 409: // La original sigue en curso: reintentar con LA MISMA clave
        $orden->programarReintento();
        break;

    case 502: // Rechazo definitivo: el saldo ya fue devuelto
        $orden->marcarFallida($r['body']['error']['code']);
        break;

    default:  // Ante la duda, verificar antes de decidir
        $orden->marcarVerificando(null);
}
import { randomUUID } from 'node:crypto';

const BASE = 'https://api.recarga.do';
const AUTH = `Bearer ${process.env.RECARGA_KEY}.${process.env.RECARGA_SECRET}`;

async function recargar({ serviceId, identifier, amount, idemKey }) {
  const res = await fetch(`${BASE}/v1/recharges`, {
    method: 'POST',
    headers: {
      'Authorization': AUTH,
      'Idempotency-Key': idemKey,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ service_id: serviceId, identifier, amount }),
    signal: AbortSignal.timeout(40_000),
  });
  return { status: res.status, body: await res.json() };
}

// La clave se genera UNA vez y se guarda con la orden.
orden.idemKey ??= randomUUID();
await orden.save();

let r;
try {
  r = await recargar({ serviceId: 23, identifier: '8095551235', amount: 100, idemKey: orden.idemKey });
} catch (e) {
  // Timeout de red: la recarga PUDO ejecutarse. Reintentar con LA MISMA clave.
  r = await recargar({ serviceId: 23, identifier: '8095551235', amount: 100, idemKey: orden.idemKey });
}

switch (r.status) {
  case 201:
    await orden.cobrada(r.body.data.transaction_id);
    break;
  case 202: // Retenido, no cobrado. Consultar; nunca reintentar con clave nueva.
    await orden.verificando(r.body.error.details.transaction_id);
    break;
  case 409: // Sigue en curso
    await orden.reintentarEn(2000);
    break;
  case 502: // Saldo ya devuelto
    await orden.fallida(r.body.error.code);
    break;
  default:
    await orden.verificando(null);
}
import os, uuid, requests

BASE = "https://api.recarga.do"
AUTH = f"Bearer {os.environ['RECARGA_KEY']}.{os.environ['RECARGA_SECRET']}"


def recargar(service_id: int, identifier: str, amount: float, idem_key: str):
    r = requests.post(
        f"{BASE}/v1/recharges",
        headers={
            "Authorization": AUTH,
            "Idempotency-Key": idem_key,
            "Content-Type": "application/json",
        },
        json={"service_id": service_id, "identifier": identifier, "amount": amount},
        timeout=(5, 40),
    )
    return r.status_code, r.json()


# La clave se genera UNA vez y se persiste con la orden.
if not orden.idem_key:
    orden.idem_key = str(uuid.uuid4())
    orden.save()

try:
    status, body = recargar(23, "8095551235", 100.00, orden.idem_key)
except requests.Timeout:
    # La recarga PUDO ejecutarse: se reintenta con LA MISMA clave.
    status, body = recargar(23, "8095551235", 100.00, orden.idem_key)

if status == 201:
    orden.cobrada(body["data"]["transaction_id"])
elif status == 202:
    # Importe retenido, no cobrado. Consultar, nunca reintentar con clave nueva.
    orden.verificando(body["error"]["details"]["transaction_id"])
elif status == 409:
    orden.reintentar_en(segundos=2)
elif status == 502:
    # Rechazo definitivo: el saldo ya fue devuelto.
    orden.fallida(body["error"]["code"])
else:
    orden.verificando(None)

Explorador OpenAPI

Especificación OpenAPI 3.0 completa. Puede probar cada endpoint desde aquí: pulse Authorize e introduzca api_key.api_secret (con el punto en medio) para usar las credenciales de prueba.

Descargas y soporte

RecursoDescripción
openapi.yamlEspecificación OpenAPI 3.0. Genere un cliente en su lenguaje.
Colección de PostmanTodos los endpoints listos para importar, con el sandbox preconfigurado.
API.mdEsta referencia en Markdown.
Reglas de negocioSaldo, límites, estados e idempotencia en detalle.

Documentación técnica interna

DocumentoContenido
Análisis del sistemaArquitectura, módulos, integraciones y deuda técnica.
Arquitectura de la APIDiseño de la v1 y el porqué de cada decisión.
Auditoría de seguridadHallazgos clasificados con evidencia.
Esquema de datosTablas, relaciones y anomalías.
DespliegueConfiguración, migraciones y monitoreo.
RemediacionesAcciones pendientes sobre el sistema existente.
Control de cambiosQué cambió, cuándo y con qué impacto.

Soporte

Al reportar una incidencia, incluya siempre el request_id de la respuesta y, si aplica, el transaction_id. Con eso localizamos la operación completa —incluida la conversación con el proveedor— en segundos.

Próximamente

  • POST /v1/transactions/{id}/reverse — anulación de recargas.
  • GET /v1/invoices y POST /v1/service-payments — consulta y pago de facturas.
  • Webhooks salientes para notificar cambios de estado sin necesidad de consultar.