API REST
El contrato entre tu instancia y el plano de control. Todo el dinero que mueve Payezz pasa por aquí.
La ruta de cargos existe y compila; le faltan sus tres consultas a base de datos, así que hoy responde con error.
1. Autenticación
Todas las rutas cuelgan de https://api.payezz.app/v1/. Cada instancia lleva un par instance_id más instance_secret, inyectado como secreto en el contenedor durante la provisión y rotable desde el plano de control. Tú no tienes que gestionarlo: tu instancia ya lo tiene.
Cada petición firma HMAC-SHA256(secret, timestamp + "." + cuerpo_crudo). El cuerpo que se firma es el crudo, byte a byte, no el resultado de volver a serializar el objeto: dos serializaciones distintas del mismo JSON producen firmas distintas.
POST /v1/payments/charges HTTP/1.1
Host: api.payezz.app
Content-Type: application/json
Payezz-Instance: inst_7f3a91c0
Payezz-Timestamp: 1789459200
Payezz-Signature: v1=6b1f...c39a
Idempotency-Key: renewal:svc_4821:2026-10
Payezz-Api-Version: 2026-09-01Reglas que no se negocian en la verificación:
- Comparación en tiempo constante. Comparar secretos con el operador de desigualdad filtra información por el tiempo de respuesta.
- Ventana de reloj de más o menos 300 segundos. Fuera de ventana,
401. - Caché de las firmas ya vistas dentro de la ventana, para bloquear repeticiones.
Payezz-Api-Versionobligatoria en todas las peticiones.
2. Clientes
POST /v1/customers crea o recupera el cliente en tu cuenta conectada. El mapeo entre tu identificador y el de Stripe lo guarda el plano de control, y la respuesta siempre trae el identificador vigente, aunque el anterior se hubiera borrado.
// petición
{
"external_id": "cli_918",
"email": "ana@ejemplo.com",
"name": "Ana Ruiz",
"metadata": { "instance_customer_id": "918" }
}
// respuesta 200
{ "customer_id": "cus_QZ...", "created": true }3. Cobro con el cliente delante
POST /v1/payments/intents es el cobro de un checkout: hay alguien en la pantalla que puede autenticarse si su banco lo pide. Devuelve el client_secret con el que tu instancia monta el formulario de pago, además del desglose de comisión y el identificador de la entrada del libro de comisiones.
// petición
{
"customer_id": "cus_QZ...",
"amount_minor": 1499,
"currency": "eur",
"purpose": "new_order",
"reference": { "kind": "invoice", "id": "inv_2026_0417" },
"description": "Plan Estándar, octubre 2026",
"statement_descriptor_suffix": "ACME",
"metadata": { "order_id": "4821" },
"automatic_payment_methods": true,
"return_url": "https://tienda.acme.com/checkout/volver"
}
// respuesta 200
{
"payment_intent_id": "pi_3Q...",
"status": "requires_payment_method",
"client_secret": "pi_3Q..._secret_...",
"publishable_key": "pk_live_...",
"stripe_account": "acct_1Q...",
"fee": { "pct_bps": 190, "fixed_minor": 20,
"total_minor": 48, "currency": "eur" },
"ledger_entry_id": "cle_01J..."
}Los valores admitidos en purpose son new_order, renewal, invoice, proration, reactivation, addon y manual_admin. No es decoración: es lo que permite conciliar después qué parte de la facturación viene de cada sitio.
4. Cobro sin el cliente delante
POST /v1/payments/charges es la renovación: se cobra contra un método de pago guardado, sin nadie mirando. Tiene tres desenlaces y los tres hay que tratarlos.
// petición
{
"customer_id": "cus_QZ...",
"payment_method_id": "pm_1Q...",
"amount_minor": 1499,
"currency": "eur",
"purpose": "renewal",
"reference": { "kind": "service", "id": "svc_4821" },
"description": "Renovación, Plan Estándar",
"metadata": { "service_id": "4821", "period": "2026-10" }
}
// 200, cobrado
{ "payment_intent_id": "pi_3Q...", "status": "succeeded",
"fee": { "total_minor": 48, "currency": "eur" },
"ledger_entry_id": "cle_01J..." }
// 200, el banco pide autenticación fuera de sesión
{ "payment_intent_id": "pi_3Q...", "status": "requires_action",
"client_secret": "pi_3Q..._secret_...",
"recovery_url": null, "decline_code": null }
// 402, rechazo del banco
{ "error": { "code": "card_declined",
"decline_code": "insufficient_funds",
"retryable": true, "retry_after_hint_days": 3,
"message_for_customer": "Tu banco ha rechazado el pago." } }El caso requires_action es el que más sistemas de facturación tratan mal: marcan el servicio como impagado y lo suspenden sin dar salida. La instancia debe enviar al comprador un correo con un enlace a una página de confirmación que reanude ese PaymentIntent. No es un fallo de cobro, es un cobro a medias.
5. Métodos de pago guardados
POST /v1/payments/setup-intents para guardar una tarjeta, GET /v1/payments/methods para listarlas y POST /v1/payments/methods/{id}/detach para desvincularlas. Siempre sobre la cuenta conectada, nunca sobre la plataforma.
6. Reembolsos
POST /v1/refunds admite reembolso total o parcial. La política de qué parte de la comisión se devuelve y qué parte se retiene la aplica el plano de control, no la instancia, para que sea la misma para todo el mundo y quede registrada.
// petición
{ "payment_intent_id": "pi_3Q...", "amount_minor": 500,
"reason": "requested_by_customer", "note": "Error de facturación" }
// respuesta
{ "refund_id": "re_3Q...", "status": "succeeded",
"commission": { "returned_minor": 10, "retained_minor": 20,
"policy": "pct_proporcional_fijo_retenido" } }7. Comisiones de pagos fuera de la pasarela
Cuando cobras por transferencia, en efectivo o por cualquier vía que no pasa por la pasarela, y marcas la factura como pagada, tu instancia llama antes a POST /v1/commissions/manual. La comisión se devenga, se acumula y se liquida al cierre del mes.
// petición
{
"reference": { "kind": "invoice", "id": "inv_2026_0418" },
"amount_minor": 2400,
"currency": "eur",
"method": "bank_transfer",
"occurred_at": "2026-10-03T09:12:00Z",
"external_ref": "Transferencia 4409/2026"
}
// respuesta
{ "ledger_entry_id": "cle_01J...", "status": "accrued",
"fee": { "total_minor": 66, "currency": "eur" },
"settles_in_period": "2026-10" }Los valores de method son bank_transfer, cash, paypal_external, crypto, other y admin_marked_paid. Si la llamada falla, la factura se queda en pendiente con un aviso: no se pierde tu trabajo, pero tampoco se salta el registro.
8. Estado de la cuenta y comisiones
GET /v1/account devuelve el estado de tu cuenta conectada, y GET /v1/commissions/summary el resumen de comisión cobrada y devengada. Son las dos llamadas que alimentan la sección de comisión de tu propio panel.
9. Idempotencia
Cada petición que mueve dinero lleva Idempotency-Key. El formato recomendado es <propósito>:<referencia>:<periodo>, por ejemplo renewal:svc_4821:2026-10.
Nunca uses la fecha de hoy como parte de la clave. Una clave del estilo renewal-4821-2026-10-03 hace que un reintento al día siguiente cobre otra vez, porque la clave cambia. El periodo que factura sí puede formar parte de la clave; el día en que se intenta, no.
- El plano de control guarda
(instance_id, idempotency_key)con su respuesta durante siete días. Stripe solo garantiza veinticuatro horas; la ventana es mayor a propósito, porque un reintento humano tras un incidente llega tarde. - La clave que viaja a Stripe se deriva con
hmac(instance_id, idempotency_key), para que dos instancias no puedan colisionar ni sondearse. - Repetir una clave con un cuerpo distinto devuelve
409 idempotency_key_reuse, nunca un cobro nuevo.
10. Errores y reintentos
| Situación | Respuesta | Qué hace tu instancia |
|---|---|---|
| Firma inválida o reloj fuera de ventana | 401 | No reintentar. Alertar: es configuración o reloj desajustado. |
| Cuenta conectada sin cobros habilitados | 409 account_not_ready | No reintentar. Avisar al proveedor. |
| Moneda no soportada por el plan | 422 currency_not_supported | No reintentar. |
| Rechazo del banco | 402 con retryable | Programar según la política de dunning. |
| Error nuestro o de Stripe, o tiempo agotado | 503 o sin respuesta | Reintentar con la misma clave de idempotencia, con espera creciente, hasta seis veces en veinticuatro horas. |
| Límite de peticiones | 429 con Retry-After | Respetar la cabecera. |
La regla que resume todas las demás: un tiempo agotado no es un fallo de cobro. Puede haber cobrado. La instancia jamás trata un 503 como «no se cobró»: reintenta con la misma clave y espera al webhook.
¿Falta algo que necesitas para integrarte? Escribe a hola@payezz.app con el caso concreto.