openapi: 3.0.3

info:
  title: Recarga.do API
  version: 1.0.0
  description: |
    API pública de **Recarga.do** para recargas electrónicas y pago de servicios en
    República Dominicana.

    ## Modelo de integración

    API REST de **consulta directa**: su aplicación llama por HTTPS y recibe JSON.
    No se incrusta ningún `<iframe>` (la API no sirve HTML y responde
    `X-Frame-Options: DENY`), no hay redirecciones a pantallas de Recarga.do, no hay
    SDK y no hay sesiones ni cookies. La interfaz de usuario es íntegramente suya.

    `Access-Control-Allow-Origin: *` permite llamarla desde el navegador, pero el
    `api_secret` debe permanecer en su servidor, nunca dentro de una app distribuida.

    ## Empezar en 3 pasos

    1. Solicite sus credenciales (`api_key` y `api_secret`).
    2. Consulte su saldo: `GET /v1/balance`.
    3. Ejecute una recarga: `POST /v1/recharges` con el header `Idempotency-Key`.

    ## Ambiente de pruebas

    Toda cuenta con ambiente `development` opera contra un simulador: **no genera
    cargos reales ni recargas reales**. El escenario se elige con el último dígito
    del campo `identifier`:

    | Termina en | Resultado |
    |------------|-----------|
    | `0`        | 502 · `INVALID_PHONE` — el saldo se devuelve |
    | `1`        | 502 · `TRANSACTION_REJECTED` — el saldo se devuelve |
    | `2`        | 202 · `TRANSACTION_PENDING` — el saldo queda **retenido** |
    | cualquier otro | 201 · `SUCCESS` — el saldo se cobra |

    Verifique en qué ambiente está su token con `GET /v1/account`.

    ## Idempotencia — léalo antes de integrar

    `POST /v1/recharges` **exige** el header `Idempotency-Key` (se recomienda un UUID v4).

    Ante un timeout de red, **reintente con la misma clave**, nunca con una nueva. Una
    clave nueva es una operación nueva y puede recargar dos veces al cliente final.

    - Misma clave + mismo cuerpo, ya completada → devuelve la respuesta original con el
      header `Idempotent-Replay: true`. No se ejecuta una segunda recarga.
    - Misma clave, operación aún en curso → `409 IDEMPOTENT_REQUEST_IN_PROGRESS`.
    - Misma clave + cuerpo distinto → `422`.

    Las claves se conservan 24 horas.

    ## Manejo del saldo

    - `saldo_disponible` — gastable ahora mismo.
    - `saldo_retenido` — comprometido por operaciones en curso: ni cobrado ni gastable.
    - `saldo_actual` = `saldo_disponible` + `saldo_retenido`.

    El importe se **reserva antes** de enviar la orden al proveedor. Si el proveedor
    rechaza, se devuelve. Si el resultado es desconocido, permanece retenido hasta
    resolverse.

  contact:
    name: Soporte Recarga.do
    url: https://doc.recarga.do
  license:
    name: Uso privado — clientes de Recarga.do

servers:
  - url: https://api.recarga.do
    description: Producción

tags:
  - name: Sistema
    description: Estado y metadatos de la API. Los dos primeros son públicos.
  - name: Cuenta
    description: Datos de su empresa, saldo y límites.
  - name: Catálogo
    description: Servicios, categorías y paquetes disponibles.
  - name: Recargas
    description: Operaciones que mueven dinero. Requieren `Idempotency-Key`.
  - name: Transacciones
    description: Consulta de historial, detalle y recibos.

security:
  - bearerAuth: []

paths:

  /v1/health:
    get:
      tags: [Sistema]
      summary: Estado del servicio
      description: Endpoint público. Úselo para monitoreo.
      security: []
      responses:
        '200':
          description: Servicio operativo
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/RespuestaOk'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          status: { type: string, example: ok }

  /v1/info:
    get:
      tags: [Sistema]
      summary: Versión y entorno de la API
      security: []
      responses:
        '200':
          description: Información de la API
          content:
            application/json:
              example:
                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_c8cb01424da3ba89c613e0c7

  /v1/health/deep:
    get:
      tags: [Sistema]
      summary: Estado de dependencias
      description: Comprueba la base de datos y demás dependencias. Requiere autenticación.
      responses:
        '200':
          description: Todas las dependencias operativas
          content:
            application/json:
              example:
                success: true
                data:
                  status: ok
                  checks:
                    database: { status: ok, latency_ms: 12 }
                message: Estado de dependencias
                request_id: req_...
        '503': { description: Alguna dependencia degradada }
        '401': { $ref: '#/components/responses/NoAutenticado' }

  /v1/account:
    get:
      tags: [Cuenta]
      summary: Datos de su empresa y del token
      description: |
        Use este endpoint para saber si su token opera en **producción** o en
        **sandbox** (`ambiente: development`).
      responses:
        '200':
          description: Cuenta consultada
          content:
            application/json:
              example:
                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_12ab55e376d42ddd317a302f
        '401': { $ref: '#/components/responses/NoAutenticado' }

  /v1/balance:
    get:
      tags: [Cuenta]
      summary: Saldo, retenciones, límites y consumo
      responses:
        '200':
          description: Balance consultado
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/RespuestaOk'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Balance' }
              example:
                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_bd6b2e5397b3824003f021e0
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '404': { description: La empresa no tiene un balance configurado }

  /v1/categories:
    get:
      tags: [Catálogo]
      summary: Categorías con servicios activos
      responses:
        '200':
          description: Categorías disponibles
          content:
            application/json:
              example:
                success: true
                data:
                  - { id: 1, nombre: Telefonía, servicios: 7 }
                  - { id: 2, nombre: Electricidad, servicios: 10 }
                  - { id: 3, nombre: Agua, servicios: 5 }
                message: Categorías disponibles
                request_id: req_...
        '401': { $ref: '#/components/responses/NoAutenticado' }

  /v1/services:
    get:
      tags: [Catálogo]
      summary: Catálogo de servicios
      description: |
        Devuelve los servicios activos. El `id` de esta respuesta es el `service_id`
        que debe enviar en `POST /v1/recharges`.

        Respete siempre `monto_minimo` y `monto_maximo`; el valor `0` significa
        "sin límite por ese lado".
      parameters:
        - name: categoria
          in: query
          schema: { type: string }
          example: Telefonía
          description: Filtra por nombre exacto de categoría.
        - name: q
          in: query
          schema: { type: string }
          example: claro
          description: Busca por coincidencia parcial en el nombre del servicio.
      responses:
        '200':
          description: Servicios disponibles
          content:
            application/json:
              example:
                success: true
                data:
                  - id: 12
                    nombre: Claro Recarga
                    descripcion: Recarga de balance Claro
                    tipo: standard
                    categoria: Telefonía
                    categoria_id: 1
                    monto_minimo: 20.00
                    monto_maximo: 5000.00
                    acepta_anulacion: true
                message: 59 servicios disponibles
                request_id: req_...
        '401': { $ref: '#/components/responses/NoAutenticado' }

  /v1/services/{id}/packages:
    get:
      tags: [Catálogo]
      summary: Paquetes de datos de un servicio
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
          example: 12
      responses:
        '200':
          description: Paquetes disponibles
          content:
            application/json:
              example:
                success: true
                data:
                  - id: 5
                    codigo: CLARO_D1
                    nombre: Paquete diario 1GB
                    descripcion: 1GB por 24 horas
                    precio: 60.00
                    duracion: 1 día
                    datos: 1GB
                    minutos: 30
                    sms: 50
                message: 12 paquetes disponibles
                request_id: req_...
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '404': { description: El servicio no existe o está inactivo }

  /v1/recharges:
    post:
      tags: [Recargas]
      summary: Ejecutar una recarga
      description: |
        Ejecuta una recarga contra el proveedor y descuenta el importe de su saldo.

        **El header `Idempotency-Key` es obligatorio.** Ante un timeout, reintente con
        la misma clave; nunca genere una nueva para el mismo cobro.

        No envíe `empresa_id`: la empresa la determina su token. Si lo envía y no
        coincide, la petición se rechaza con `403`.
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema: { type: string, maxLength: 128 }
          example: 8fe9d681-a99e-4fda-b610-3c0a1f7e42bd
          description: Cadena única por operación. Se recomienda un UUID v4.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SolicitudRecarga' }
            examples:
              telefono:
                summary: Recarga de teléfono móvil
                value: { service_id: 12, identifier: '8095551234', amount: 100 }
              conTerminal:
                summary: Con referencia de caja
                value: { service_id: 12, identifier: '8095551234', amount: 100, terminal_id: CAJA-01 }
              sandboxExito:
                summary: Sandbox — éxito (termina en 5)
                value: { service_id: 12, identifier: '8095551235', amount: 100 }
              sandboxPendiente:
                summary: Sandbox — pendiente (termina en 2)
                value: { service_id: 12, identifier: '8095551232', amount: 100 }
      responses:
        '201':
          description: Recarga procesada exitosamente
          headers:
            Idempotent-Replay:
              schema: { type: string }
              description: Presente con valor `true` si es la repetición de una operación anterior.
          content:
            application/json:
              example:
                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
        '202':
          description: |
            Resultado desconocido. **El importe queda retenido, no cobrado.**
            No reintente a ciegas: consulte la transacción indicada.
          content:
            application/json:
              example:
                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
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '403':
          description: El `empresa_id` enviado no corresponde al token
        '409':
          description: |
            Ya hay una operación en curso con la misma `Idempotency-Key`.
            Espere y consulte su estado; no genere una clave nueva.
          content:
            application/json:
              example:
                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_...
        '422':
          description: Datos inválidos, saldo insuficiente o límite excedido
          content:
            application/json:
              examples:
                faltaClave:
                  summary: Falta Idempotency-Key
                  value:
                    success: false
                    error:
                      code: VALIDATION_ERROR
                      message: El header Idempotency-Key es obligatorio en operaciones financieras.
                      details: { Idempotency-Key: requerido }
                    request_id: req_...
                saldoInsuficiente:
                  summary: Saldo insuficiente
                  value:
                    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_...
                limiteExcedido:
                  summary: Límite del período excedido
                  value:
                    success: false
                    error:
                      code: LIMIT_EXCEEDED
                      message: Se excedió el límite diario configurado.
                      details: { periodo: diario, limite: 50000.00, consumido: 49500.00 }
                    request_id: req_...
        '429': { $ref: '#/components/responses/RateLimit' }
        '502':
          description: El proveedor rechazó la operación. **El importe fue devuelto a su saldo.**
          content:
            application/json:
              example:
                success: false
                error: { code: INVALID_PHONE, message: 'Número inválido.' }
                request_id: req_2fe16c3c610ef66635a798ac

  /v1/transactions:
    get:
      tags: [Transacciones]
      summary: Historial de transacciones
      description: Devuelve únicamente transacciones de su empresa.
      parameters:
        - { name: page, in: query, schema: { type: integer, default: 1, minimum: 1 } }
        - { name: per_page, in: query, schema: { type: integer, default: 50, minimum: 1, maximum: 200 } }
        - name: status
          in: query
          schema: { type: string, enum: [SUCCESS, FAILED, REVERSED, PENDING, PROCESSING] }
        - { name: date_from, in: query, schema: { type: string, format: date }, example: '2026-08-01' }
        - { name: date_to, in: query, schema: { type: string, format: date }, example: '2026-08-12' }
        - name: identifier
          in: query
          schema: { type: string }
          description: Coincidencia exacta. Útil para localizar una recarga tras un timeout.
      responses:
        '200':
          description: Transacciones encontradas
          content:
            application/json:
              example:
                success: true
                data:
                  items:
                    - 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'
                  pagination: { page: 1, per_page: 50, total: 1284, total_pages: 26 }
                message: 1284 transacciones encontradas
                request_id: req_...
        '401': { $ref: '#/components/responses/NoAutenticado' }

  /v1/transactions/{id}:
    get:
      tags: [Transacciones]
      summary: Detalle de una transacción
      description: Una transacción de otra empresa devuelve `404`, no `403`.
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer }, example: 48849 }
      responses:
        '200':
          description: Transacción consultada
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/RespuestaOk'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Transaccion' }
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '404': { description: Transacción no encontrada }

  /v1/transactions/{id}/receipt:
    get:
      tags: [Transacciones]
      summary: Recibo de una transacción
      description: Solo disponible para transacciones en estado `SUCCESS`.
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer }, example: 48849 }
      responses:
        '200':
          description: Recibo generado
          content:
            application/json:
              example:
                success: true
                data:
                  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
                message: Recibo generado
                request_id: req_...
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '404': { description: Transacción no encontrada }
        '409': { description: La transacción no está completada }

components:

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Envíe `Authorization: Bearer <api_key>.<api_secret>` — la clave y el secreto
        separados por un punto.

        Alternativa equivalente con headers separados:
        `X-Api-Key: <api_key>` y `X-Api-Secret: <api_secret>`.

  responses:
    NoAutenticado:
      description: Credenciales ausentes o inválidas
      content:
        application/json:
          example:
            success: false
            error: { code: UNAUTHENTICATED, message: Credenciales ausentes o inválidas. }
            request_id: req_eff8794682d577b7f23b4ddd
    RateLimit:
      description: Demasiadas solicitudes
      headers:
        X-RateLimit-Limit: { schema: { type: integer } }
        X-RateLimit-Remaining: { schema: { type: integer } }
        X-RateLimit-Reset: { schema: { type: integer } }
      content:
        application/json:
          example:
            success: false
            error:
              code: RATE_LIMIT_EXCEEDED
              message: Demasiadas solicitudes. Intente más tarde.
              details: { limite_por_minuto: 120, retry_after: 24 }
            request_id: req_...

  schemas:

    RespuestaOk:
      type: object
      required: [success, data, message, request_id]
      properties:
        success: { type: boolean, example: true }
        data: { description: Contenido de la respuesta }
        message: { type: string, example: Operación realizada correctamente }
        request_id:
          type: string
          example: req_9f2c1ab34de5f6a7b8c9d0e1
          description: Identificador único. Cítelo en cualquier consulta de soporte.

    RespuestaError:
      type: object
      required: [success, error, request_id]
      properties:
        success: { type: boolean, example: false }
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string, example: INSUFFICIENT_BALANCE }
            message: { type: string }
            details: { type: object }
        request_id: { type: string }

    Balance:
      type: object
      properties:
        saldo_actual: { type: number, format: double, description: Disponible + retenido }
        saldo_disponible: { type: number, format: double, description: Gastable ahora mismo }
        saldo_retenido: { type: number, format: double, description: Comprometido por operaciones en curso }
        tipo_credito: { type: string, enum: [prepago, postpago] }
        alerta_saldo_bajo: { type: number, format: double }
        limites:
          type: object
          properties:
            diario: { type: number, format: double }
            semanal: { type: number, format: double }
            mensual: { type: number, format: double }
          description: El valor 0 significa "sin límite".
        consumo_periodo:
          type: object
          properties:
            diario: { type: number, format: double }
            semanal: { type: number, format: double }
            mensual: { type: number, format: double }
        saldo_bajo: { type: boolean }
        moneda: { type: string, example: DOP }
        actualizado_en: { type: string }

    SolicitudRecarga:
      type: object
      required: [service_id, identifier, amount]
      properties:
        service_id:
          type: integer
          example: 12
          description: Obtenido de `GET /v1/services`.
        identifier:
          type: string
          minLength: 3
          maxLength: 100
          example: '8095551234'
          description: Teléfono, número de contrato o NIC, según el servicio.
        amount:
          type: number
          format: double
          example: 100
          description: Mayor que cero, máximo 2 decimales, dentro del rango del servicio.
        terminal_id:
          type: string
          maxLength: 50
          example: CAJA-01
          description: Referencia opcional de caja o terminal.

    Transaccion:
      type: object
      properties:
        transaction_id: { type: integer, example: 48849 }
        xid: { type: string, example: RD-b1d02d05e291c41716ff89bf }
        status: { type: string, enum: [SUCCESS, FAILED, REVERSED, PENDING, PROCESSING] }
        service:
          type: object
          properties:
            id: { type: integer }
            nombre: { type: string }
        identifier: { type: string }
        amount: { type: number, format: double }
        currency: { type: string, example: DOP }
        auth_code: { type: string, nullable: true }
        reference: { type: string, nullable: true }
        terminal_id: { type: string, nullable: true }
        message: { type: string, nullable: true }
        created_at: { type: string }
        processed_at: { type: string, nullable: true }
