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 configurado en el portal para ese método.Configura el ruteo del método en el portal (o usa Sim mientras pruebas).
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 de PAC no configuradas o inválidas en el perfil fiscal del tenant, o el CFDI no pasó sus validaciones).Revisa error_detail en el CFDI (GET /v1/invoices/{id}) y el perfil fiscal en el portal.
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: se hace desde el portal (o PUT /admin/tenants/{id}/fiscal-profile). 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: cargar usuario/contraseña del PAC (hoy Facturama) en el portal.
400invoice.pac_credentials_incompleteLas credenciales del PAC existen pero les falta usuario o contraseña.Vuelve a capturar ambas en el portal.
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 desde el portal.

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 configurado 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.
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.