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