Validación de cuentas
Sabe de quién es una CLABE — nombre y RFC, directo del banco — antes de que se mueva el dinero. Una llamada para iniciar, otra para leer la respuesta.
Tus usuarios escriben un solo número: su CLABE. Detrás, Tonder confirma que la cuenta es real y que les pertenece — por nombre y RFC, directo del banco — antes de arriesgar un solo peso real. Es una API pequeña e independiente: una llamada para iniciar, otra para leer la respuesta.
Míralo en un cajero de iGaming. El demo interactivo es un cajero de casino simulado que valida la CLABE del jugador desde el primer depósito y antes de un retiro, contra el sandbox, mostrando cada llamada al API conforme ocurre. Está protegido con contraseña: pídesela a tu contacto en Tonder.
Cómo funciona
Tonder envía un depósito de 0.01 MXN a la CLABE. Corre por nuestra cuenta — nunca aparece en tus estados de cuenta — y ocurre una vez por cuenta.
El recibo interbancario oficial (el CEP que Banxico publica por cada transferencia) devuelve el nombre y RFC del titular — lo que el banco tiene registrado, no lo que alguien tecleó en un formulario.
Compara contra quien esperabas y paga, marca o bloquea. name_match y rfc_match regresan
con el resultado.
El resultado se recuerda: una consulta repetida de la misma CLABE dentro de la ventana (24 horas por defecto) lo reutiliza — sin segundo depósito. Pasada la ventana Tonder vuelve a verificar, porque una cuenta puede cambiar de dueño en el banco.
Dónde encaja
Envía mode para indicar cómo se usará la cuenta. La misma validación sirve para los tres casos.
Depósitos — mode: "deposit"
Cuando un usuario registra la cuenta desde la que va a depositar, una verificación silenciosa la convierte en cuenta verificada, y todo lo que esa cuenta toque después hereda esa confianza.
| Lo que siente tu usuario | Lo que obtiene tu negocio |
|---|---|
| Un campo, cero fricción. Sin lista de bancos, sin RFC que escribir, sin pantalla de "verificación pendiente" — un estado Verificada que aparece solo. | Cada depósito atribuible a un titular confirmado desde el primer día; reembolsos que no se pueden desviar por ingeniería social a la cuenta de un extraño; una historia de cumplimiento que se responde sola. |
Retiros — mode: "withdrawal"
El error caro en los retiros es el dinero enviado a una cuenta que no es de tu usuario. La validación mueve ese descubrimiento de después de la transferencia a antes — el único momento en que todavía puedes hacer algo.
| Sin validación | Con validación |
|---|---|
| Te enteras de una cuenta equivocada cuando la transferencia rebota — o peor, cuando no rebota. El usuario espera, soporte investiga y la pérdida suele ser tuya. | La inconsistencia aparece antes del envío, con los datos del propio banco en la mano. Tú eliges la reacción — bloquear con un mensaje claro, o pagar y marcar — y los retiros repetidos a cuentas conocidas siguen siendo instantáneos. |
Ambos — mode: "both"
Usada en ambos lados, una sola validación se vuelve un ciclo: la cuenta verificada al depositar ya es de confianza al retirar. El dinero que entra y el que sale se mueven contra una misma identidad confirmada — nunca pagas la verificación dos veces, el primer retiro no tiene nada que esperar, y depositar desde la cuenta de otro y retirar a la cuenta de otro (las dos mitades de casi todo el fraude de pagos) se cierran con la misma prueba de titularidad.
Bueno saberlo
| Solo la CLABE | Tu usuario nunca elige banco ni escribe el RFC de su cuenta. Tonder resuelve la institución a partir de la CLABE y el banco nos dice el resto. |
| Una vez, y se recuerda | Una validación por cuenta, en caché. Una consulta repetida dentro de la ventana (24 horas por defecto) reutiliza el resultado. Pasada la ventana, Tonder vuelve a verificar. |
| Los nombres son difusos, el RFC no | name_match ignora mayúsculas, acentos y espacios de más — pero no el orden de las palabras ni un apellido extra (Kuhlman Dede y Dede Kuhlman Garcia regresan false para Dede Kuhlman). rfc_match es exacto, sin distinguir mayúsculas. Ambas se recalculan en cada llamada, incluso en caché. |
| El veredicto es tuyo | Tonder devuelve el holder_name, el holder_id (RFC) del banco y las dos banderas. Tu política decide qué significa una coincidencia — o una discrepancia —: pagar, marcar o bloquear. |
| Si el banco tarda | A veces el recibo tarda más. El estado se queda en pending — es un "intenta de nuevo en un momento", no una cuenta mala. |
| Hecho para auditorías | Cada validación conserva su verification_id y la confirmación del banco, así que cualquier decisión se puede explicar meses después. |
Endpoints
Dos llamadas integran la validación en tu flujo. Ambas se autentican con tu API key de Tonder —
Authorization: Token <API_KEY>, como todos los demás endpoints. Las validaciones se acotan por tu
API key; business_id es opcional.
El prefijo Token no es opcional. El header lleva un esquema y una credencial:
Authorization: Token c256f3…. Si envías la llave sola — Authorization: c256f3… — la API no
tiene esquema bajo el cual leerla y responde 401 "Authentication credentials were not provided"
como si no hubieras enviado nada. Es la misma regla que en todos los endpoints de Tonder; es el
error más común de la primera llamada. Usa tu llave pública — la secreta responde
401 "Invalid token".
| Método | Ruta | Propósito |
|---|---|---|
POST | /api/v1/account-verifications/ | Inicia (o devuelve la validación en caché de) una CLABE. |
GET | /api/v1/account-verifications/{account_number} | Lee una validación, refrescada en vivo mientras está pendiente. |
Cuentas de prueba en sandbox. Usa CLABEs de la institución de prueba 646180… con dígito
verificador válido — 646180000000000009 funciona, y cualquier otra de ese rango también. Otras
instituciones se rechazan río arriba (502). El recibo llega en segundos y devuelve un nombre y RFC
sintéticos; cópialos del primer GET a expected_holder_name / expected_holder_id para ver
las banderas en true.
Pasos
Envía el account_number (CLABE). Opcionalmente agrega tu business_id, el mode
(deposit · withdrawal · both) y los valores que esperas — expected_holder_name y
expected_holder_id (RFC) — para recibir las banderas de coincidencia.
curl -X POST https://stage.tonder.io/api/v1/account-verifications/ \
-H "Authorization: Token YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"account_number": "646180000000000009",
"business_id": "97",
"mode": "withdrawal",
"expected_holder_name": "Dede Kuhlman",
"expected_holder_id": "RBZZ190718TEA"
}'curl -X POST https://app.tonder.io/api/v1/account-verifications/ \
-H "Authorization: Token YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"account_number": "646180000000000009",
"business_id": "97",
"mode": "withdrawal",
"expected_holder_name": "Dede Kuhlman",
"expected_holder_id": "RBZZ190718TEA"
}'Recibes un verification_id y un status — normalmente pending:
{
"account_number": "646180000000000009",
"verification_id": "accv_3JkKAxCDvjeoUGgQPYzOMEFDl0K",
"status": "pending",
"holder_name": null,
"holder_id": null,
"institution": "90646",
"name_match": null,
"rfc_match": null,
"reason": null
}Mientras está pending, Tonder la refresca en vivo desde el banco; una vez succeeded o
failed, es final.
curl "https://stage.tonder.io/api/v1/account-verifications/646180000000000009?business_id=97" \
-H "Authorization: Token YOUR_API_KEY"curl "https://app.tonder.io/api/v1/account-verifications/646180000000000009?business_id=97" \
-H "Authorization: Token YOUR_API_KEY"Cuando llega el recibo del banco:
{
"account_number": "646180000000000009",
"verification_id": "accv_3JkKAxCDvjeoUGgQPYzOMEFDl0K",
"status": "succeeded",
"holder_name": "Dede Kuhlman",
"holder_id": "RBZZ190718TEA",
"institution": "90646",
"name_match": true,
"rfc_match": true,
"reason": null
}Usa status más las banderas para pagar, marcar o bloquear — en tus términos. Una consulta
repetida de la misma CLABE dentro de la ventana devuelve el resultado en caché al instante.
Estados
status | Significado | ¿Final? |
|---|---|---|
pending | El recibo del banco aún no llega — vuelve a consultar en un momento. | No |
succeeded | Titularidad confirmada; regresan holder_name y holder_id. | Sí |
failed | No se pudo verificar; ver reason. | Sí |
Campos de la respuesta
| Campo | Descripción |
|---|---|
account_number | La CLABE validada. |
verification_id | Identificador de esta validación — guárdalo; es tu rastro de auditoría. |
status | pending, succeeded o failed. |
holder_name | Nombre del titular tal como lo tiene el banco. null mientras está pendiente. |
holder_id | RFC del titular tal como lo tiene el banco. null mientras está pendiente. |
institution | Código de la institución bancaria, resuelto desde la CLABE — presente desde la primera respuesta, antes de que el banco conteste. Ver Referencia bancaria. |
name_match | holder_name vs. expected_holder_name, difuso. null si no lo enviaste. |
rfc_match | holder_id vs. expected_holder_id, exacto. null si no lo enviaste. |
reason | Por qué una validación quedó en failed. null en otro caso. |
Errores
| HTTP | Cuerpo | Significado |
|---|---|---|
400 | {"account_number": ["This value does not match the required pattern."]} | Un campo falló la validación — el cuerpo mapea cada campo a sus errores. Misma forma para un mode inválido. |
401 | {"detail": "Authentication credentials were not provided."} | Header faltante o incorrecto. Usa Authorization: Token <API_KEY> con tu llave pública. |
404 | {"detail": "No verification for **************0009"} | No existe validación para esa CLABE. El número regresa enmascarado. |
502 | {"detail": "…create returned HTTP 400"} | El banco no pudo iniciar la validación — normalmente una CLABE con dígito verificador incorrecto, o una institución no soportada en este ambiente. |
Intégrala en tu flujo
Dos llamadas, un ciclo, una decisión. Aquí va cada pieza.
Cuándo llamarla
| Modo | Llámala cuando… | Después |
|---|---|---|
deposit | El usuario guarda la CLABE desde la que va a depositar. | Muestra Verificada cuando tenga éxito; atribuye depósitos y dirige reembolsos a esa cuenta. |
withdrawal | El usuario pide un retiro a una CLABE que aún no has verificado. | Crea el retiro solo cuando la validación tenga éxito y coincida. |
both | El usuario registra su cuenta. | Una validación cubre todos los depósitos y retiros posteriores. |
Envía expected_holder_name y expected_holder_id desde tu registro del usuario — eso
convierte la respuesta en un sí/no en lugar de dos strings que tendrías que comparar tú.
La secuencia
POST /account-verifications/. Si la CLABE se validó en las últimas 24 horas, la respuesta ya
viene succeeded (o failed) — sáltate el ciclo y decide.
El recibo del banco suele llegar en segundos. Consulta GET /account-verifications/{account_number} cada pocos segundos; detente en el primer succeeded
o failed. Si sigue pending pasado tu presupuesto (un minuto es de sobra), no lo trates
como falla — deja la solicitud en cola y reintenta en un momento. Nunca pagues contra un
resultado pending.
Aplica tu política a status y las banderas (tabla abajo). Guarda verification_id junto
al registro de la cuenta del usuario — es tu rastro de auditoría.
Crea el retiro con POST /process/ usando la misma CLABE,
o marca la cuenta de depósito como Verificada. Antes de un retiro posterior, vuelve a
llamar al POST: dentro de la ventana es una respuesta en caché instantánea; fuera de ella,
Tonder vuelve a verificar por ti.
Una política para empezar
Tonder devuelve hechos; el veredicto es tuyo. Este es un punto de partida razonable:
status | rfc_match | name_match | Haz |
|---|---|---|---|
succeeded | true | true | Paga / marca Verificada. |
succeeded | true | false | Marca para revisión. Los nombres son difusos — acentos, orden, segundo apellido — y el RFC ya prueba la titularidad. |
succeeded | false | cualquiera | Bloquea. rfc_match es exacto; otro RFC es otra persona. |
succeeded | null | null | No enviaste valores esperados — compara holder_name / holder_id tú mismo. |
failed | — | — | Bloquea y muestra al usuario algo accionable a partir de reason. |
pending (pasado tu presupuesto) | — | — | Reintenta después. No es una cuenta mala. |
Código
const BASE = 'https://stage.tonder.io/api/v1'; // app.tonder.io en producción
const headers = { Authorization: `Token ${process.env.TONDER_API_KEY}`, 'Content-Type': 'application/json' };
async function validateAccount(clabe, user) {
let v = await fetch(`${BASE}/account-verifications/`, {
method: 'POST',
headers,
body: JSON.stringify({
account_number: clabe,
business_id: process.env.TONDER_BUSINESS_ID,
mode: 'withdrawal',
expected_holder_name: user.fullName,
expected_holder_id: user.rfc,
}),
}).then((r) => r.json());
// Consulta mientras esté pendiente — suelen ser segundos. Renuncia a *esperar*, no a la cuenta.
for (let i = 0; i < 20 && v.status === 'pending'; i++) {
await new Promise((r) => setTimeout(r, 3000));
v = await fetch(
`${BASE}/account-verifications/${clabe}?business_id=${process.env.TONDER_BUSINESS_ID}`,
{ headers },
).then((r) => r.json());
}
await db.saveVerification(user.id, clabe, v.verification_id, v.status);
if (v.status === 'pending') return { decision: 'retry_later' };
if (v.status === 'failed') return { decision: 'block', reason: v.reason };
if (v.rfc_match === false) return { decision: 'block', reason: 'rfc_mismatch' };
if (v.name_match === false) return { decision: 'flag', reason: 'name_mismatch' };
return { decision: 'pay', holder: v.holder_name };
}import os, time, requests
BASE = "https://stage.tonder.io/api/v1" # app.tonder.io en producción
HEADERS = {"Authorization": f"Token {os.environ['TONDER_API_KEY']}", "Content-Type": "application/json"}
BUSINESS_ID = os.environ["TONDER_BUSINESS_ID"]
def validate_account(clabe: str, user) -> dict:
v = requests.post(f"{BASE}/account-verifications/", headers=HEADERS, json={
"account_number": clabe,
"business_id": BUSINESS_ID,
"mode": "withdrawal",
"expected_holder_name": user.full_name,
"expected_holder_id": user.rfc,
}).json()
# Consulta mientras esté pendiente — suelen ser segundos. Renuncia a *esperar*, no a la cuenta.
for _ in range(20):
if v["status"] != "pending":
break
time.sleep(3)
v = requests.get(
f"{BASE}/account-verifications/{clabe}",
headers=HEADERS, params={"business_id": BUSINESS_ID},
).json()
db.save_verification(user.id, clabe, v["verification_id"], v["status"])
if v["status"] == "pending":
return {"decision": "retry_later"}
if v["status"] == "failed":
return {"decision": "block", "reason": v["reason"]}
if v["rfc_match"] is False:
return {"decision": "block", "reason": "rfc_mismatch"}
if v["name_match"] is False:
return {"decision": "flag", "reason": "name_mismatch"}
return {"decision": "pay", "holder": v["holder_name"]}No hay webhook para validaciones — el GET es la notificación. Mantén el ciclo del lado del
servidor y nunca expongas la API key al navegador.
Siguientes pasos
Demo de iGaming
Un cajero de casino simulado que corre Validación de cuentas en el sandbox (protegido con contraseña).
Iniciar una validación de cuenta
El endpoint POST — esquema y playground.
Leer una validación de cuenta
El endpoint GET — esquema y playground.
Retiros
Dispersa a la cuenta que acabas de verificar.
Referencia bancaria
Códigos de institución y estructura de la CLABE.
