Cobro de facturas con el usuario delante
Cobra una factura con el usuario delante, con tarjeta guardada o nueva, y consulta el historial de intentos de cobro.
Actualizado el . 5 min de lectura
Incluido en los planes Profesional y Avanzado. Ver precios
Con estos endpoints tu aplicación cobra una factura existente mientras
el usuario está delante, y consulta los intentos de cobro anteriores.
Están bajo /external/v1/invoices/:id/*, junto con el resto de la API
de facturación.
No los confundas con el cobro sin nadie delante: son dos endpoints distintos y no se pueden intercambiar.
Dos formas de cobrar una factura
POST /invoices/:id/pay
Con el usuario delante
POST /invoices/:id/charge
Sin nadie delante
Plan y permisos
Antes que el permiso de la aplicación se comprueba el plan de tu
organización. Sin los aplicativos Finanzas y Pagos con tarjeta
(Stripe), la respuesta es 403 con errorCode: 'MODULE_NOT_ENABLED'.
| Endpoint | Aplicativos del plan | Permiso de la API (scope) |
|---|---|---|
POST /external/v1/invoices/:id/pay | Finanzas y Pagos con tarjeta (Stripe) | billing:charge |
GET /external/v1/invoices/:id/payment-intents | Finanzas y Pagos con tarjeta (Stripe) | billing:payment-methods:read |
Sin el permiso adecuado, los endpoints responden 403 INSUFFICIENT_SCOPE
con un mensaje genérico que no dice cuál falta, desde la primera
llamada y sin periodo de gracia. El catálogo completo está en
Permisos de la API.
billing:charge mueve dinero real de la tarjeta del cliente, así que
solo lo concede un Super Administrador, con un motivo de al menos
veinte caracteres que queda en la auditoría. La consulta del historial
de cobros, en cambio, solo lee y exige billing:payment-methods:read:
una aplicación puede revisar el estado de los cobros sin poder
iniciarlos.
Iniciar un cobro
/api/external/v1/invoices/:id/payCrea un PaymentIntent sobre la factura. Con tarjeta guardada lo confirma con el usuario presente; sin ella devuelve el clientSecret para Stripe Elements. Permiso: billing:charge.
Cuerpo
- userIdstringobligatorio
- externalId del usuario propietario de la factura.
- paymentMethodIdstring
- Identificador en UTILIA OS de un método de pago guardado. Si lo omites, se devuelve el clientSecret para una tarjeta nueva.
- saveCardboolean
- Solo al cobrar con tarjeta nueva (sin paymentMethodId). Con true, la tarjeta queda guardada tras el cobro. Guardarla NO autoriza cobros futuros: para eso hace falta una autorización de cobro.
Ejemplo: cobro con tarjeta guardada
curl -X POST https://os.utilia.ai/api/external/v1/invoices/inv-uuid/pay \
-H "X-Api-Key: utl_live_..." \
-H "Content-Type: application/json" \
-d '{
"userId": "user_01HXYZ",
"paymentMethodId": "pm-uuid-1"
}'Respuesta (200):
{
"clientSecret": "pi_..._secret_...",
"paymentIntentId": "pi-utilia-uuid",
"status": "succeeded",
"alreadySucceeded": true,
"publishableKey": "pk_live_...",
"stripeAccountId": "acct_..."
}Con alreadySucceeded: true, el cobro se confirmó sin autenticación adicional y tu interfaz no tiene que hacer nada más. La factura pasa a PAID en cuanto UTILIA OS recibe la confirmación de la pasarela, normalmente en segundos.
Ejemplo: cobro con tarjeta nueva
curl -X POST https://os.utilia.ai/api/external/v1/invoices/inv-uuid/pay \
-H "X-Api-Key: utl_live_..." \
-H "Content-Type: application/json" \
-d '{
"userId": "user_01HXYZ",
"saveCard": true
}'Respuesta (200):
{
"clientSecret": "pi_..._secret_...",
"paymentIntentId": "pi-utilia-uuid",
"status": "requires_payment_method",
"alreadySucceeded": false,
"publishableKey": "pk_live_...",
"stripeAccountId": "acct_..."
}En este caso tu interfaz recibe el clientSecret y monta Stripe Elements:
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';
const stripePromise = loadStripe(publishableKey, { stripeAccount: stripeAccountId });
<Elements stripe={stripePromise} options={{ clientSecret }}>
<PayForm />
</Elements>
function PayForm() {
const stripe = useStripe();
const elements = useElements();
async function handleSubmit() {
const result = await stripe!.confirmPayment({
elements: elements!,
confirmParams: { return_url: window.location.href },
redirect: 'if_required',
});
if (result.error) {
// mostrar error al usuario
}
// Si no hay error, el aviso INVOICE_PAID confirmará el cobro
}
return <form onSubmit={handleSubmit}><PaymentElement /><button>Pagar</button></form>;
}Qué hacer según la respuesta
¿Qué hago con la respuesta de pay?
¿Qué trae la respuesta?
alreadySucceeded: true:
Pago hecho
No hagas nada más. La factura pasa a PAID en cuanto la pasarela lo confirma.requires_action:
Falta 3D Secure
Llama a stripe.confirmCardPayment() con el clientSecret.requires_payment_method:
Tarjeta nueva
Monta Stripe Elements con el clientSecret para que el usuario la introduzca.processing:
En curso
También con requires_confirmation: la pasarela lo está procesando. Espera el aviso.
Con canceled o failed, el cobro no ha salido; el motivo está en el
campo errorCode del historial de intentos de cobro. En todos los
casos, lo que confirma el pago es el aviso INVOICE_PAID.
Errores del cobro
403 BILLING_NOT_ENABLED: la facturación de la aplicación está apagada.409 STRIPE_ACCOUNT_NOT_READY: tu organización no tiene la cuenta Stripe conectada o no tiene los cobros habilitados (en algunos casos llega con400o503).409 BILLING_CURRENCY_NOT_SUPPORTED: la cuenta de cobro no admite la divisa de la factura.404 EXTERNAL_USER_NOT_FOUND: eluserIdindicado no está registrado en la aplicación.404 INVOICE_NOT_ACCESSIBLE: la factura no existe, no es de esta aplicación o no es del usuario indicado (siempre la misma respuesta, para no revelar si existe).400 INVOICE_STATE_INVALID: la factura no se puede cobrar (borrador o sin importe pendiente).400 INVOICE_ALREADY_CANCELLED: la factura está anulada.400 INVOICE_ALREADY_PAID: la factura ya estaba pagada.404 PAYMENT_METHOD_NOT_FOUND: el método guardado no pertenece al usuario.400 PAYMENT_METHOD_INACTIVE: la tarjeta elegida no está activa (caducada o desvinculada). Añade una nueva.400 VALIDATION_ERROR: la pasarela rechazó el cobro (tarjeta rechazada, fondos insuficientes, CVC incorrecto, 3D Secure necesario…). El cuerpo incluye el mensaje de la pasarela, recortado. El código original (card_declined,insufficient_funds,expired_card…) no viaja en este error: está en el campoerrorCodedel historial de intentos de cobro.
Historial de intentos de cobro
/api/external/v1/invoices/:id/payment-intentsDevuelve los intentos de cobro (PaymentIntents) de una factura, del más reciente al más antiguo. Permiso: billing:payment-methods:read.
Parámetros
- idpathobligatorio
- Identificador de la factura en UTILIA OS.
- userIdqueryobligatorio
- externalId del usuario.
Respuesta (200)
[
{
"id": "pi-utilia-1",
"amount": 53.45,
"currency": "EUR",
"status": "succeeded",
"initiatedBy": "CUSTOMER",
"createdAt": "2026-04-20T11:22:00.000Z",
"succeededAt": "2026-04-20T11:22:33.000Z",
"failedAt": null,
"errorCode": null,
"errorMessage": null
},
{
"id": "pi-utilia-0",
"amount": 53.45,
"currency": "EUR",
"status": "failed",
"initiatedBy": "CUSTOMER",
"createdAt": "2026-04-20T11:10:00.000Z",
"succeededAt": null,
"failedAt": "2026-04-20T11:10:05.000Z",
"errorCode": "card_declined",
"errorMessage": "Tu banco ha rechazado la tarjeta."
}
]Campos
- idstringobligatorio
- Identificador del intento de cobro en UTILIA OS.
- amountdecimalobligatorio
- Importe del intento.
- currencystringobligatorio
- Divisa (código ISO 4217).
- statusenumobligatorio
- requires_payment_method, requires_action, processing, succeeded, canceled o failed.
- initiatedByenumobligatorio
- CUSTOMER (portal o aplicación externa), STAFF (panel de UTILIA OS) o SYSTEM_RECURRING (cobro automático programado).
- createdAtdatetimeobligatorio
- Cuándo se creó el intento.
- succeededAtdatetime
- Cuándo se confirmó el cobro.
- failedAtdatetime
- Cuándo se detectó el fallo.
- errorCodestring
- Código del error de la pasarela (card_declined, expired_card…).
- errorMessagestring
- Mensaje de error ya traducido al castellano.
Flujo recomendado para tu interfaz
Cobro con el usuario delante y tarjeta guardada
Participan: Tu usuario, Tu aplicación, UTILIA OS.
De Tu usuario a Tu aplicación:
Abre la factura y pulsa Pagar
Preselecciona su tarjeta predeterminada (GET /payment-methods).De Tu aplicación a UTILIA OS:
POST /api/external/v1/invoices/:id/pay
Con el paymentMethodId de la tarjeta elegida.De UTILIA OS a Tu aplicación:
alreadySucceeded: true (respuesta)
El banco no ha pedido nada más.De UTILIA OS a Tu aplicación:
Aviso INVOICE_PAID
Actualiza tu estado con el aviso.
Si la respuesta trae otra cosa, sigue el árbol de Qué hacer según la respuesta.
Cuadrar con los avisos
El aviso INVOICE_PAID es la fuente de verdad: da una factura por pagada solo cuando lo recibas y su firma HMAC sea válida. Tu interfaz puede mostrar «Procesando pago» mientras espera, pero lo que guardes en tu base de datos tiene que esperar al aviso.
Cabeceras y límite de uso
Los mismos que el resto de endpoints /external/v1/*: X-Api-Key y 60 peticiones por minuto por clave.