
Cómo ganar contracargos en México: arma el pipeline de razón-a-evidencia para Conekta y Mercado Pago
Un contracargo es una disputa de tarjeta donde el banco emisor te jala los fondos; para quedarte con el dinero envías evidencia que rebata la razón exacta, antes del deadline, por rail. La mayoría de las pérdidas son evidencia que pudiste haber registrado en el checkout. Arma un pipeline de razón-a-evidencia: registra, ensambla en el webhook, envía por API. Es un patrón de ingeniero, no asesoría legal.
El bug no es la disputa: es la evidencia que nunca registraste
Te cae un contracargo y la primera reacción es pensar que tuviste mala suerte. No. Un contracargo (chargeback) es una disputa de tarjeta donde el banco emisor le jala los fondos de vuelta a tu cuenta porque el tarjetahabiente reclamó el cargo. Para quedarte con el dinero tienes que enviar evidencia que rebata la razón exacta de la disputa, antes de un deadline, siguiendo las reglas de la marca de tarjeta y del emisor. Ese es el trabajo.
Y aquí está la parte que las páginas de ayuda de las pasarelas no te cuentan: la mayoría de los contracargos que pierdes no se pierden porque el cliente tuviera razón. Se pierden porque cuando cae la disputa —semanas después del cargo— no tienes la evidencia a la mano. El resultado 3DS, el AVS, la IP, el número de guía, la confirmación de entrega. Todo eso lo pudiste haber registrado en el checkout y en el envío, y no lo hiciste. La disputa no es el bug. El bug es la evidencia que nunca persististe.
El trabajo de ingeniería que las pasarelas glosan es este: un pipeline de razón -> evidencia requerida, enviado programáticamente por cada rail. No es un botón. Es un sistema que registra inputs en el checkout, ensambla el paquete correcto cuando llega el webhook de disputa, y lo manda por la API antes del deadline.
Antes de seguir, lo honesto: esto es un patrón de ingeniero, no asesoría legal ni de compliance. Yo no decido si ganas. El emisor decide, según las reglas de la marca de tarjeta, y la resolución tarda de semanas a meses. Lo que yo te puedo dar es el pipeline que hace que cuando pelees, pelees con la evidencia completa en vez de con las manos vacías.
¿Qué evidencia realmente rebate cada razón?
Esta es la matriz de evidencia, y es el corazón de todo. Antes de escribir una sola línea de código tienes que tener clarísimo qué evidencia rebate cada razón de disputa. Si mandas el paquete equivocado para la razón equivocada, pierdes aunque tengas todo registrado.
- Fraude / “no reconozco el cargo”: el resultado de autenticación 3DS, el AVS, el dispositivo + IP de la sesión, prueba de entrega, e historial de compras previas de ese mismo cliente. La idea es demostrar que fue una transacción legítima y autenticada, no un fraude.
- Producto no recibido: número de guía (tracking) + confirmación de entrega + el hilo de comunicación con el cliente. Demuestras que el producto llegó.
- Producto no como se describe: la publicación (listing) o la descripción exacta que vio el cliente, los términos y condiciones que aceptó, tu política de reembolsos, y las comunicaciones. Demuestras que recibió lo que compró bajo las reglas que aceptó.
El punto clave: este paquete se ensambla con datos que ya deberías estar registrando. No es algo que sales a juntar después de que cae la disputa. Para entonces ya es tarde o ya se te perdió.
Registra la evidencia en el checkout y en el envío (antes de que caiga la disputa)
No puedes enviar lo que no capturaste. Y como la disputa cae semanas después, tienes que persistir los inputs ahora, en el momento del cargo y del envío, no cuando ya tienes el webhook de contracargo encima.
En el checkout registra: el resultado 3DS2, el AVS, el fingerprint de IP/dispositivo, el payment_id o charge_id, y una referencia al historial del cliente. En el fulfillment registra: número de guía, confirmación de entrega con timestamps, y el hilo de comunicaciones.
Lo crítico es que todo quede llaveado por el id del pago/cargo, para que cuando llegue el webhook de disputa —que viene con ese id— puedas hacer el join y reconstruir el paquete al instante.
-- Ilustrativo: esquema mínimo de log de evidencia.
-- Lo llenas en el checkout y en el envío, NO cuando cae la disputa.
CREATE TABLE evidence_log (
charge_id text PRIMARY KEY, -- la llave para hacer join con el webhook
-- inputs del checkout
threeds_result text, -- p.ej. "authenticated" / "attempted"
avs_result text,
ip_address inet,
device_id text,
customer_id text,
prior_orders int, -- historial de compras del cliente
-- inputs del fulfillment
tracking_number text,
delivered_at timestamptz, -- confirmación de entrega
comms_thread_url text, -- hilo de comunicación con el cliente
-- inputs para "no como se describe"
listing_url text, -- la publicación que vio el cliente
accepted_terms_url text, -- T&C que aceptó
refund_policy_url text, -- política de reembolsos vigente
created_at timestamptz DEFAULT now()
);
Si solo te llevas una cosa de este post: este INSERT en el checkout vale más que cualquier API de contracargos. La API de contracargos solo sirve si tienes algo que enviar.
En el webhook, arma el paquete específico de la razón
Cuando cae la disputa, el rail te avisa por webhook. Ahí lees la razón, traes la evidencia que hace match desde tu log, y construyes el paquete.
Dos cosas específicas por rail que condicionan este paso. En Mercado Pago solo debes enviar documentación si el campo documentation_required es true y date_documentation_deadline es una fecha futura. Si no, no hay nada que enviar o ya pasó el deadline. En Conekta el evento es charge.chargeback, con su ciclo de vida (created, updated, under_review, lost, won); el payload trae un arreglo files y un deadline evidence_due_by.
# Ilustrativo: handler de webhook que ramifica por razón -> matriz de evidencia.
# Idempotente: si ya procesaste este event.id, no reenvíes.
def handle_dispute_webhook(event, db):
if db.already_processed(event["id"]): # idempotencia
return 200
dispute = event["data"]
charge_id = dispute["charge_id"]
reason = normalize_reason(dispute["reason"]) # mapea al vocabulario de tu matriz
evidence = db.get_evidence_log(charge_id) # join por charge_id
if evidence is None:
# no registraste nada en el checkout: aquí se pierde la pelea
log.warning("sin evidencia para %s; revisa el paso de logging", charge_id)
packet = build_packet_for_reason(reason, evidence)
db.mark_processed(event["id"])
return packet
def build_packet_for_reason(reason, ev):
# la matriz de evidencia, en código.
# los nombres coinciden con las columnas de evidence_log.
if reason in ("fraud", "no_reconozco"):
return [ev.threeds_result, ev.avs_result, ev.ip_address,
ev.device_id, ev.delivered_at, ev.prior_orders]
if reason == "producto_no_recibido":
return [ev.tracking_number, ev.delivered_at, ev.comms_thread_url]
if reason == "no_como_se_describe":
return [ev.listing_url, ev.accepted_terms_url,
ev.refund_policy_url, ev.comms_thread_url]
return []
Esto se construye sobre los webhooks que ya tienes corriendo. Si todavía no los tienes firmes, primero arma esa base: te dejé cómo construir los webhooks de Conekta sobre los que se monta todo esto en integrar Conekta con OXXO, SPEI y webhooks.
Enviar a Mercado Pago: las reglas de la API de contracargos
Aquí van los específicos de Mercado Pago. Envías la evidencia con un POST al endpoint de documentación de contracargos (/v1/chargebacks/{id}/documentation, según los docs de MP). Las reglas que tienes que respetar sí o sí, como de 2026 —confirma las vigentes—:
- Los archivos deben ser .jpg / .png / .pdf, y el conjunto no debe exceder 10 MB en total (es un límite global, no por archivo).
- Solo envías si
documentation_requiredestrueydate_documentation_deadlinees una fecha futura; ese campo refleja la ventana de envío, así que trátalo como tu deadline real (confirma el plazo exacto en los docs vigentes de MP). - Un upload exitoso devuelve HTTP 200 y mueve
documentation_statusareview_pending.
# Ilustrativo: envío a la API de contracargos de Mercado Pago.
# Archivos .jpg/.png/.pdf, conjunto <= 10 MB; solo si documentation_required + deadline futuro.
import requests
MP_FILE_MAX = 10 * 1024 * 1024 # 10 MB en total (no por archivo)
ALLOWED = {"image/jpeg", "image/png", "application/pdf"}
def submit_mp(chargeback, files, token):
if not chargeback.get("documentation_required"):
return # MP no pide documentación: no envíes
if chargeback["date_documentation_deadline"] <= now():
return # ya pasó el deadline
for f in files:
assert f["content_type"] in ALLOWED, "tipo de archivo no permitido"
# el límite de 10 MB es del CONJUNTO, no por archivo
assert sum(f["size"] for f in files) <= MP_FILE_MAX, "el total de archivos > 10 MB"
r = requests.post(
f"https://api.mercadopago.com/v1/chargebacks/{chargeback['id']}/documentation",
headers={"Authorization": f"Bearer {token}"},
files=[("file", (f["name"], f["bytes"], f["content_type"])) for f in files],
)
# 200 => documentation_status pasa a review_pending
r.raise_for_status()
return r.status_code
Lo que tienes que tener clarísimo sobre el ciclo de vida en MP: la resolución puede tardar hasta 6 meses dependiendo de la marca de la tarjeta, y el monto disputado queda retenido en tu cuenta hasta que se resuelve. No es dinero que tienes; es dinero congelado. Planéalo en tu conciliación. Los detalles oficiales están en la gestión de contracargos de Mercado Pago. Si todavía estás armando la integración base, aquí está cómo integrar Mercado Pago.
Enviar a Conekta: el evento charge.chargeback
En Conekta el patrón es el mismo, montado sobre el evento charge.chargeback. Lo que está verificado en sus docs: dispara eventos de ciclo de vida —created, updated, under_review, lost, won— y el payload expone un arreglo files (con file_name, url, created_at) y un deadline evidence_due_by (timestamp Unix). Envías notas + archivos para disputar y rastreas el estatus por esos eventos.
Lo honesto: la documentación de Conekta no me especifica el límite exacto de tamaño de archivo ni la ventana de envío. No los voy a inventar. Confírmalos contra sus docs vigentes antes de mandar a producción —esas reglas son sensibles al tiempo—.
# Ilustrativo: rama Conekta. Mismo patrón que MP.
# CONFIRMA límite de tamaño y ventana en los docs vigentes de Conekta.
def handle_conekta_chargeback(event, db):
if db.already_processed(event["id"]): # idempotencia: los webhooks llegan duplicados/fuera de orden
return 200
kind = event["type"] # charge.chargeback.created / updated / under_review / lost / won
data = event["data"]["object"]
# no asumas secuencia: deriva el estado del ÚLTIMO evento por charge_id
if kind == "charge.chargeback.created":
evidence = db.get_evidence_log(data["charge_id"])
packet = build_packet_for_reason(normalize_reason(data["reason"]), evidence)
deadline = data["evidence_due_by"] # timestamp Unix
# submit notes + files antes de evidence_due_by (reglas de archivo: confirmar)
submit_conekta(data["id"], packet, deadline)
elif kind in ("charge.chargeback.won", "charge.chargeback.lost"):
db.record_outcome(data["charge_id"], won=(kind.endswith("won")))
db.mark_processed(event["id"])
return 200
El flujo espejea a MP: haces match de la razón -> ensamblas el paquete -> envías antes de evidence_due_by -> rastreas por los eventos del ciclo de vida. Referencia oficial: Conekta — charge.chargeback.
Mercado Pago
- API de contracargos: POST a /v1/chargebacks/{id}/documentation
- Archivos .jpg/.png/.pdf, conjunto máx 10 MB en total
- Solo si documentation_required + date_documentation_deadline futuro
- Éxito = HTTP 200 -> documentation_status review_pending
- Hasta 6 meses; monto retenido hasta resolver
Conekta
- Evento charge.chargeback (created/updated/under_review/lost/won)
- Payload con arreglo files + deadline evidence_due_by
- Envías notas + archivos para disputar
- Tamaño de archivo y ventana: confirmar en sus docs vigentes
- Rastreas resultado por eventos won / lost
Rastrea el estatus, concilia el monto retenido y mide tu tasa de éxito
Esto es un pipeline, no un disparo único. Después de enviar, rastreas: en MP, documentation_status desde review_pending hasta su resolución; la resolución te llega por IPN/Webhook y consultas el caso con el Get Chargeback (ahí ves la elegibilidad de cobertura en coverage_elegible y la decisión final en coverage_applied). En Conekta, los eventos won / lost.
Concilia el monto retenido una vez que resuelve. Hasta entonces el dinero está bloqueado —en MP, recuérdalo, hasta 6 meses—. No lo cuentes como ingreso disponible.
Y lo más importante para mejorar: mide tu tasa de éxito por razón + por marca de tarjeta. Cuando ves que pierdes el 80% de los “producto no recibido” de cierta marca, casi siempre apunta a un input de evidencia específico que te falta —no estabas guardando la confirmación de entrega, por ejemplo—. Ese número es el que te dice dónde está la fuga.
Y ahí cierras el ciclo: regresas al paso 1 y registras lo que te faltaba. El pipeline se afina solo con cada disputa que pierdes por evidencia ausente.
- Registra la evidenciaen el checkout (3DS/AVS/IP) y en el envío (guía/entrega/comms)
- Arma el paquete en la disputalee la razón, junta la evidencia que rebate
- Envía antes del deadlineMP: .jpg/.png/.pdf, conjunto ≤ 10 MB; Conekta: confirma reglas
- Rastrea el estatusMP: review_pending -> resuelto por IPN; Conekta: won / lost
- Mide la tasa de éxitopor razón + marca; arregla la fuga de evidencia
Preguntas frecuentes
¿Esto me garantiza ganar? No. El emisor decide según las reglas de la marca de tarjeta, la resolución tarda de semanas a meses, y no las vas a ganar todas. Lo que el pipeline garantiza es que pelees con la evidencia completa en vez de con las manos vacías.
¿Esto es asesoría legal? No. Es un pipeline de evidencia de ingeniero. Los resultados de contracargos siguen las reglas de la marca de tarjeta y del emisor, y yo no soy ninguno de los dos.
¿Cuál es la victoria más grande? La prevención. 3DS2 + reglas de fraude evitan que la disputa caiga, y eso vale más que cualquier evidencia después del hecho. El lado de Stripe de esto está en disputas y contracargos con Stripe Radar.
¿Qué tipos y tamaños de archivo para MP? .jpg / .png / .pdf, con un máximo de 10 MB para el conjunto (no por archivo). Para Conekta, confirma los límites vigentes en sus docs.
¿Cuánto tarda la resolución? En MP, hasta 6 meses dependiendo de la marca de tarjeta, y el monto queda retenido mientras tanto.
Prevenir es mejor que curar
El pipeline recupera dinero que de otro modo perderías, y vale la pena armarlo. Pero seamos claros: el verdadero apalancamiento es la prevención. 3DS2 y reglas de fraude bien afinadas hacen que la disputa nunca caiga, y eso le gana siempre a pelearla después. Si quieres ubicar dónde encaja cada pasarela en este panorama, ve pasarelas de pago en México, y para más patrones de pagos en la región, más guías de pagos LATAM.
Y repito lo de siempre: esto es un patrón de ingeniero, no asesoría legal, y las reglas de cada rail son sensibles al tiempo. Confirma los específicos vigentes de Mercado Pago y de Conekta antes de mandar a producción. Construye el pipeline, pero deja la prevención prendida.