Saltearse al contenido

Cobros con tarjeta

Host Factura no es sólo facturación: también cobra. El módulo de pagos genera links de cobro, cobra con tarjetas que el cliente ya tiene guardadas, maneja suscripciones recurrentes y —esto es lo propio de Host Factura— puede emitir el CFE del cobro cuando el cobro se confirma.

Cobrar y facturar son dos cosas separadas. Un cobro con tarjeta no emite ningún comprobante salvo que lo pidas explícitamente, y podés emitirlo después. Para cobrar no necesitás saber nada de facturación.

Las preferencias de cobro de la empresa las administra la propia empresa desde el panel, no la integración. Lo que tu sistema necesita saber al arrancar —si la cuenta puede cobrar y si emite comprobante fiscal— viene en el diagnóstico de la credencial:

GET /api/v1/echo
{
"data": {
"facturacionEnabled": true,
"habilitado": true,
"politicaFiscal": "facturar_al_cobrar",
"mediosHabilitados": ["cards", "banks", "paynet"],
"notificarComprador": true
}
}
CampoQué significa
facturacionEnabledSi la cuenta emite CFE. Con false, la única política posible es solo_registrar
habilitadoInterruptor de cobros de la empresa. false corta links y cobros nuevos
politicaFiscalQué se emite cuando el cobro se confirma (ver más abajo)
mediosHabilitadoscards, banks, paynet — lo que se ofrece en la página de pago
notificarCompradorSi se le avisa por correo al comprador cuando su pago se registra

El camino por defecto: el comprador paga en una página segura externa y vos no tocás datos de tarjeta.

POST /api/v1/pagos/links
Authorization: Bearer <API_KEY>
Idempotency-Key: orden-12345-link-1
Content-Type: application/json
{
"monto": "1234.56",
"moneda": "UYU",
"returnUrl": "https://tienda.ejemplo.com/pago/retorno",
"referenciaIntegrador": "orden-12345",
"politicaFiscal": "facturar_al_cobrar",
"borradorCfe": {
"tipo": 101,
"items": [{ "name": "Servicio mensual", "quantity": 1, "price": 1234.56, "billingIndex": 3 }]
}
}
{
"data": {
"solicitud": {
"id": "8f14e45f-ceea-467a-9f5a-2b1c3d4e5f60",
"referenciaIntegrador": "orden-12345",
"estado": "pendiente",
"monto": "1234.56",
"moneda": "UYU",
"tieneBorrador": true
},
"redirectUrl": "https://pagos.ejemplo.com/checkout/abc123",
"gatewayToken": "abc123"
},
"meta": { "reusada": false }
}

Redirigís al comprador a redirectUrl (o se lo mandás por correo con POST /v1/pagos/links/{id}/enviar-email).

Si mandás borradorCfe, el comprobante se valida al crear el link (ítems, CAE vigente, certificado, simulación de emisión) y se emite recién cuando el cobro se confirma. Es deliberado: un CFE quema su número aunque después se anule.

Si la validación falla, la respuesta es 422 PAGOS_BORRADOR_INVALIDO con la lista de problemas en error.details.detalles. Corregí y volvé a crear el link: es mucho más barato que descubrirlo cuando el cliente ya pagó.

El campo que hace operable todo lo demás.

  • Es único por cuenta: un segundo link con la misma referencia y otra Idempotency-Key se rechaza con 409 PAGOS_REFERENCIA_DUPLICADA. Es la defensa contra cobrar dos veces la misma orden.
  • Sirve como filtro exacto en GET /v1/pagos/links?referenciaIntegrador=… y GET /v1/pagos?referenciaIntegrador=….
  • En una cuenta sin facturación electrónica es el único identificador que el operador reconoce: no hay número de CFE con el que cruzar.

Para cobros desatendidos: tu backend cobra sin que el comprador esté presente. Son cuatro llamadas:

POST /api/v1/clientes → clienteId (una vez por cliente)
POST /api/v1/pagos/clientes/{clienteId}/tarjetas/alta (una vez por tarjeta)
GET /api/v1/pagos/clientes/{clienteId}/tarjetas → tarjetaId
POST /api/v1/pagos/cobrar { tarjetaId, monto, moneda } (cada cobro)

Toda tarjeta pertenece a un cliente. Lo creás (o lo recuperás) con tu propio identificador:

POST /api/v1/clientes
{
"nombre": "Ana Pérez",
"referenciaIntegrador": "usuario-4821",
"email": "ana@ejemplo.com"
}

Es un upsert: con la misma referenciaIntegrador devuelve siempre el mismo cliente, así que no necesitás guardar nuestro id. El documento es opcional: para guardarle una tarjeta y cobrarle no hace falta. Ver Clientes.

POST /api/v1/pagos/clientes/{clienteId}/tarjetas/alta

Cuerpo opcional: cliente con el contacto del titular (firstName, lastName, email, phone, documentNumber) para el antifraude de la red de pagos. Devuelve registrationUrl, metodo y campos.

La respuesta no es la confirmación: la tarjeta aparece en el listado cuando llega el aviso de la red de pagos, y ahí se emite el webhook tarjeta.enrolada.

GET /api/v1/pagos/clientes/{clienteId}/tarjetas?soloElegiblesRecurrente=true

Nunca se devuelve PAN, CVV ni el token de la tarjeta: sólo mascara, brandCode, brand, dueDate y el id con el que se cobra.

POST /api/v1/pagos/cobrar
Idempotency-Key: orden-12345-cobro-1
{
"tarjetaId": "3a2f1e0d-9c8b-4a7f-8e6d-5c4b3a2f1e0d",
"monto": "1500.00",
"moneda": "UYU",
"descripcion": "Orden 12345"
}
{
"data": {
"solicitudId": "8f14e45f-ceea-467a-9f5a-2b1c3d4e5f60",
"pagoId": "3c9a7b1d-2e4f-5a6b-7c8d-9e0f1a2b3c4d",
"yaEstabaCobrado": false,
"facturado": false
},
"meta": { "idempotencyKey": "orden-12345-cobro-1" }
}

El cliente sale de la tarjeta: no se manda. El cuerpo es una lista cerrada: cualquier otro campo es un 422.

Idempotency-Key es obligatoria acá. Sin clave, cada llamada es un cobro nuevo: un reintento por timeout de red te cobraría dos veces al cliente. Sin cabecera, 400 PAGOS_IDEMPOTENCY_KEY_REQUERIDA; con un formato inválido (8 a 128 caracteres de A-Za-z0-9._:~-), 400 PAGOS_IDEMPOTENCY_KEY_INVALIDA. Nunca se deriva ni se inventa una. Usá una clave distinta para cada cobro distinto: la clave de un cobro no vence.

Sin nada más, el cobro no emite comprobante (facturado: false). Si querés que el CFE salga junto con el cobro, agregá el bloque factura:

{
"tarjetaId": "3a2f1e0d-9c8b-4a7f-8e6d-5c4b3a2f1e0d",
"monto": "1500.00",
"moneda": "UYU",
"factura": {
"tipo": "contado",
"borrador": {
"tipo": 101,
"items": [{ "name": "Orden 12345", "quantity": 1, "price": 1229.51, "billingIndex": 3 }]
}
}
}
factura.tipoQué emiteDato
contadoe-Ticket o e-Factura por lo cobradoborrador: la misma forma que el cuerpo de emitir un CFE
reciboe-Recibo que salda facturas ya emitidasfacturas: [{ "cfeEmitidoId", "monto" }], hasta 40; la suma tiene que dar lo cobrado

El comprobante se valida antes de cobrar: si no se puede emitir, no se cobra y la respuesta es 422 PAGOS_BORRADOR_INVALIDO con los problemas en error.details.detalles. Pedir factura exige también el alcance cfe:emitir, y en una empresa sin facturación electrónica da 409 PAGOS_POLITICA_REQUIERE_FACTURACION.

HTTPQué pasóQué hacer
200Cobrado. yaEstabaCobrado: true = era el mismo cobro de un intento anteriorÉxito
400Falta la clave de idempotencia o es inválidaCorregir el request
403Pediste factura y la credencial no tiene cfe:emitirCobrar sin factura, o ampliar la credencial
409Tarjeta no elegible, cobros apagados, clave reciclada, o factura en una empresa que no emiteCorregir
422Rechazo definitivo (declinada, moneda no soportada), o factura no emitible (no se cobró)No reintentar igual
503Fallo transitorio, o el cobro quedó en_verificacionReintentar con la misma clave
POST /api/v1/pagos/planes → el monto y la periodicidad viven en el PLAN
POST /api/v1/pagos/suscripciones/invitaciones → cliente + plan; devuelve la url a compartir

La invitación es la única forma de que nazca una suscripción: la empresa invita y el cliente contrata desde el link eligiendo su tarjeta. No se puede suscribir a alguien con una tarjeta que ya tenés guardada — quien paga tiene que consentir el débito recurrente, y ese consentimiento se da una sola vez, en el link.

intervalo: weekly, monthly, quarterly, semiannual, annual.

Pausar, reanudar y cancelar: el estado lo define la red de pagos. Se pide la transición y se guarda lo que responde; no se da por pausada por optimismo. Una transición rechazada devuelve 409 PAGOS_SUSCRIPCION_TRANSICION_INVALIDA.

Cancelar también deshabilita la factura recurrente vinculada: dejarla activa seguiría facturando un servicio que ya nadie paga.

El precio de una suscripción no se edita: se programa.

POST /api/v1/pagos/suscripciones/{id}/precio → { "monto": "1800.00", "motivo": "Ajuste anual" }
DELETE /api/v1/pagos/suscripciones/{id}/precio → cancela el cambio que todavía no rige

No se manda la fecha de vigencia: la calcula el servidor con el preaviso de la cuenta. Si quien arma el request pudiera elegirla, el preaviso sería decorativo. La respuesta trae vigenteDesde y rigeEnElProximoCobro: eso es lo que hay que comunicarle al cliente.

PolíticaQué se emite al confirmarse el cobro
facturar_al_cobrare-Ticket / e-Factura al contado, por el monto cobrado
recibo_al_cobrarCFE de cobranza (e-Recibo). Para cobrar una factura a crédito ya emitida
solo_registrarNada. Es la única posible si la cuenta no emite CFE

Quién decide la política:

  • Cobro con tarjeta: el propio cobro, con el bloque factura (contado = facturar_al_cobrar, recibo = recibo_al_cobrar). Sin bloque, solo_registrar. La configuración de la empresa no se aplica.
  • Link de cobro: la del link manda; si no tiene, la de la empresa.

Además:

  • El comprobante sale cuando el cobro se confirma, no cuando se crea el link.
  • Si el importe informado no cuadra con el pedido, la plata se registra igual pero no se dispara la emisión: queda en pendiente_facturacion para que una persona lo resuelva con POST /v1/pagos/{id}/facturar.
  • reciboCfeId no garantiza que DGI lo aceptó. Un comprobante puede quedar emitido y rechazado: el estado real se consulta con GET /v1/cfe/info/{id}.
POST /api/v1/pagos/{id}/facturar → alcance cfe:emitir
{
"factura": { "tipo": "contado", "borrador": { "tipo": 101, "items": [ … ] } }
}

El mismo bloque factura que en el cobro, validado antes de emitir. Sirve para dos cosas: emitir el comprobante de un cobro que se hizo sin él, y corregir y reintentar uno que quedó en pendiente_facturacion (sin cuerpo, reintenta con lo que ya tenía). Emite un CFE real y consume numeración.

GET /api/v1/pagos/{id}/comprobante.pdf

No es un CFE: no numera, no tiene CAE ni QR, no va a DGI y lleva la leyenda “Comprobante de pago. No es un comprobante fiscal (CFE)”. Es el único papel que una cuenta sin facturación electrónica le puede dar a su comprador. Si la cuenta factura, lo que corresponde entregar es la representación impresa del CFE.

en_verificacion: el estado que hay que tratar bien

Sección titulada «en_verificacion: el estado que hay que tratar bien»

La red de pagos no contestó la captura. El cobro puede estar hecho. Se emite el webhook pago.en_verificacion.

Qué NO hacerPor qué
Entregar el productoTodavía no hay cobro confirmado
Declararlo impagoPuede haber plata adentro
Reintentar con otra Idempotency-KeyEso sí cobra dos veces
Mostrar un botón “reintentar cobro”Misma razón

Qué hacer: esperar el pago.confirmado o el pago.fallido que resuelve el job de conciliación, o reintentar con la misma clave (viene en datos.idempotencyKey del evento).

Un cobro en en_verificacion lo resuelve la conciliación automática, que es la única que sabe registrarlo con todas sus salvaguardas. Si uno queda trabado ahí más tiempo del razonable, es un caso de soporte, no algo que la integración deba destrabar por su cuenta.

EstadoSignifica
borradorCreada, sin link de pago todavía
pendienteLink emitido, esperando que el comprador pague
en_verificacionAmbiguo: puede estar cobrado. Nunca dispara la emisión del CFE
pagadaLlegó el aviso y el importe cerró. Es lo que dispara la emisión
expiradaVenció expiraEn. Creá un link nuevo
anuladaAnulada. Ya no se puede pagar
fallidaEl cobro falló de forma definitiva
EstadoSignifica
aprobadoLa plata entró y no hay comprobante: el cobro no lo pidió. Se puede facturar después
pendiente_facturacionPlata adentro y la emisión falló. Trae intentosFacturacion y ultimoErrorFacturacion
facturadoHay reciboCfeId. No garantiza que DGI lo aceptó
anuladoRevertido, total o parcialmente
fallidoNo prosperó

El campo anulable del cobro es la fuente de verdad para saber si POST /v1/pagos/{id}/anular aplica. Un cobro con comprobante fiscal emitido no es anulable: la reversa de un CFE es una nota de crédito, porque el número ya se consumió.

Copia del estado en la red de pagos: active, past_due, paused, cancelled. Ortogonal al estado, requiereCobroManual significa que la tarjeta no sirve para cobro desatendido: no es un fallo puntual, es estructural mientras no se cambie la tarjeta.

GET /api/v1/pagos?estado=pendiente_facturacion → el comprobante se pidió y falló
GET /api/v1/pagos?estado=aprobado → cobros sin comprobante (no se pidió)
GET /api/v1/pagos?referenciaIntegrador=orden-12345 → el cobro de TU orden
GET /api/v1/pagos?desde=2026-09-01&hasta=2026-09-30 → la ventana que quieras conciliar

pendiente_facturacion es la consulta de alarma: plata cobrada cuyo comprobante se pidió y todavía no salió, sin acotar a ninguna ventana — acotarla escondería justamente el caso grave. Los montos son el bruto cobrado, nunca neteados por la comisión del adquirente.

  • GET /v1/echo al arrancar: conocés si la cuenta puede cobrar y si emite comprobante fiscal.
  • Toda orden lleva referenciaIntegrador única.
  • Todo POST que mueve plata manda Idempotency-Key derivada de la orden, no aleatoria.
  • Los reintentos usan la misma clave.
  • Los clientes se crean con POST /v1/clientes y tu referenciaIntegrador: nunca un id copiado del panel.
  • Si la empresa factura, cada cobro con tarjeta que sea una venta lleva factura o se factura después.
  • en_verificacion no entrega producto ni se reintenta con clave nueva.
  • Hay un receptor de webhooks con verificación de firma y deduplicación.
  • pago.pendiente_facturacion llega a una persona: es plata cobrada sin comprobante.
  • Se ramifica por error.code, nunca por error.message.

La referencia completa de cada endpoint, con todos los campos y ejemplos, está en la Referencia OpenAPI.