Setup Guide5-Minute Integration

Call API — Implementation Guide

Everything you need to integrate OTP voice calls into your application. From getting your API key to handling webhooks — complete with code examples in Node.js, Python, PHP, and Java.

This guide walks you through integrating the Tetrax Call API from start to finish. By the end, your application will be able to deliver OTP codes via automated voice calls to any phone number — including feature phones and landlines.

One API Call

A single POST request sends the OTP. The call is placed automatically.

Works on Any Phone

No app, no data connection, no smartphone required. Works on landlines too.

Digit-by-Digit Clarity

Each number is spoken individually — no confusion between "five" and "nine".

Real-Time Tracking

Get instant delivery updates via webhooks or poll the API for status.

Dashboard Analytics

Monitor call volumes, success rates, and delivery trends in real time.

Any Stack

Works with any language. REST API with examples for Node.js, Python, PHP, and Java.

Prerequisites

Before you start, make sure you have the following:

  • A Tetrax account — Sign up free at tetrax.in/signin
  • A Call API key — Created from the Applications page in your dashboard
  • A phone number to test with — Use your own mobile number with country code
  • A backend server — To securely make API calls and verify OTPs
  • cURL or any HTTP client to test the API
1

Get Your Call API Key

Your API key authenticates every request. Each application gets its own key prefixed with trx_call_.

Steps:

  1. Log in to your Tetrax Dashboard
  2. Go to Applications in the sidebar
  3. Click New Application
  4. Enter a name (e.g., "My Auth App")
  5. Select "Call API" as the service/product
  6. Choose Live or Test environment
  7. Click Create App
  8. Copy the trx_call_ API key shown in the success banner
⚠️
Save Your Key SecurelyThe API key is shown only once after creation. Store it in your backend environment variables — never in client-side code or version control.
# .env file  keep this secure!
TETRAX_CALL_API_KEY=trx_call_xxxxxxxxxxxx
2

Send Your First OTP Voice Call

Let's send a test OTP using cURL. Replace the API key and phone number with your own values.

curl -X POST https://APIURL.tetrax.in/v1/call/otp \
  -H "Content-Type: application/json" \
  -H "x-api-key: trx_call_your_key" \
  -d '{
    "phone": "+919876543210",
    "otp": "4324"
  }'

Expected response:

{
  "success": true,
  "callId": "call_1712345678",
  "message": "OTP voice call initiated successfully",
  "simulated": false,
  "call": {
    "id": 1,
    "phone": "+919876543210",
    "otp": "4324",
    "status": "initiated",
    "called_at": "2026-07-23T12:00:00.000Z"
  }
}
What happens next?5–15 seconds after the API responds, the recipient's phone will ring. When they answer, they will hear: "Your OTP is four, three, two, four." followed by a beep. The call ends automatically after delivery.

Request Parameters

ParameterTypeRequiredDescription
phonestringYesRecipient phone number with country code. E.g., +919876543210 for India. Supports international numbers.
otpstringYesThe OTP code to deliver. Must be a 3–10 digit numeric string. Each digit is spoken individually.

Response Fields

FieldTypeDescription
successbooleanWhether the OTP call was successfully initiated.
callIdstringUnique identifier for this OTP call. Use this to track status via webhooks or API.
messagestringHuman-readable status message.
simulatedbooleanTrue if the call is simulated (test mode without actual provider).
call.idintegerDatabase ID of the call record.
call.phonestringThe phone number the call was sent to.
call.otpstringThe OTP that was sent (for your records).
call.statusstringInitial status: "initiated".
call.called_atstring (ISO 8601)Timestamp when the call was initiated.
3

Handle OTP Verification in Your Application

The OTP verification flow is handled entirely on your end. Here's how the full authentication flow works:

OTP Authentication Flow

1
User enters phone number

User submits their phone number into your login/signup form.

2
Your backend generates OTP

Generate a random numeric OTP (e.g., 4324). Store it in your database with the phone number and an expiry time (recommended: 5 minutes).

3
Your backend calls Tetrax API

Send a POST request to /v1/call/otp with the phone and OTP. The call is placed automatically.

4
User receives call + hears OTP

The user's phone rings. When they answer, they hear each digit spoken individually.

5
User enters OTP in your app

The user types the OTP they heard into your verification form.

6
Your backend verifies OTP

Compare the user-entered OTP against the stored value. Check expiry. If valid, grant access.

Example OTP Generation & Verification

// 1. Generate a random 4-digit OTP
const otp = String(Math.floor(1000 + Math.random() * 9000));
const expiresAt = Date.now() + 5 * 60 * 1000; // 5 minutes

// 2. Store in database (example with MongoDB)
await OtpStore.create({
  phone: "+919876543210",
  otp: otp,
  expiresAt: new Date(expiresAt),
  used: false
});

// 3. Send OTP via Tetrax Call API
const response = await fetch(
  "https://APIURL.tetrax.in/v1/call/otp",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": process.env.TETRAX_CALL_API_KEY,
    },
    body: JSON.stringify({ phone: "+919876543210", otp }),
  }
);

const result = await response.json();
console.log("Call initiated:", result.callId);
// User submits the OTP they heard on the call
app.post("/verify-otp", async (req, res) => {
  const { phone, otp: userOtp } = req.body;

  // 1. Look up stored OTP
  const stored = await OtpStore.findOne({ phone, used: false });

  if (!stored) {
    return res.status(400).json({ error: "No OTP requested for this phone" });
  }

  // 2. Check expiry
  if (Date.now() > stored.expiresAt.getTime()) {
    return res.status(400).json({ error: "OTP has expired. Request a new one." });
  }

  // 3. Verify OTP
  if (userOtp !== stored.otp) {
    return res.status(400).json({ error: "Invalid OTP" });
  }

  // 4. Mark as used (prevent replay attacks)
  await OtpStore.updateOne({ _id: stored._id }, { used: true });

  // 5. Grant access — return JWT or session token
  const token = generateJwt({ phone });
  return res.json({ success: true, token });
});
4

Track Call Delivery with Webhooks

Webhooks notify your backend in real time when the status of an OTP call changes. This lets you track delivery without polling the API.

Setting Up Your Webhook Endpoint

Create an endpoint on your server that receives POST requests. Tetrax will send status updates to this URL whenever the call status changes.

// Your webhook endpoint — receives updates from Tetrax
app.post("/webhooks/tetrax-voice-status", async (req, res) => {
  const { call_id, status, duration } = req.body;

  // Log the status update
  console.log(`Call ${call_id} status: ${status}`);

  // Update your database
  await OtpCallLog.updateOne(
    { callId: call_id },
    { status, duration, updatedAt: new Date() }
  );

  // If the call failed, you could retry with SMS fallback
  if (status === "failed" || status === "no-answer") {
    await sendSmsOtpFallback(call_id);
  }

  // Always respond 200 to acknowledge receipt
  res.json({ received: true });
});

Webhook Events

EventDescriptionAction to Take
initiatedCall has been queuedLog the event. No action needed.
ringingPhone is ringingLog the event.
answeredUser answered the callLog the event. OTP is being delivered.
completedCall ended successfullyMark as delivered. OTP was read to user.
failedProvider errorConsider SMS fallback or retry.
busyLine was busyConsider retrying after a delay.
no-answerUser did not answerConsider SMS fallback or retry.

Webhook Payload

{
  "call_id": "call_1712345678",
  "status": "completed",
  "duration": 45
}

Webhook Parameters

ParameterTypeDescription
call_idstringThe unique call ID returned when you initiated the OTP call.
statusstringCurrent status: initiated, ringing, answered, completed, failed, busy, or no-answer.
durationintegerCall duration in seconds. Only present for answered/completed calls.
ℹ️
Webhook ConfigurationConfigure your webhook URL in the Call API settings page of your Tetrax dashboard. You can also test webhooks using the playground tool.
5

Full Integration Examples by Language

Complete, production-ready code snippets for integrating the Call API into your backend.

Node.js (Express)

const express = require("express");
const crypto = require("crypto");
const router = express.Router();

// POST /api/send-otp — Send OTP via voice call
router.post("/send-otp", async (req, res) => {
  try {
    const { phone } = req.body;

    // Validate phone number
    if (!phone || phone.replace(/[^0-9+]/g, "").length < 10) {
      return res.status(400).json({ error: "Valid phone number required" });
    }

    // Generate OTP
    const otp = crypto.randomInt(100000, 999999).toString();
    const expiresAt = Date.now() + 5 * 60 * 1000;

    // Store in database
    await db.otps.create({ phone, otp, expiresAt });

    // Call Tetrax Call API
    const response = await fetch(
      "https://APIURL.tetrax.in/v1/call/otp",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "x-api-key": process.env.TETRAX_CALL_API_KEY,
        },
        body: JSON.stringify({ phone, otp }),
      }
    );

    const result = await response.json();

    if (!result.success) {
      return res.status(502).json({ error: "Failed to initiate call" });
    }

    res.json({
      message: "OTP voice call initiated",
      callId: result.callId,
      // Never return the actual OTP in production!
    });
  } catch (err) {
    console.error("Send OTP error:", err);
    res.status(500).json({ error: "Internal server error" });
  }
});

// POST /api/verify-otp — Verify the OTP user entered
router.post("/verify-otp", async (req, res) => {
  try {
    const { phone, otp } = req.body;

    const record = await db.otps.findOne({
      where: { phone, used: false },
      order: [["createdAt", "DESC"]],
    });

    if (!record) {
      return res.status(400).json({ error: "No OTP requested for this phone" });
    }

    if (Date.now() > record.expiresAt) {
      return res.status(400).json({ error: "OTP expired. Request a new one." });
    }

    if (record.otp !== otp) {
      return res.status(400).json({ error: "Invalid OTP" });
    }

    // Mark as used
    await record.update({ used: true });

    // Generate session token
    const token = jwt.sign({ phone, verified: true }, process.env.JWT_SECRET, {
      expiresIn: "7d",
    });

    res.json({ success: true, token });
  } catch (err) {
    console.error("Verify OTP error:", err);
    res.status(500).json({ error: "Internal server error" });
  }
});

module.exports = router;

Python (FastAPI)

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import httpx
import secrets
import os

app = FastAPI()

TETRAX_API_KEY = os.getenv("TETRAX_CALL_API_KEY")
TETRAX_API_URL = "https://APIURL.tetrax.in/v1/call/otp"

class SendOtpRequest(BaseModel):
    phone: str

class VerifyOtpRequest(BaseModel):
    phone: str
    otp: str

@app.post("/api/send-otp")
async def send_otp(req: SendOtpRequest):
    # Generate OTP
    otp = str(secrets.randbelow(900000) + 100000)
    
    # Store in database (pseudo-code)
    # await db.store_otp(req.phone, otp, expire_in=300)
    
    # Call Tetrax API
    async with httpx.AsyncClient() as client:
        response = await client.post(
            TETRAX_API_URL,
            headers={
                "Content-Type": "application/json",
                "x-api-key": TETRAX_API_KEY,
            },
            json={"phone": req.phone, "otp": otp},
        )
        result = response.json()
    
    if not result.get("success"):
        raise HTTPException(status_code=502, detail="Voice call failed")
    
    return {"message": "OTP call initiated", "callId": result["callId"]}

@app.post("/api/verify-otp")
async def verify_otp(req: VerifyOtpRequest):
    # Look up stored OTP (pseudo-code)
    # stored = await db.get_otp(req.phone)
    
    # if not stored:
    #     raise HTTPException(400, "No OTP requested")
    # if stored.expires_at < datetime.utcnow():
    #     raise HTTPException(400, "OTP expired")
    # if stored.otp != req.otp:
    #     raise HTTPException(400, "Invalid OTP")
    
    # Mark as used and return success
    return {"success": True, "token": "user_session_token_here"}

PHP (Laravel)

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

class OtpController extends Controller
{
    public function sendOtp(Request $request)
    {
        $request->validate(['phone' => 'required|string']);

        $otp = strval(random_int(100000, 999999));

        // Store in database
        // Otp::create([
        //     'phone' => $request->phone,
        //     'otp' => $otp,
        //     'expires_at' => now()->addMinutes(5),
        // ]);

        $response = Http::withHeaders([
            'x-api-key' => env('TETRAX_CALL_API_KEY'),
        ])->post('https://APIURL.tetrax.in/v1/call/otp', [
            'phone' => $request->phone,
            'otp' => $otp,
        ]);

        if (!$response->successful()) {
            return response()->json(['error' => 'Failed to initiate call'], 502);
        }

        return response()->json([
            'message' => 'OTP voice call initiated',
            'callId' => $response->json('callId'),
        ]);
    }

    public function verifyOtp(Request $request)
    {
        $request->validate([
            'phone' => 'required|string',
            'otp' => 'required|string',
        ]);

        // Look up stored OTP
        // $stored = Otp::where('phone', $request->phone)
        //     ->where('used', false)
        //     ->latest()
        //     ->first();

        // if (!$stored || $stored->otp !== $request->otp) {
        //     return response()->json(['error' => 'Invalid OTP'], 400);
        // }

        // if ($stored->expires_at->isPast()) {
        //     return response()->json(['error' => 'OTP expired'], 400);
        // }

        // $stored->update(['used' => true]);

        return response()->json([
            'success' => true,
            'token' => Str::random(40),
        ]);
    }
}

Java (Spring Boot)

import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.*;
import java.util.*;

@RestController
@RequestMapping("/api")
public class OtpController {

    private final RestTemplate restTemplate = new RestTemplate();

    @PostMapping("/send-otp")
    public Map<String, Object> sendOtp(@RequestBody Map<String, String> body) {
        String phone = body.get("phone");
        String otp = String.valueOf(100000 + new Random().nextInt(900000));

        // Store OTP in database
        // otpRepository.save(new Otp(phone, otp, new Date()));

        // Call Tetrax API
        HttpHeaders headers = new HttpHeaders();
        headers.set("x-api-key", System.getenv("TETRAX_CALL_API_KEY"));
        headers.setContentType(MediaType.APPLICATION_JSON);

        Map<String, String> payload = new HashMap<>();
        payload.put("phone", phone);
        payload.put("otp", otp);

        HttpEntity<Map<String, String>> request = new HttpEntity<>(payload, headers);

        ResponseEntity<Map> response = restTemplate.postForEntity(
            "https://APIURL.tetrax.in/v1/call/otp",
            request,
            Map.class
        );

        Map<String, Object> result = new HashMap<>();
        result.put("message", "OTP voice call initiated");
        result.put("callId", response.getBody().get("callId"));
        return result;
    }

    @PostMapping("/verify-otp")
    public Map<String, Object> verifyOtp(@RequestBody Map<String, String> body) {
        String phone = body.get("phone");
        String otp = body.get("otp");

        // Verify against stored OTP
        // Otp stored = otpRepository.findLatestByPhone(phone);
        // if (stored == null || !stored.getOtp().equals(otp)) {
        //     throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid OTP");
        // }
        // if (stored.getExpiresAt().before(new Date())) {
        //     throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "OTP expired");
        // }

        Map<String, Object> result = new HashMap<>();
        result.put("success", true);
        result.put("token", UUID.randomUUID().toString());
        return result;
    }
}

Best Practices

🔐 Store API Keys Securely

Never hard-code API keys or commit them to version control. Use environment variables or a secrets manager. Rotate keys periodically from the dashboard.

⏱ Set OTP Expiry

Always set an expiry time (recommended: 5 minutes) on OTPs stored in your database. This prevents replay attacks and reduces security risk.

🔄 Implement SMS Fallback

Voice calls may not always be answered. Use webhooks to detect failed/no-answer calls and fall back to SMS OTP delivery automatically.

📊 Rate Limit Your Users

Implement your own rate limiting per user (e.g., max 3 OTP requests per minute) to prevent abuse. The Tetrax API has platform-level rate limits per account tier, but app-level limits add an extra layer of protection.

📝 Log Everything

Log all OTP requests, deliveries, and verification attempts for auditing and troubleshooting. The Tetrax dashboard provides API-side logs, but your own logs give you full control.

📞 Use International Format

Always use phone numbers in international format with the + prefix and country code (e.g., +919876543210 for India, +14155552671 for US). This ensures correct routing for all countries.

Troubleshooting

OTP call not reaching the phone

  • Verify the phone number includes the correct country code (e.g., +91 for India)
  • Check that the phone is not in aeroplane mode or Do Not Disturb mode
  • Ensure the number is not on a Do Not Call (DNC/DND) registry
  • Check the call status in the dashboard's Call History page
  • Try sending the OTP to a different number to rule out number-specific issues

Getting 401 Unauthorized

  • Make sure you're using the x-api-key header (not Authorization: Bearer)
  • Verify the API key starts with trx_call_
  • Check that the key hasn't expired or been revoked — regenerate from the dashboard if needed
  • Ensure the key doesn't have extra whitespace or newline characters

Getting 400 Bad Request

  • Ensure phone is a string with country code (e.g., +919876543210)
  • Ensure otp is a numeric string between 3 and 10 digits
  • Check that your JSON is properly formatted (no trailing commas)
  • Verify the Content-Type: application/json header is set

Getting 429 Too Many Requests

  • Your account tier has a per-minute rate limit. Check your tier's limits in the dashboard.
  • Wait for the Retry-After seconds before sending another request.
  • Contact support to raise your rate-limit tier.
  • Implement your own rate limiting per user to avoid hitting platform limits.

Call is marked as "simulated"

Simulated calls occur in test mode or during initial provisioning. If you see"simulated": true in the response, the call was logged but not actually placed. Contact support to activate live calling for your account.

ℹ️
Need More Help?Can't find what you're looking for? Contact our support team and we'll help you get up and running.

Was this guide helpful?

Help us improve our documentation.