Errores

MODO PRUEBA

Todos los errores de la Api usan el mismo envelope, estilo Stripe. El campo estable para tu código es error.code — el estado HTTP acompaña, pero no siempre sigue la convención REST "perfecta" (lo explicamos más abajo).

El envelope

json
{
  "error": {
    "type": "invalid_request_error",
    "code": "refund.exceeds_refundable",
    "message": "El monto excede el saldo devolvible (84900 centavos).",
    "doc_url": "https://winal.com.mx/docs/errores.html#err-refund.exceeds_refundable",
    "request_id": "0HN7F3K2J4Q1O:00000003"
  }
}

request_id es el mismo valor que el header X-Request-Id de la respuesta — inclúyelo si escribes a soporte. type agrupa la familia del error (invalid_request_error, authentication_error, authorization_error, idempotency_error, rate_limit_error, api_error).

Atajo
doc_url apunta a esta misma página, con el ancla de la fila exacta del código (#err-<code>): pega el doc_url de cualquier error en el navegador y caes directo en su explicación. Un código todavía sin ancla te deja al inicio de la página.

Estados HTTP que puedes recibir

HTTPCuándo
400Solicitud inválida: falta un campo, formato incorrecto, o una regla de negocio que la Api trata como error del llamador.
401Falta Authorization, la API key no existe, está mal formada o fue revocada — siempre el mismo mensaje genérico (no delata cuál de los tres pasó).
403La clave o el client_secret no autorizan la operación sobre ESE recurso, o la API key no trae el scope que el endpoint exige (insufficient_scope — ver abajo).
404El recurso no existe (o, en /public/*, el client_secret no coincide — mismo 404 genérico por diseño).
409Conflicto de estado: reintento en vuelo, o la máquina de estados detectó una modificación concurrente.
422Reusaste un Idempotency-Key con un cuerpo distinto.
429Excediste el límite de solicitudes.
500Error interno no controlado. Reintenta con backoff o escala con el request_id.

Idempotencia (POST que mueven dinero)

Aplica a POST /v1/payment_intents, /confirm, /cancel, /capture y POST /v1/refunds — todos exigen Idempotency-Key (UUID). POST /v1/webhook_endpoints es la excepción: no pasa por esta capa, así que el header ahí es opcional y se ignora.

HTTPcodeCausaQué hacer
400idempotency_key_requiredFalta el header o no es un UUID válido.Genera un UUID v4 nuevo por operación de negocio (no por request HTTP).
409 + Retry-After: 2idempotency_key_in_progressOtra solicitud con el mismo key sigue procesándose.Reintenta en unos segundos con el mismo key.
422idempotency_key_reusedYa usaste ese key con un cuerpo distinto.Bug del cliente: nunca reuses un key para una operación distinta.

Una repetición exitosa del mismo key con el mismo cuerpo devuelve la respuesta original cacheada, con el header Idempotency-Replayed: true — es seguro reintentar así tras cualquier timeout de red.

Errores de payment_intents

HTTPcodeCausaQué hacer
400payment_intent.invalid_amountamount_minor no es positivo, o falta currency.Valida antes de enviar; el monto es siempre centavos enteros.
400payment_intent.invalid_currencyEl código de moneda no es ISO 4217 válido.Usa MXN — hoy es la única moneda que soporta el conector Sim.
403payment_intent.unauthorizedNi la API key ni el client_secret autorizan este intent.Verifica que el id y el client_secret correspondan al mismo intent.
404payment_intent.not_foundEl id no existe (visible solo vía /v1; en /public se generaliza).Confirma el id devuelto al crear el intent.
409payment_intent.not_confirmableIntentaste confirmar un intent que no está en requires_payment_method/requires_confirmation (p. ej. ya succeeded).Lee el estado actual antes de reintentar confirmar.
400payment_intent.no_routeNo hay conector dado de alta para ese método en tu cuenta.En modo prueba el conector Sim viene activo y no hace falta nada. Para producción, el alta de conectores reales la hacemos nosotros: escríbenos a hola@winal.com.mx.
409payment_intent.concurrent_modificationDos operaciones intentaron mutar el mismo intent a la vez.Vuelve a leer el intent y reintenta la operación.
400attempt.not_capturablePediste /capture pero no hay un intento authorized pendiente.La captura solo aplica tras un intento de tarjeta autorizado sin capturar (tok_sim_auth en pruebas).
404attempt.not_foundEl intento referido no existe.Usa un attempt_id devuelto por ?expand=attempts.

Errores de refunds

HTTPcodeCausaQué hacer
400refund.invalid_amountamount_minor no es estrictamente positivo.Omite el campo para devolver todo el saldo restante, o manda un entero positivo.
404attempt.not_foundEl attempt_id no existe.Usa el id de un intento real (no el del payment_intent).
409refund.attempt_not_refundableEl intento no está captured/settled/partially_refunded.Solo se puede devolver un cargo ya capturado.
400refund.currency_mismatchEl monto solicitado trae una moneda distinta a la del cargo.Usa la misma moneda del cargo original.
400refund.nothing_refundableEl cargo ya no tiene saldo por devolver.Consulta el saldo restante antes de reintentar.
400refund.exceeds_refundableEl monto pedido excede lo cobrado menos devoluciones ya vivas.El mensaje trae el saldo devolvible exacto en centavos.
404refund.not_foundEl id de la devolución no existe.Usa el id devuelto al crear el refund.

Errores de propinas, reportes y facturación

Verificados en vivo contra el servidor de pruebas.

HTTPcodeCausaQué hacer
400payment_intent.invalid_tiptip_minor es negativo (en POST /v1/payment_intents o en /confirm).Envía tip_minor ≥ 0, o omítelo (equivale a 0 / "no tocar la propina existente" en confirm).
400reports.invalid_periodfrom/to no son ISO-8601 válidos, o from no es anterior a to.En /v1/reports/cash-cut ambos son obligatorios; en el resto de /v1/reports/* son opcionales (default: últimos 30 días).
400reports.invalid_currencyEl ?currency= no es un código ISO 4217 válido.Usa MXN (default si omites el parámetro).
400reports.invalid_formatFalta ?format= en /v1/reports/polizas, o no es contpaqi/aspel_coi.No hay default: pasa explícitamente uno de los dos valores.
400reports.invalid_cursorEl ?cursor= de /v1/reports/payments es inválido o está corrupto.Usa el next_cursor devuelto por la página anterior, no lo construyas a mano.
400invoice.invalid_bodyFalta payment_intent_id (POST /v1/invoices//payments) o total_minor positivo (POST /v1/invoices/ppd).Revisa el cuerpo contra Referencia de API → Facturas.
400invoice.invalid_receptorFalta receptor o alguno de sus campos (rfc, nombre, uso_cfdi, regimen_fiscal, cp).Los cinco campos del receptor son obligatorios; usa XAXX010101000 para público en general.
400invoice.invalid_currencyLa currency de POST /v1/invoices/ppd no es ISO 4217 válida.Omite el campo para MXN por default, o usa un código válido.
400invoice.pac_errorEl PAC rechazó el timbrado: credenciales inválidas, o el CFDI no pasó sus validaciones.Revisa error_detail en el CFDI (GET /v1/invoices/{id}). Si el problema son las credenciales o el perfil fiscal, corrígelos tú mismo en tu tablero (winal.com.mx/app → Facturación).
400invoice.not_stampedPediste el XML/PDF de un CFDI que aún no timbró, o registraste un pago (POST /v1/invoices/{id}/payments) contra una factura PPD que aún no timbró.Consulta status antes de descargar; un REP solo aplica sobre una factura PPD ya stamped.
404invoice.not_foundEl id del CFDI no existe.Usa el id devuelto al crear la factura.
400invoice.receptor_incoherenteUn RFC genérico (XAXX010101000/XEXX010101000) sin la tercia que exige el SAT: nombre PÚBLICO EN GENERAL, regimen_fiscal 616 y uso_cfdi S01. El mensaje del error dice cuál de las tres falló.Con RFC genérico, manda esa combinación exacta. Con un RFC real, elige el uso_cfdi que tu cliente pida — ver Facturación CFDI.
404invoice.intent_not_foundEl payment_intent_id a facturar no existe (o no es de tu cuenta).Usa el id que devolvió POST /v1/payment_intents.
400invoice.intent_not_succeededEl cobro todavía no está succeeded (típicamente processing: la Api ya aceptó el cobro pero el proveedor aún no confirma).Solo se factura un cobro liquidado. Espera el webhook payment_intent.succeeded —o consulta el intent— antes de facturar; nunca asumas el desenlace por tiempo.
400invoice.already_existsEse cobro ya tiene un CFDI vivo (pendiente o timbrado).Un cobro se factura una sola vez. Lee el CFDI existente en vez de crear otro.
400invoice.conceptos_sum_mismatchLa suma de importe_minor de los conceptos no iguala el total del comprobante.El mensaje trae ambas cifras en centavos: cuadra los conceptos contra el total del cobro.
400invoice.livemode_mismatchEl modo del cobro no coincide con el ambiente del PAC configurado (p. ej. cobro de prueba contra PAC productivo).Guarda de seguridad: nunca se timbra un CFDI real desde un cobro de prueba. Alinea la llave (sk_test_/sk_live_) con el ambiente del PAC en el perfil fiscal.
400invoice.fiscal_profile_missingTu cuenta aún no tiene perfil fiscal (RFC del emisor, régimen, lugar de expedición).Es lo PRIMERO que hay que configurar antes de facturar. Cárgalo tú mismo en la sección Facturación de tu tablero: https://winal.com.mx/app/. No es un error de tu código.
400invoice.pac_credentials_missingHay perfil fiscal, pero sin credenciales del PAC para ese ambiente.Siguiente paso tras el perfil fiscal: carga usuario y contraseña del PAC (hoy Facturama) en la sección Facturación de tu tablero: https://winal.com.mx/app/.
400invoice.pac_credentials_incompleteLas credenciales del PAC existen pero les falta usuario o contraseña.Recaptúralas en la sección Facturación de tu tablero: https://winal.com.mx/app/.
400invoice.pac_unreachableNo se pudo contactar al PAC (red o indisponibilidad del proveedor).Transitorio: reintenta con backoff usando la misma Idempotency-Key — no se duplica el CFDI.
400invoice.pac_bad_responseEl PAC respondió algo que no se pudo interpretar.Reintenta con la misma clave; si persiste, escala con el request_id.
400invoice.pac_no_uuidEl PAC respondió sin UUID de timbre — el CFDI no se considera timbrado.Nunca lo des por bueno sin uuid_fiscal. Reintenta con la misma clave y confirma con GET /v1/invoices/{id}.
El orden en que los vas a ver
Al integrar facturación desde cero, los errores llegan casi siempre en esta secuencia: invoice.fiscal_profile_missing (configura el perfil fiscal) → invoice.pac_credentials_missing (carga las credenciales del PAC) → invoice.receptor_incoherente o invoice.conceptos_sum_mismatch (ajusta el cuerpo) → CFDI timbrado. Los dos primeros son configuración de la cuenta, no bugs de tu código.

Errores de antifraude

HTTPcodeCausaQué hacer
409payment_intent.blocked_by_riskUna regla de riesgo con action: "block" disparó sobre este confirm. No se creó ningún attempt.El mensaje trae la razón exacta (p. ej. el tope de monto excedido). Ver Antifraude.

Cuando la regla es action: "review" en vez de block, no hay error: el confirm responde 200 con un objeto review nuevo (ver Antifraude) — el intent queda a la espera de que un operador la apruebe o la rechace. Hoy no existe un endpoint de /v1 para resolverla: si activas reglas con acción review, avísanos a hola@winal.com.mx para acordar cómo se atienden.

Errores de customers / payment_methods

HTTPcodeCausaQué hacer
404customer.not_foundEl id del cliente no existe.Usa el id devuelto al crear el cliente.
400payment_method.invalid_tokenFalta payment_token al guardar un método.Es requerido: token de un solo uso de la tokenización (tok_sim_* en pruebas).
404payment_method.not_foundEl id del método no existe.Usa el id devuelto al guardarlo.
409payment_method.not_chargeableEl método referido por payment_method_id en confirm no está active.El mensaje trae el estado real del método; solo un método active puede cobrar.
400payment_method.no_routeNo hay conector dado de alta que pueda resolver el guardado del método.Configura el ruteo en el portal antes de guardar métodos.
409payment_method.concurrent_modificationDos operaciones intentaron mutar el mismo método a la vez.Vuelve a leer el método y reintenta.

Errores de receivables

HTTPcodeCausaQué hacer
400receivable.missing_fieldsFalta customer_name, customer_email o concepto.Los tres son siempre requeridos.
400receivable.invalid_amountamount_minor no es positivo.Manda un entero > 0 en centavos.
400receivable.invalid_currencycurrency no es ISO 4217 válida.Usa MXN.
400receivable.invalid_datedue_date inválida.Manda un ISO-8601 válido.
400receivable.incomplete_fiscal_receptorSe mandó solo alguno de los cuatro datos fiscales del receptor.customer_rfc, customer_uso_cfdi, customer_regimen_fiscal y customer_cp van juntos o ninguno.
404receivable.not_foundEl id no existe.Usa el id devuelto al crear la cuenta.
400receivable.no_phoneSe pidió whatsapp_link sin customer_phone capturado.Captura customer_phone al crear la cuenta.

Errores de conciliación bancaria

HTTPcodeCausaQué hacer
400bank_statement.bank_requiredFalta bank.Manda el nombre del banco emisor.
400bank_statement.invalid_periodFaltan/son inválidos period_start/period_end, o el rango está invertido.Formato yyyy-MM-dd, con period_startperiod_end.
400bank_statement.invalid_tolerancetolerance_days negativo.Omite el campo o manda un entero ≥ 0.
400bank_statement.unknown_presetpreset no es bbva/banorte/santander.Usa uno de los tres, o el mapeo explícito de columnas.
400bank_statement.mapping_requiredNo se dio preset ni un mapeo explícito completo.Manda date_column/description_column/credit_column/debit_column.
400bank_statement.invalid_mappingEl mapeo explícito de columnas es inconsistente.Revisa que las columnas no se traslapen y sean válidas (0-based).
400bank_statement.invalid_match_status?match_status= no es matched/unmatched/partial.Usa uno de esos tres valores, o ninguno.
400bank_statement.too_largeEl archivo excede 5 MiB.Parte el estado de cuenta por período más corto.
400bank_statement.parse_errorUna fila no parsea (fecha/monto inválidos, o trae abono Y cargo — o ninguno — a la vez).El mensaje trae el número de fila exacto; revisa el mapeo de columnas contra tu CSV real.

Errores de autofactura

HTTPcodeCausaQué hacer
400autofactura.invalid_bodyFalta alguno de los campos requeridos del cuerpo.Revisa contra Referencia de API → Autofactura pública.
400autofactura.invalid_rfcEl rfc no cumple el formato del SAT.Usa un RFC válido (o XAXX010101000 para público en general).
404autofactura.not_availableEl slug no existe, o el comercio deshabilitó autofactura — mismo 404 genérico para ambos casos.Confirma con el comercio que la autofactura esté habilitada.
404autofactura.receipt_not_foundEl receipt_code no existe, es de otro tenant, o no coincide con el rfc de un CFDI ya emitido — mismo 404 genérico en los tres casos (anti-enumeración).Verifica el receipt_code del ticket y que el RFC sea el correcto.

Errores de onboarding_application

HTTPcodeCausaQué hacer
400onboarding_application.invalid_legal_nameFalta legal_name al crear.Es obligatorio desde el alta mínima.
400onboarding_application.invalid_person_typeperson_type ausente o distinto de fisica/moral.Usa uno de esos dos valores.
400onboarding_application.invalid_contact_emailFalta contact_email al crear.Es obligatorio desde el alta mínima.
400onboarding_application.invalid_documentUn documento trae document_type inválido, o falta reference/reference_hash.Usa uno de los 4 tipos válidos: ine, comprobante_domicilio, constancia_fiscal, caratula_estado_cuenta.
404onboarding_application.not_foundEl id no existe.Usa el id devuelto al crear la solicitud.
409onboarding_application.transition_conflictPUT/submit sobre una solicitud que ya no es editable, o carrera entre dos requests.Lee el estado actual antes de reintentar.
400onboarding_application.missing_fieldssubmit sin todos los campos obligatorios.El mensaje lista exactamente cuáles faltan — revisa Onboarding.
400onboarding_application.missing_documentssubmit sin los 4 documentos requeridos.Adjunta los 4 tipos antes de enviar a revisión.

Errores de connect

HTTPcodeCausaQué hacer
400connect.invalid_application_idonboarding_application_id ausente o no es un uuid.Manda el id de una solicitud de Onboarding real.
404connect.application_not_foundLa solicitud referida no existe.Verifica el id.
400connect.application_not_approvedLa solicitud existe pero no está approved.Espera a que Onboarding la resuelva como aprobada.
409connect.account_conflictEsa solicitud ya está ligada a una cuenta Connect.Usa GET /v1/connect/accounts para encontrar la cuenta existente.
404connect.account_not_foundEl id de la cuenta (o un connect_account_id en splits) no existe.Verifica el id.
400connect.account_suspendedLa cuenta Connect no está activa.Solo cuentas activas pueden recibir splits.
400connect.invalid_payment_intentpayment_intent_id ausente o no es un uuid.Usa el id de un cobro real, ya exitoso.
400connect.invalid_chargeFalta charge_amount_minor positivo o currency.Ambos son requeridos en POST /v1/connect/transfers.
400connect.invalid_currencyMoneda ISO 4217 inválida.Usa MXN.
400connect.missing_allocationssplits vacío o ausente.Manda al menos una porción.
400connect.invalid_allocationUna porción trae connect_account_id inválido o amount_minor no positivo.Revisa cada elemento de splits.
400connect.split_mismatchapplication_fee_minor + Σ splits no cuadra exactamente con charge_amount_minor.El mensaje explica la regla; ajusta los montos para que sumen exacto.
404connect.transfer_not_foundEl id del transfer no existe.Verifica el id.

Errores de payouts

HTTPcodeCausaQué hacer
400payout.missing_fieldsFalta clabe, beneficiary_name o concepto.Los tres son siempre requeridos.
400payout.invalid_amountFalta amount_minor positivo o currency.Ambos son requeridos.
400payout.invalid_currencycurrency no es un código ISO 4217 parseable.Usa un código válido.
400payout.unsupported_currencyMoneda ISO 4217 válida pero distinta de MXN.Las dispersiones SPEI solo operan en pesos.
400payout.invalid_clabeLa CLABE no son 18 dígitos, o el dígito de control es incorrecto.Verifica la CLABE con el algoritmo Banxico 3-7-1 antes de enviarla.
404payout.not_foundEl id no existe o es de otro tenant.Usa el id devuelto al crear el payout.

Errores de billers / service_payments

HTTPcodeCausaQué hacer
400biller.missing_referenceFalta reference en el inquiry.Es siempre requerido.
404biller.not_foundEl code no existe o está inactivo.Usa un code de GET /v1/billers.
400biller.invalid_referencereference no cumple el formato del biller.Revisa el reference_label del biller.
404biller.reference_not_foundFormato válido pero la cuenta no existe para ese biller.Verifica la referencia con el pagador.
400service_payment.missing_fieldsFalta biller_code o reference.Ambos son requeridos.
400idempotency_key_requiredFalta el header Idempotency-Key o no es un UUID válido.Este endpoint lo valida él mismo (mismo mensaje que el resto de la API).
409service_payment.idempotency_conflictMisma llave, cuerpo distinto.Usa una llave nueva para una operación distinta.
400service_payment.amount_mismatchamount_minor enviado no coincide con el adeudo vigente.Omite el campo para cobrar el adeudo tal cual, o consulta primero con inquiry.
404service_payment.not_foundEl id no existe o es de otro tenant.Usa el id devuelto al crear el pago.

Errores de recharges

Catálogo completo (incluida la activación de operadoras y el catálogo de productos) en Recargas de tiempo aire. Aquí solo la guarda de producción:

HTTPcodeCausaQué hacer
400recharge.livemode_unsupportedLa API key es sk_live_ (modo producción) y el único gateway conectado hoy solo simula la entrega — no hay agregador real conectado aún.Integra y prueba con una llave sk_test_; el endpoint sigue bloqueado en producción hasta que se conecte un agregador real.
400service_payment.livemode_unsupportedIntentaste pagar un servicio con una llave de producción (sk_live_). El único proveedor conectado hoy es un simulador.Gemelo de recharge.livemode_unsupported: se rechaza ANTES de cobrar o asentar nada, para no cobrarte por un servicio que jamás se pagaría. Úsalo con sk_test_ hasta que se conecte un agregador real.
Errores de conectores gated (BNPL, terminales, network tokens)
payment_intent.no_route es lo que verás al confirmar payment_method: "bnpl" hoy — el conector Sim no lo implementa (ver Métodos de pago → BNPL). La administración de terminales (terminal.not_found, terminal.invalid_transition, etc.) vive bajo /admin (portal), no como error público de /v1.

Autenticación, autorización y límite de solicitudes

HTTPcodeCausaQué hacer
401api_key.missing_or_invalidFalta Authorization: Bearer sk_..., o la clave no existe / está mal formada / fue revocada.Revisa el header; genera una clave nueva desde el portal si la sospechas revocada.
403insufficient_scopeLa API key autenticada no trae el scope que ese endpoint exige (p. ej. payouts:write, webhooks:manage). El mensaje nombra el scope faltante.Emite una llave con ese scope (o con el comodín *, acceso total) desde el portal. Ver Autenticación y seguridad → Scopes de la API key.
429 + Retry-After: 60rate_limit_exceeded300 solicitudes/min por API key en /v1/*; 60/min por IP en /public/*.Aplica backoff y agrupa reintentos; usa el mismo Idempotency-Key si reintentas una mutación.
Nota técnica: por qué algunos "conflictos" son 400 y no 409
La Api deriva el estado HTTP inspeccionando el texto de error.code (contiene not_found → 404, unauthorized → 403, contiene concurrent/conflict/not_confirmable/not_refundable/blocked_by_risk/revoked → 409, si no → 400) — no lee una categoría explícita del error de dominio. Por eso refund.exceeds_refundable, refund.nothing_refundable y attempt.not_capturable llegan como 400 aunque conceptualmente son conflictos de estado. Para tu lógica de reintento, confía siempre en error.code, no en suposiciones sobre el HTTP status.