TAuth API v1

TAuth API

One-time passwords (OTP) over SMS and WhatsApp: TAuth generates the PIN, delivers it through the requested channel (with automatic fallback to another one) and verifies it.

Base URL:

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

Authentication

Every request carries the client API key, issued by T-mob, in the Authorization header:

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

A missing header, or an invalid or disabled key, gets a 403.

The API key is secret: use it from your backend only, never from a mobile app or a browser.

Flow

  1. Send the OTP to the user's number with POST /v1/send-otp/. The response has a transaction: store it with the user.
  2. The user gets the PIN by WhatsApp or SMS and types it in your app.
  3. Verify the PIN with POST /v1/verify-otp/{transaction}/. If otp_status is Verified, the number is validated.
  4. Optional: show the delivery status with GET /v1/otp/{transaction}/, offer a "resend" button with POST /v1/otp/{transaction}/resend/, or receive the events through a webhook.

Send an OTP

POST /v1/send-otp/

Body

FieldTypeDescription
msisdnstringrequired Destination number in international format, with the country code: optional + and 6 to 15 digits (+5491112345678). Spaces are ignored. The destination country is taken from the country code and must be enabled for your account.
channelstringoptional whatsapp or sms. Defaults to the main channel configured for your account that is available for the number's country.
fallbackstringoptional sms or whatsapp. If the main channel does not deliver the message, the same PIN is resent through this one. If the main channel is not available for the number's country, this one is used directly. See Channels and fallback.
templatestringoptional Message template configured for your account (text, PIN length, expiration). Defaults to the default template.
countrystringoptional 2-letter ISO code (AR). If sent, it must match the country of the msisdn (extra check).

Optional headers

HeaderDescription
Idempotency-KeyUnique id of the request (up to 100 characters). Repeating the request with the same key returns the same response without sending another OTP. See Idempotency.

Example

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"}'

Response 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"
  }
}

Sending is asynchronous: 202 means the OTP was accepted and is on its way.

Errors

CodeerrorCause
400Invalid JSON.The body is not a JSON object.
400Invalid parameters.msisdn is missing or malformed.
400Invalid channel.channel or fallback is neither sms nor whatsapp.
400Country not enabled.The number's country is not enabled for your account (or the number has no country code).
400Country not available for channel.The requested channel is not available for that country and no available fallback was sent.
400Invalid template.The template does not exist or is inactive.
400Invalid msisdn.The number does not match its country format, or does not match country.
400Invalid Idempotency-Key.The key is empty, too long or has non-printable characters.
402Monthly spend limit reached.Your account reached its monthly spend limit.
409Request in progress.Another request with the same Idempotency-Key is being processed.
422Idempotency-Key already used with another body.The key was already used with different data.
429MSISDN rate limit exceeded.Too many OTPs to that number. See Limits.
429Client rate limit exceeded.Too many OTPs per minute for your account.

Verify an 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"}'

Response 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
  }
}

A 200 does not mean the PIN is right: check otp.otp_status.

otp_statusMeaning
VerifiedRight PIN. The OTP is consumed: a second attempt gets a 404.
Not verifiedWrong PIN. The user can try again while there are remaining_attempts.
BlockedNo attempts left. The OTP can no longer be verified: request a new one.
ExpiredPast expires_at. Request a new one (or resend it before it expires).

Errors

CodeerrorCause
400Invalid JSON.The body is not a JSON object.
400Invalid parameters.otp is missing or not a string.
404Invalid transaction.The transaction does not exist, belongs to another account, was already verified or expired more than 24 hours ago.

Get the status

GET /v1/otp/{transaction}/

Returns the delivery and OTP status, in the same format as the verification (without the PIN). Useful to show "sent by WhatsApp", "delivered", etc. in your app.

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

A verified transaction returns otp_status: "Verified". An unknown one, or one of another account, gets a 404.

To get the changes without polling, use webhooks.

Resend

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

Resends the same PIN (no body needed). If another provider or channel is available it uses the next one; otherwise the same one. The expiration restarts.

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

Returns 202 with the updated status and otp.remaining_resends.

CodeerrorCause
403Resend not enabled.Resends are not enabled for your account.
404Invalid transaction.Unknown, of another account or already verified.
409OTP expired. / OTP blocked.The OTP expired or has no attempts left: request a new one.
429Resend too soon.The minimum time since the last send has not passed.
429Resend limit exceeded.The maximum number of resends was reached.

Country format

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

Given the user's public IP, returns the number format of their country, to prefill the prefix or validate the number before sending.

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. if the IP is not public; 400 Country not available. if the country is not enabled.

Usage

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

Your usage per day and country, with the amount billed. Without parameters it returns the current month; the maximum range is 92 days. Today's data is refreshed every 30 minutes.

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 splits into blocked (no attempts left), expired (wrong PINs entered, then expired) and abandoned (no PIN was ever entered). Today's numbers are provisional: a live OTP counts as not verified until it is verified.

rejected counts the rejected send-otp requests of the day and rejections splits them by reason: invalid_json, invalid_msisdn, invalid_channel, country_not_enabled, channel_not_available, invalid_template, spend_limit, client_rate_limit and msisdn_rate_limit. Useful to spot integration errors. Rejections of numbers with no known country come with "country": null.

Every message sent is billed (fallbacks and resends included), at your account's price for that country and channel. Days are counted in Argentina time (UTC-3).

Statuses

Message delivery (message.status)

StatusMeaning
AcceptedAccepted, queued for sending.
In progressSent to the carrier, no delivery confirmation yet. Some destinations never confirm: the message may arrive anyway.
DeliveredThe carrier confirmed the delivery.
FailedIt could not be delivered through any available channel.

message.channel is the channel of the last send: after a fallback it changes (e.g. from whatsapp to sms).

OTP (otp.otp_status)

Not verified, Verified, Blocked or Expired. See Verify an OTP.

Channels and fallback

channel picks the main channel and fallback the alternative one. If the main channel does not deliver, TAuth resends the same PIN in the same transaction through the alternative channel and restarts the expiration. Nothing to do on your side: the user enters whichever PIN arrives.

The fallback kicks in when:

When every option is exhausted, message.status becomes Failed.

Each channel must be enabled for the number's country. If the requested channel is not and a fallback was sent, the OTP goes out directly through the alternative channel (the response shows it in message.channel). Every message sent, fallbacks included, is billed at its channel price.

Webhooks

If T-mob sets a webhook URL for your account, TAuth sends it a JSON POST for each event:

EventWhenExtra fields
message.deliveredThe carrier confirmed the delivery.channel
message.fallbackThe PIN was resent through another provider or channel (fallback or requested resend).reason, step
message.failedNo channels left to deliver it.reason
otp.verifiedThe PIN was verified.
otp.blockedNo verification attempts left.
billing.spend_alertThe month spend reached the alert percentage of the limit (once a month). No transaction.month, spent, limit, currency, percent
billing.spend_limit_reachedThe month spend reached the limit: from now on send-otp answers 402 (once a month).same
POST /your/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"}

Verifying the signature

X-TAuth-Signature is sha256= + the hex HMAC-SHA256 of {timestamp}.{body}, keyed with the webhook secret (issued by T-mob). Compute it over the raw body (before parsing) and reject timestamps older than 5 minutes.

# 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;
}

Delivery

Idempotency

If a send-otp times out or fails at the network level, you can't tell whether the OTP went out. To retry without sending two messages, use the Idempotency-Key header with a unique value per request (e.g. a UUID):

Errors

Errors are JSON with an error field:

{"error": "Invalid template."}
CodeMeaning
400Invalid request.
402The account reached its monthly spend limit.
403Missing or invalid API key, or feature not enabled.
404Unknown transaction or route.
405HTTP method not allowed for the route (empty body).
409State conflict (OTP expired or blocked, request in progress).
422Idempotency-Key reused with a different body.
429A limit was exceeded. Retry later.
5xxTAuth error. Retry with backoff (and with an Idempotency-Key on send-otp).

Limits

T-mob sets them per account. Defaults:

LimitDefault
Verification attempts per OTP5
OTPs per number5 per hour
OTPs per minute for the accountunlimited
Resends per OTP3, at least 30 seconds apart
PIN expirationper template (60 seconds by default), restarts on each resend
Monthly spendno limit, unless one is agreed (when reached, send-otp answers 402)

Full example

# Python 3, with requests
import uuid
import requests

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

# 1. Send
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. The user types the PIN they got
pin = input("PIN: ")

# 3. Verify
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("Number verified")
else:
    print("Not verified:", r.json())