TAuth API v1

TAuth API

Códigos de un solo uso (OTP) por SMS y WhatsApp: TAuth genera el PIN, lo entrega por el canal pedido (con fallback automático a otro canal) y lo verifica.

URL base:

https://api-tauth.ti.t-mobs.com/v1/

Autenticación

Cada request lleva la API key del cliente, que entrega T-mob, en el header Authorization:

Authorization: Token 3f2a9c1e-5b7d-4e8a-9f10-2c3d4e5f6a7b

Sin el header, o con una key inválida o desactivada, la respuesta es 403.

La API key es secreta: se usa solo desde el backend, nunca desde una app móvil o el navegador.

Flujo

  1. Enviar el OTP al número del usuario con POST /v1/send-otp/. La respuesta trae un transaction: guardarlo asociado al usuario.
  2. El usuario recibe el PIN por WhatsApp o SMS y lo ingresa en tu app.
  3. Verificar el PIN con POST /v1/verify-otp/{transaction}/. Si otp_status es Verified, el número está validado.
  4. Opcional: mostrar el estado de la entrega con GET /v1/otp/{transaction}/, ofrecer un botón "reenviar" con POST /v1/otp/{transaction}/resend/ o recibir los eventos por webhook.

Enviar un OTP

POST /v1/send-otp/

Body

CampoTipoDescripción
msisdnstringobligatorio Número de destino en formato internacional, con el código de país: + opcional y de 6 a 15 dígitos (+5491112345678). Los espacios se ignoran. El país de destino se toma del código de país y tiene que estar habilitado para tu cuenta.
channelstringopcional whatsapp o sms. Sin este campo se usa el canal principal configurado para tu cuenta que esté disponible en el país del número.
fallbackstringopcional sms o whatsapp. Si el canal principal no entrega el mensaje, se reenvía el mismo PIN por este canal. Si el canal principal no está disponible en el país del número, se usa directamente este. Ver Canales y fallback.
templatestringopcional Template del mensaje configurado para tu cuenta (texto, largo del PIN, vencimiento). Sin este campo se usa el template por defecto.
countrystringopcional Código ISO de 2 letras (AR). Si se envía, tiene que coincidir con el país del msisdn (control adicional).

Headers opcionales

HeaderDescripción
Idempotency-KeyIdentificador único del pedido (hasta 100 caracteres). Si se repite el request con la misma key, se devuelve la misma respuesta sin mandar otro OTP. Ver Idempotencia.

Ejemplo

curl -X POST https://api-tauth.ti.t-mobs.com/v1/send-otp/ \
  -H "Authorization: Token <api_key>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: signup-8f14e45f" \
  -d '{"msisdn": "+5491112345678", "channel": "whatsapp", "fallback": "sms"}'

Respuesta 202 Accepted

{
  "transaction": "0b6e3c55-3d7e-4a43-9d61-5f0d6f8f2f1a",
  "message": {
    "status": "Accepted",
    "description": "Accepted for Delivery",
    "channel": "whatsapp",
    "updated_at": "2026-10-28T14:03:21.512000Z"
  },
  "otp": {
    "otp_status": "Not verified",
    "expires_at": "2026-10-28T14:04:21.512000Z"
  }
}

El envío es asincrónico: 202 significa que el OTP fue aceptado y está en camino.

Errores

CódigoerrorCausa
400Invalid JSON.El body no es un objeto JSON.
400Invalid parameters.Falta el msisdn o no tiene un formato válido.
400Invalid channel.channel o fallback no son sms ni whatsapp.
400Country not enabled.El país del número no está habilitado para tu cuenta (o el número no tiene código de país).
400Country not available for channel.El canal pedido no está disponible para ese país y no se mandó un fallback disponible.
400Invalid template.El template no existe o está inactivo.
400Invalid msisdn.El número no respeta el formato de su país, o no coincide con country.
400Invalid Idempotency-Key.La key está vacía, es muy larga o tiene caracteres no imprimibles.
402Monthly spend limit reached.Se alcanzó el tope de gasto mensual de tu cuenta.
409Request in progress.Otro request con la misma Idempotency-Key se está procesando.
422Idempotency-Key already used with another body.La key ya se usó con otros datos.
429MSISDN rate limit exceeded.Demasiados OTP a ese número. Ver Límites.
429Client rate limit exceeded.Demasiados OTP por minuto en tu cuenta.

Verificar un OTP

POST /v1/verify-otp/{transaction}/
curl -X POST https://api-tauth.ti.t-mobs.com/v1/verify-otp/0b6e3c55-3d7e-4a43-9d61-5f0d6f8f2f1a/ \
  -H "Authorization: Token <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"otp": "482193"}'

Respuesta 200 OK

{
  "transaction": "0b6e3c55-3d7e-4a43-9d61-5f0d6f8f2f1a",
  "message": {
    "status": "Delivered",
    "description": "OTP reached final destination",
    "channel": "whatsapp",
    "updated_at": "2026-10-28T14:03:24.101000Z"
  },
  "otp": {
    "otp_status": "Verified",
    "expires_at": "2026-10-28T14:04:21.512000Z",
    "remaining_attempts": 5
  }
}

Una respuesta 200 no implica que el PIN sea correcto: hay que mirar otp.otp_status.

otp_statusSignificado
VerifiedPIN correcto. El OTP se consume: un segundo intento responde 404.
Not verifiedPIN incorrecto. Se puede volver a intentar mientras queden remaining_attempts.
BlockedSe agotaron los intentos. El OTP ya no se puede verificar: hay que pedir uno nuevo.
ExpiredPasó expires_at. Hay que pedir uno nuevo (o reenviarlo antes de que venza).

Errores

CódigoerrorCausa
400Invalid JSON.El body no es un objeto JSON.
400Invalid parameters.Falta otp o no es un string.
404Invalid transaction.La transacción no existe, es de otra cuenta, ya fue verificada o venció hace más de 24 horas.

Consultar el estado

GET /v1/otp/{transaction}/

Devuelve el estado de la entrega y del OTP, con el mismo formato que la verificación (sin el PIN). Sirve para mostrar en tu app "enviado por WhatsApp", "entregado", etc.

curl https://api-tauth.ti.t-mobs.com/v1/otp/0b6e3c55-3d7e-4a43-9d61-5f0d6f8f2f1a/ \
  -H "Authorization: Token <api_key>"

Una transacción ya verificada responde otp_status: "Verified". Una inexistente o de otra cuenta, 404.

Para enterarte de los cambios sin consultar, conviene usar webhooks.

Reenviar

POST /v1/otp/{transaction}/resend/

Reenvía el mismo PIN (no hace falta body). Si hay otro proveedor u otro canal disponible, usa el siguiente; si no, el mismo. El vencimiento se reinicia.

curl -X POST https://api-tauth.ti.t-mobs.com/v1/otp/0b6e3c55-3d7e-4a43-9d61-5f0d6f8f2f1a/resend/ \
  -H "Authorization: Token <api_key>"

Responde 202 con el estado actualizado y otp.remaining_resends.

CódigoerrorCausa
403Resend not enabled.Tu cuenta no tiene reenvíos habilitados.
404Invalid transaction.No existe, es de otra cuenta o ya fue verificada.
409OTP expired. / OTP blocked.El OTP venció o se agotaron los intentos: hay que pedir uno nuevo.
429Resend too soon.No pasó el tiempo mínimo desde el último envío.
429Resend limit exceeded.Se alcanzó la cantidad máxima de reenvíos.

Formato por país

GET /v1/info/?ip={ip}

Con la IP pública del usuario devuelve el formato de número de su país, para prellenar el prefijo o validar el número antes de enviar.

curl "https://api-tauth.ti.t-mobs.com/v1/info/?ip=181.30.12.4" -H "Authorization: Token <api_key>"
{
  "results": {
    "format": "+54 9 XX XXXX-XXXX",
    "current_country": "AR",
    "current_regexp": "\\+549[0-9]{10}",
    "current_prefix": "54"
  }
}

400 Invalid parameters. si la IP no es pública; 400 Country not available. si el país no está habilitado.

Consumo

GET /v1/usage/?from=YYYY-MM-DD&to=YYYY-MM-DD

Tu consumo por día y por país, con el importe que se factura. Sin parámetros devuelve el mes en curso; el rango máximo es de 92 días. Los datos del día actual se actualizan cada 30 minutos.

curl "https://api-tauth.ti.t-mobs.com/v1/usage/?from=2026-10-01&to=2026-10-31" -H "Authorization: Token <api_key>"
{
  "currency": "USD",
  "from": "2026-10-01",
  "to": "2026-10-31",
  "days": [
    {"date": "2026-10-28", "country": "AR", "otps": 120, "verified": 101, "not_verified": 19,
     "blocked": 2, "expired": 5, "abandoned": 12,
     "sms_sent": 30, "sms_delivered": 28, "whatsapp_sent": 118, "whatsapp_delivered": 112,
     "rejected": 3, "rejections": {"invalid_msisdn": 2, "msisdn_rate_limit": 1},
     "amount": "5.0400"}
  ],
  "totals": {"otps": 120, "verified": 101, "not_verified": 19, "blocked": 2, "expired": 5,
             "abandoned": 12, "sms_sent": 30, "sms_delivered": 28,
             "whatsapp_sent": 118, "whatsapp_delivered": 112, "rejected": 3, "amount": "5.0400"}
}

not_verified se divide en blocked (se agotaron los intentos), expired (se ingresaron PIN incorrectos y venció) y abandoned (nunca se ingresó un PIN). Los números del día actual son provisorios: un OTP vigente cuenta como no verificado hasta que se verifica.

rejected cuenta los send-otp rechazados del día y rejections los separa por motivo: invalid_json, invalid_msisdn, invalid_channel, country_not_enabled, channel_not_available, invalid_template, spend_limit, client_rate_limit y msisdn_rate_limit. Sirve para detectar errores de integración. Los rechazos de números sin país reconocido van con "country": null.

Se factura cada mensaje enviado (incluidos los fallbacks y los reenvíos), al precio de tu cuenta para ese país y canal. Los días se cuentan en hora de Argentina (UTC-3).

Estados

Entrega del mensaje (message.status)

EstadoSignificado
AcceptedAceptado, en cola para enviar.
In progressEnviado al operador, sin confirmación de entrega todavía. Algunos destinos no confirman: el mensaje puede llegar igual.
DeliveredEl operador confirmó la entrega.
FailedNo se pudo entregar por ningún canal disponible.

message.channel es el canal del último envío: si hubo fallback, cambia (por ejemplo de whatsapp a sms).

OTP (otp.otp_status)

Not verified, Verified, Blocked o Expired. Ver Verificar un OTP.

Canales y fallback

Con channel se elige el canal principal y con fallback el canal alternativo. Si el canal principal no entrega, TAuth reenvía el mismo PIN en la misma transacción por el canal alternativo y reinicia el vencimiento. No hay que hacer nada: el usuario usa el PIN que le llegue.

El fallback se activa cuando:

Si se agotan todas las opciones, message.status pasa a Failed.

Cada canal tiene que estar habilitado para el país del número. Si el canal pedido no lo está y se mandó fallback, el OTP sale directamente por el canal alternativo (la respuesta lo indica en message.channel). Cada mensaje enviado, incluidos los fallbacks, se factura al precio de su canal.

Webhooks

Si T-mob configura una URL de webhook para tu cuenta, TAuth le hace un POST JSON por cada evento:

EventoCuándoCampos extra
message.deliveredEl operador confirmó la entrega.channel
message.fallbackSe reenvió el PIN por otro proveedor o canal (fallback o reenvío pedido).reason, step
message.failedNo quedan canales para entregar.reason
otp.verifiedEl PIN se verificó.
otp.blockedSe agotaron los intentos de verificación.
billing.spend_alertEl gasto del mes llegó al porcentaje de aviso del tope (una vez por mes). Sin transaction.month, spent, limit, currency, percent
billing.spend_limit_reachedEl gasto del mes llegó al tope: desde ahora send-otp responde 402 (una vez por mes).ídem
POST /tu/webhook HTTP/1.1
Content-Type: application/json
X-TAuth-Event: message.delivered
X-TAuth-Timestamp: 1793196204
X-TAuth-Signature: sha256=5d41402abc4b2a76b9719d911017c592...

{"event":"message.delivered","transaction":"0b6e3c55-3d7e-4a43-9d61-5f0d6f8f2f1a","created_at":"2026-10-28T14:03:24.101000Z","channel":"whatsapp"}

Verificar la firma

X-TAuth-Signature es sha256= + el HMAC-SHA256, en hexadecimal, de {timestamp}.{body}, donde la clave es el secreto del webhook (lo entrega T-mob). Calcularlo sobre el body tal cual llega (sin parsear) y rechazar timestamps de más de 5 minutos.

# Python
import hashlib, hmac, time

def valid(headers, raw_body: bytes, secret: str) -> bool:
    ts = headers["X-TAuth-Timestamp"]
    expected = "sha256=" + hmac.new(secret.encode(), ts.encode() + b"." + raw_body,
                                    hashlib.sha256).hexdigest()
    return (hmac.compare_digest(expected, headers["X-TAuth-Signature"])
            and abs(time.time() - int(ts)) < 300)
// Node.js
const crypto = require("crypto");

function valid(headers, rawBody, secret) {
  const ts = headers["x-tauth-timestamp"];
  const expected = "sha256=" + crypto.createHmac("sha256", secret)
    .update(ts + "." + rawBody).digest("hex");
  const got = headers["x-tauth-signature"] || "";
  return got.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))
    && Math.abs(Date.now() / 1000 - Number(ts)) < 300;
}

Entrega

Idempotencia

Si un send-otp da timeout o error de red, no se sabe si el OTP salió. Para reintentar sin mandar dos mensajes, se usa el header Idempotency-Key con un valor único por pedido (por ejemplo un UUID):

Errores

Los errores responden JSON con el campo error:

{"error": "Invalid template."}
CódigoSignificado
400Request inválido.
402Se alcanzó el tope de gasto mensual de la cuenta.
403API key ausente o inválida, o función no habilitada.
404Transacción o ruta inexistente.
405Método HTTP no permitido para la ruta (sin body).
409Conflicto de estado (OTP vencido o bloqueado, request en curso).
422Idempotency-Key reutilizada con otro body.
429Se superó un límite. Reintentar más tarde.
5xxError de TAuth. Reintentar con backoff (y con Idempotency-Key en send-otp).

Límites

Los configura T-mob para cada cuenta. Valores por defecto:

LímitePor defecto
Intentos de verificación por OTP5
OTP por número5 por hora
OTP por minuto en la cuentasin límite
Reenvíos por OTP3, con al menos 30 segundos entre envíos
Vencimiento del PINsegún el template (por defecto 60 segundos), se reinicia con cada reenvío
Gasto mensualsin tope, salvo que se acuerde uno (al alcanzarlo, send-otp responde 402)

Ejemplo completo

# Python 3, con requests
import uuid
import requests

BASE = "https://api-tauth.ti.t-mobs.com/v1"
AUTH = {"Authorization": "Token <api_key>"}

# 1. Enviar
r = requests.post(f"{BASE}/send-otp/",
                  json={"msisdn": "+5491112345678", "channel": "whatsapp", "fallback": "sms"},
                  headers={**AUTH, "Idempotency-Key": str(uuid.uuid4())}, timeout=10)
r.raise_for_status()
transaction = r.json()["transaction"]

# 2. El usuario ingresa el PIN que recibió
pin = input("PIN: ")

# 3. Verificar
r = requests.post(f"{BASE}/verify-otp/{transaction}/", json={"otp": pin},
                  headers=AUTH, timeout=10)
if r.status_code == 200 and r.json()["otp"]["otp_status"] == "Verified":
    print("Número verificado")
else:
    print("No verificado:", r.json())