
Cómo aceptar CoDi (y la verdad sobre DiMo) en tu app: guía de integración de un ingeniero mexicano
Para aceptar CoDi de forma programática, o eres participante de SPEI (carga regulatoria pesada) o vas por un agregador: generas un QR/cargo dinámico por orden y verificas el webhook firmado RSA-SHA512 sobre el cuerpo crudo antes de surtir. La liquidación es en tiempo real, 24/7 e irreversible — un reembolso es un nuevo pago SPEI de salida, no un contracargo. No hay API pública de cobro DiMo para negocios; DiMo es P2P por número de teléfono.
No puedes simplemente llamarle a Banxico — así funciona de verdad aceptar CoDi
Empecemos por la realidad operativa, no por la definición: no existe una API pública y de autoservicio de Banxico que tú, como builder, puedas consumir para cobrar con CoDi o DiMo. Eso no es un detalle; es lo primero que cambia tu arquitectura.
Tienes exactamente dos caminos. Uno: ser participante de SPEI directamente — banco o Institución de Fondos de Pago Electrónico — lo que implica una carga regulatoria pesada y un proceso que no resuelves con código. Dos: ir por un agregador que ya es participante y te expone una API limpia encima de CoDi. Para la enorme mayoría de los que estamos shippeando producto, el camino es el agregador.
El por qué ahora: la Circular 9/2026 de Banxico (publicada en el DOF en junio de 2026) empuja una experiencia estandarizada de SPEI/CoDi/DiMo, con cumplimiento de las instituciones para el 14 de diciembre de 2026. Traducción práctica: más comercios van a querer aceptar estos rieles. No voy a re-explicarte la política — esto es el how-to de integración. Y un disclaimer honesto desde la primera línea: soy ingeniero, no tu equipo de cumplimiento. Ser participante directo de SPEI y los bordes regulatorios se los dejas a tu contador y a compliance. Lo que yo te doy es el patrón que sí construyes.
Si quieres ubicar dónde encaja CoDi en el panorama completo, lo desgloso en pasarelas de pago en México, y las reglas de Banxico que están moviendo todo esto las cubro en cuenta nivel 2 bis, Banxico, SPEI y CoDi.
CoDi vs DiMo: qué es realmente cada uno (y sobre cuál puedes construir)
Hay que separar esto con honestidad, porque la confusión aquí te lleva a construir sobre algo que no existe.
CoDi (Cobro Digital, desde 2019) es un cobro iniciado por el comercio sobre SPEI, mediante un código QR o una notificación push que llega a la app bancaria del cliente. El cliente paga desde su banco. La pieza clave para ti: un QR dinámico es un código individual atado a un solo cargo y una transacción única — monto + tu referencia. No lo confundas con un QR estático, que es reutilizable. Para cobrar por cada orden, quieres el dinámico.
DiMo (Dinero Móvil, desde 2023) es una transferencia interbancaria en tiempo real identificada por número de teléfono, sin CLABE, montada sobre SPEI. Es principalmente persona a persona.
Ambos liquidan sobre SPEI: en tiempo real, 24/7, e irreversible. No hay contracargo estilo tarjeta. Esa irreversibilidad cambia tu backend, y la trato más abajo.
Para aceptación programática de negocio, el riel sobre el que construyes es CoDi — no DiMo. Y la razón merece su propia sección.
La parte honesta: no existe una API de cobro DiMo para negocios
Aquí es donde la mayoría de los artículos de marketing te mienten por omisión. A día de hoy no hay una API pública y limpia de cobro DiMo para negocios. Punto — y confírmalo vigente a 2026 con tu agregador, porque el ecosistema se mueve.
DiMo está diseñado alrededor del número de teléfono y es, en la práctica, P2P. No es un riel de cobro para comercios con un backend que puedas integrar. Si construyes un flujo asumiendo un “cobro DiMo por API”, estás construyendo sobre algo que no existe — y lo vas a descubrir en producción, que es el peor momento.
Para aceptación programática real tienes dos opciones honestas: CoDi (QR dinámico vía agregador) o CLABEs virtuales / SPEI tradicional, donde generas una CLABE por cada orden o por cliente y concilias el depósito entrante.
Si un cliente te dice “te pago por DiMo”, lo que eso significa en la práctica es una transferencia P2P manual que tú concilias después — no un cargo orquestado por tu API. Y como siempre: confirma esto contra la documentación vigente de tu agregador.
Generar un cargo CoDi dinámico (POST /dapp-codes/)
Vamos al build. El patrón es: un cargo dinámico por cada orden, con el monto y tu referencia interna. Te muestro el patrón con dapp.mx como UN ejemplo de agregador — el endpoint y la auth exactos los confirmas contra la documentación vigente de tu propio agregador.
Con dapp creas el cargo con POST /dapp-codes/. La auth es una API key privada sobre HTTP Basic: la API key va como password, y el usuario va vacío. El gotcha que te va a costar una tarde: si no mandas el header User-Agent, la API responde 403. Mándalo siempre, de forma explícita.
# Ilustrativo — específico de dapp.mx como ejemplo de agregador.
# Confirma endpoint, auth y campos contra la documentación VIGENTE de tu agregador (2026).
import requests
from requests.auth import HTTPBasicAuth
DAPP_API_KEY = "tu_api_key_privada" # nunca en el cliente; solo backend
def crear_cargo_codi(order_id: str, monto: str):
resp = requests.post(
"https://api.dapp.mx/v2/dapp-codes/",
# HTTP Basic: API key como password, usuario VACIO
auth=HTTPBasicAuth("", DAPP_API_KEY),
headers={
# Sin User-Agent => 403. Mandalo siempre, de forma explicita.
"User-Agent": "miapp/1.0",
"Content-Type": "application/json",
},
json={
"amount": monto, # confirma formato/unidad exactos en tu agregador
"description": f"Orden {order_id}",
"reference": order_id, # TU referencia: asi asocias el webhook a la orden
"qr_source": 1, # 1 = CoDi en el ejemplo de dapp; confirma vigente
},
timeout=15,
)
resp.raise_for_status()
data = resp.json()
# data trae el QR dinamico / payload de push para mostrarle al cliente
return data
Comisiones, montos y límites: trátalos como time-sensitive, “confirmar vigentes a 2026” con tu agregador. No los hardcodees como verdad eterna.
Verificar el webhook firmado antes de confiar en un solo peso
Esta es la parte que las páginas de bancos se saltan, y es el corazón de la integración. La confirmación del pago no llega en la respuesta del POST — llega después, como un webhook. El webhook es el dinero. Si lo manejas mal, surtes pedidos que nadie pagó.
Aquí hay que separar el principio genérico del esquema concreto de tu agregador, porque mezclarlos desinforma. El principio genérico, inviolable: verifica la firma ANTES de confiar en el evento o surtir nada. El esquema concreto cambia por proveedor — y replicarlo EXACTO es lo que hace que la firma cuadre.
En muchos rieles se firma el cuerpo crudo (raw body), y ahí la regla es no re-serializar. Pero en el ejemplo de dapp la firma NO es sobre el cuerpo crudo: es RSA + SHA-512 sobre una cadena reconstruida a partir de campos específicos, unidos por | en este orden — id|currency|amount|description|reference|date — con los campos nulos como cadena vacía. Si verificas sobre el raw body con dapp, la firma nunca va a cuadrar y vas a rechazar webhooks legítimos (o, peor, ignorar el fallo y surtir sin verificar). Reconstruye la cadena EXACTA que firma tu agregador y verifica contra eso.
Después de verificar: idempotencia sobre el id del cargo/transacción, para que los reintentos y duplicados surtan el pedido una sola vez. Y asocia el webhook a tu orden por tu referencia — nunca surtas un pedido a partir de un evento que no casa con una orden.
# Ilustrativo — verificacion RSA-SHA512 al estilo dapp.mx: la firma es sobre
# una CADENA de campos unidos por '|', NO sobre el cuerpo crudo.
# Confirma el orden/formato EXACTO de los campos con la doc VIGENTE de tu agregador.
import json
import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.exceptions import InvalidSignature
PUBLIC_KEY = serialization.load_pem_public_key(AGGREGATOR_PUBLIC_KEY_PEM)
def webhook_codi(request):
raw = request.get_data() # bytes crudos, para parsear una sola vez
evento = json.loads(raw)
# Reconstruye la cadena firmada EXACTA: id|currency|amount|description|reference|date
# Campos nulos => cadena vacia. Confirma orden/formato con tu agregador.
partes = [
evento.get("id", "") or "",
evento.get("currency", "") or "",
str(evento.get("amount", "") or ""),
evento.get("description", "") or "",
evento.get("reference", "") or "",
evento.get("date", "") or "",
]
mensaje = "|".join(partes).encode("utf-8")
firma = base64.b64decode(evento["security"]["signature"])
try:
PUBLIC_KEY.verify(
firma,
mensaje, # verifica sobre la cadena reconstruida
padding.PKCS1v15(),
hashes.SHA512(),
)
except InvalidSignature:
return ("firma invalida", 403)
charge_id = evento["id"]
order_id = evento["reference"]
# Idempotencia: si ya procesamos este charge_id, no surtas de nuevo
if ledger.ya_procesado(charge_id):
return ("ok (duplicado ignorado)", 200)
orden = ordenes.buscar_por_referencia(order_id)
if orden is None:
return ("orden no encontrada", 404) # nunca surtas sin asociar
ledger.marcar_pagada(orden, charge_id, evento) # escribe exactamente una vez
surtir(orden)
return ("ok", 200)
Y maneja el ciclo de vida del QR: la expiración/timeout, y el estado “escaneado pero no pagó” — porque va a pasar, y tu UI y tu ledger tienen que tolerarlo.
Liquidación irreversible: qué cambia en tu backend frente a tarjetas
Si vienes de tarjetas, tu modelo mental tiene que cambiar aquí. CoDi/SPEI liquida en tiempo real y es irreversible: no hay contracargos ni disputas estilo tarjeta. No modelas un estado de auth/capture en dos pasos, ni un estado de disputa pendiente, ni recibes webhooks de chargeback.
Lo que sí cambia: un reembolso es un nuevo pago SPEI de salida, no una reversa del original. Tú eres dueño de esa lógica de salida. Y como no hay “deshacer”, el monto tiene que estar bien a la primera — tu validación pre-cobro se vuelve crítica, porque ahí es donde vive el riesgo ahora.
El trade-off es real: la superficie de disputas es más simple (no peleas chargebacks), pero la irreversibilidad mueve el riesgo hacia tu validación previa. Es un intercambio que a mí me gusta para muchos casos MX, pero entra con los ojos abiertos.
CoDi / SPEI
- Liquidación en tiempo real, 24/7, irreversible
- Sin contracargos ni disputas estilo tarjeta
- Reembolso = nuevo pago SPEI de salida
- Sin auth/capture: un solo paso
- Riesgo se mueve a tu validación pre-cobro
Tarjetas
- Auth/capture en dos pasos, reversible
- Contracargos y disputas que peleas
- Reembolso = reversa del cargo original
- Estado de disputa pendiente que modelar
- Riesgo distribuido en el ciclo de disputa
Conciliación: asocia cada pago por clave de rastreo
Esta es la disciplina operativa que las páginas de marketing nunca te cuentan, y la que rompe en producción si la ignoras.
Guarda la clave de rastreo de SPEI de cada liquidación en el registro de la orden y de tu ledger — cuando tu agregador la exponga (es un campo a confirmar en su doc; no todos lo entregan en el mismo payload firmado). Esa clave es tu fuente de verdad para la pregunta de oro: “¿esto se pagó o no?”. Cuando un cliente te diga “ya pagué” — y va a pasar — la clave de rastreo es lo que cierra el caso sin drama.
Casa la clave de rastreo con la orden y deja eso como fuente de verdad. Esto además alimenta una contabilidad limpia aguas abajo y el timbrado/CFDI que venga después. Combínalo con el manejo idempotente del webhook para que la entrada del ledger se escriba exactamente una vez. La conciliación por clave de rastreo es, en serio, donde la integración se vuelve confiable.
Si quieres ver el mismo músculo de webhooks aplicado a otro riel SPEI/OXXO, lo trabajé en integrar Conekta con OXXO, SPEI y webhooks.
- Elige tu caminoParticipante de SPEI (carga regulatoria) vs agregador. La mayoría: agregador.
- Genera un QR dinámico por ordenPOST /dapp-codes/ con monto + tu referencia; User-Agent seteado.
- Verifica el webhook + idempotenciaReconstruye la cadena de campos, RSA-SHA512; idempotente sobre el charge id.
- Concilia por clave de rastreoGuarda la clave de SPEI cuando tu agregador la exponga y cásala con la orden.
- Maneja expiración y reembolsosQR expirado / escaneado-no-pagado; reembolso = nuevo pago de salida.
Preguntas frecuentes: CoDi, DiMo y agregadores
¿Puedo aceptar DiMo por API? Hoy no hay una API pública de cobro DiMo para negocios — confírmalo vigente a 2026 con tu agregador, porque el ecosistema se mueve. DiMo es P2P por número de teléfono. Para aceptación programática usa CoDi (QR dinámico vía agregador) o CLABEs virtuales / SPEI.
¿Tengo que ser participante de SPEI? No. La mayoría de los builders usan un agregador. Ser participante directo tiene requisitos regulatorios reales — eso lo platicas con compliance, no es trabajo de código.
¿Hay contracargos en CoDi? No. La liquidación es irreversible. Un reembolso es un nuevo pago SPEI de salida, no una reversa.
¿Cuánto dura válido un QR de CoDi? Lo configura el agregador. Maneja la expiración/timeout y el estado “escaneado pero no pagó” en tu lógica.
¿Y las comisiones y límites? Confirma los valores vigentes con tu agregador y con un contador. Trátalos como time-sensitive a 2026 — yo no te los hardcodeo como verdad eterna.
Si evalúas alternativas, la pasarela dominante de MX la cubro en cómo integrar Mercado Pago, y tengo más guías en pagos LATAM.
A producción
El path completo, en checklist:
- Elige camino: participante de SPEI vs agregador — la mayoría vamos por agregador.
- Genera un QR dinámico por cada orden (monto + tu referencia), con el
User-Agentseteado. - Verifica el webhook firmado RSA-SHA512 reconstruyendo la cadena exacta de campos de tu agregador, con idempotencia sobre el charge id, y asócialo a la orden.
- Concilia por clave de rastreo cuando tu agregador la exponga — esa es tu fuente de verdad.
- Maneja la expiración del QR y el reembolso como nuevo pago de salida — no hay contracargos.
- DiMo: no hay API de cobro para negocios — usa CoDi o CLABEs virtuales.
Y lo repito sin pena: soy ingeniero, no tu contador. El status de participante, las comisiones y las reglas regulatorias confírmalos vigentes a 2026 con compliance. Fuentes oficiales para que verifiques de primera mano: dapp.mx CoDi, Banxico CoDi/DiMo y el World Bank FASTT (SPEI/CoDi/DiMo).