Validación de cuentas

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 usuarioLo 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ónCon 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 CLABETu 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 recuerdaUna 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 noname_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 tuyoTonder 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 tardaA 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íasCada 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étodoRutaPropó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

statusSignificado¿Final?
pendingEl recibo del banco aún no llega — vuelve a consultar en un momento.No
succeededTitularidad confirmada; regresan holder_name y holder_id.Sí
failedNo se pudo verificar; ver reason.Sí

Campos de la respuesta

CampoDescripción
account_numberLa CLABE validada.
verification_idIdentificador de esta validación — guárdalo; es tu rastro de auditoría.
statuspending, succeeded o failed.
holder_nameNombre del titular tal como lo tiene el banco. null mientras está pendiente.
holder_idRFC del titular tal como lo tiene el banco. null mientras está pendiente.
institutionCó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_matchholder_name vs. expected_holder_name, difuso. null si no lo enviaste.
rfc_matchholder_id vs. expected_holder_id, exacto. null si no lo enviaste.
reasonPor qué una validación quedó en failed. null en otro caso.

Errores

HTTPCuerpoSignificado
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

ModoLlámala cuando…Después
depositEl usuario guarda la CLABE desde la que va a depositar.Muestra Verificada cuando tenga éxito; atribuye depósitos y dirige reembolsos a esa cuenta.
withdrawalEl 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.
bothEl 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:

statusrfc_matchname_matchHaz
succeededtruetruePaga / marca Verificada.
succeededtruefalseMarca para revisión. Los nombres son difusos — acentos, orden, segundo apellido — y el RFC ya prueba la titularidad.
succeededfalsecualquieraBloquea. rfc_match es exacto; otro RFC es otra persona.
succeedednullnullNo 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

¿Te resultó útil esta página?

En esta página