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/
- Requests and responses are JSON (
Content-Type: application/json), UTF-8. - Dates are ISO 8601 in UTC, e.g.
2026-10-28T14:03:21.512000Z. - HTTPS only.
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.
Flow
- Send the OTP to the user's number with
POST /v1/send-otp/. The response has atransaction: store it with the user. - The user gets the PIN by WhatsApp or SMS and types it in your app.
- Verify the PIN with
POST /v1/verify-otp/{transaction}/. Ifotp_statusisVerified, the number is validated. - Optional: show the delivery status with
GET /v1/otp/{transaction}/, offer a "resend" button withPOST /v1/otp/{transaction}/resend/, or receive the events through a webhook.
Send an OTP
Body
| Field | Type | Description | |
|---|---|---|---|
msisdn | string | required | 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. |
channel | string | optional | whatsapp or sms. Defaults to the main channel configured for your account
that is available for the number's country. |
fallback | string | optional | 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. |
template | string | optional | Message template configured for your account (text, PIN length, expiration). Defaults to the default template. |
country | string | optional | 2-letter ISO code (AR). If sent, it must match the country of the
msisdn (extra check). |
Optional headers
| Header | Description |
|---|---|
Idempotency-Key | Unique 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
| Code | error | Cause |
|---|---|---|
| 400 | Invalid JSON. | The body is not a JSON object. |
| 400 | Invalid parameters. | msisdn is missing or malformed. |
| 400 | Invalid channel. | channel or fallback is neither sms nor whatsapp. |
| 400 | Country not enabled. | The number's country is not enabled for your account (or the number has no country code). |
| 400 | Country not available for channel. | The requested channel is not available for that country and no available fallback was sent. |
| 400 | Invalid template. | The template does not exist or is inactive. |
| 400 | Invalid msisdn. | The number does not match its country format, or does not match country. |
| 400 | Invalid Idempotency-Key. | The key is empty, too long or has non-printable characters. |
| 402 | Monthly spend limit reached. | Your account reached its monthly spend limit. |
| 409 | Request in progress. | Another request with the same Idempotency-Key is being processed. |
| 422 | Idempotency-Key already used with another body. | The key was already used with different data. |
| 429 | MSISDN rate limit exceeded. | Too many OTPs to that number. See Limits. |
| 429 | Client rate limit exceeded. | Too many OTPs per minute for your account. |
Verify an OTP
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_status | Meaning |
|---|---|
| Verified | Right PIN. The OTP is consumed: a second attempt gets a 404. |
| Not verified | Wrong PIN. The user can try again while there are remaining_attempts. |
| Blocked | No attempts left. The OTP can no longer be verified: request a new one. |
| Expired | Past expires_at. Request a new one (or resend it before it expires). |
Errors
| Code | error | Cause |
|---|---|---|
| 400 | Invalid JSON. | The body is not a JSON object. |
| 400 | Invalid parameters. | otp is missing or not a string. |
| 404 | Invalid transaction. | The transaction does not exist, belongs to another account, was already verified or expired more than 24 hours ago. |
Get the status
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.
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.
| Code | error | Cause |
|---|---|---|
| 403 | Resend not enabled. | Resends are not enabled for your account. |
| 404 | Invalid transaction. | Unknown, of another account or already verified. |
| 409 | OTP expired. / OTP blocked. | The OTP expired or has no attempts left: request a new one. |
| 429 | Resend too soon. | The minimum time since the last send has not passed. |
| 429 | Resend limit exceeded. | The maximum number of resends was reached. |
Country format
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
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)
| Status | Meaning |
|---|---|
| Accepted | Accepted, queued for sending. |
| In progress | Sent to the carrier, no delivery confirmation yet. Some destinations never confirm: the message may arrive anyway. |
| Delivered | The carrier confirmed the delivery. |
| Failed | It 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:
- the send fails,
- the carrier reports the message could not be delivered, or
- the time configured for your account passes without the PIN being verified and without a delivery confirmation.
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:
| Event | When | Extra fields |
|---|---|---|
message.delivered | The carrier confirmed the delivery. | channel |
message.fallback | The PIN was resent through another provider or channel (fallback or requested resend). | reason, step |
message.failed | No channels left to deliver it. | reason |
otp.verified | The PIN was verified. | |
otp.blocked | No verification attempts left. | |
billing.spend_alert | The month spend reached the alert percentage of the limit (once a month). No transaction. | month, spent, limit, currency, percent |
billing.spend_limit_reached | The 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
- Answer with a
2xxwithin 5 seconds. If processing takes longer, queue it. - On an error or timeout TAuth retries up to 6 times with increasing waits (10 s, 20 s, 40 s…, about 10 minutes overall).
- An event may arrive more than once or out of order: handle it idempotently (e.g. by
transaction+event).
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):
- Same key and same body within 24 hours: the original response is returned, with the header
Idempotent-Replayed: true. - Same key with a different body:
422. - If the first request is still being processed:
409. Retry in a few seconds. - Error responses are not stored: you can fix the request and retry with the same key.
Errors
Errors are JSON with an error field:
{"error": "Invalid template."}
| Code | Meaning |
|---|---|
| 400 | Invalid request. |
| 402 | The account reached its monthly spend limit. |
| 403 | Missing or invalid API key, or feature not enabled. |
| 404 | Unknown transaction or route. |
| 405 | HTTP method not allowed for the route (empty body). |
| 409 | State conflict (OTP expired or blocked, request in progress). |
| 422 | Idempotency-Key reused with a different body. |
| 429 | A limit was exceeded. Retry later. |
| 5xx | TAuth error. Retry with backoff (and with an Idempotency-Key on send-otp). |
Limits
T-mob sets them per account. Defaults:
| Limit | Default |
|---|---|
| Verification attempts per OTP | 5 |
| OTPs per number | 5 per hour |
| OTPs per minute for the account | unlimited |
| Resends per OTP | 3, at least 30 seconds apart |
| PIN expiration | per template (60 seconds by default), restarts on each resend |
| Monthly spend | no 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())