Verify users with OTP
Use a one-time password when you need to confirm that a user controls an international phone number. Inttegro sends the token by SMS and records each verification attempt; your application must inspect the attempt verdict before granting access.
The MCP tool catalog does not currently expose OTP initiation or verification. Keep OTP submission and verdict checks in your trusted backend; use MCP for adjacent customer or order investigation only when that context is relevant.
Prerequisites
- The user's phone number in international format
- A server-side place to associate the typed OTP transaction ID with the pending action
Initiate the SMS verification
Call Initiate OTP transaction with recipient, service_name, and token_size. The current API requires a token size from 5 through 10. The service name must be 2-20 characters; an optional sender must be 3-12 characters. Validity defaults to 10 minutes and can be set from 3 through 10,080 minutes.
Persist the returned ID in the form ot_<TRANSACTION_ID>. A transaction can be pending_delivery before transmission details are available and pending_verification after the SMS is sent.
Verify the submitted token
Submit the transaction ID, the same international phone number, and the token entered by the user. An HTTP 200 response means the attempt was recorded, not that the token matched. Continue only when verification_attempt.result.verdict is pass.
- cURL
- TypeScript
- Go
- Python
- PHP
- Ruby
- Java
- C#
RECIPIENT='<USER_PHONE_E164>'
init=$(curl https://api.inttegro.com/otp/initiate \
-H "Authorization: Bearer $INTTEGRO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: verify-user-123" \
-d "{
\"recipient\": \"$RECIPIENT\",
\"sender\": \"MyApp\",
\"service_name\": \"MyApp\",
\"token_alphabet_type\": \"numeric\",
\"token_size\": 6
}")
transaction_id=$(jq -r '.transaction.id' <<< "$init")
read -r -s OTP_CODE
verify=$(jq -n \
--arg recipient "$RECIPIENT" \
--arg token "$OTP_CODE" \
--arg transaction_id "$transaction_id" \
'{recipient: $recipient, token: $token, transaction_id: $transaction_id}' | \
curl https://api.inttegro.com/otp/verify \
-H "Authorization: Bearer $INTTEGRO_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @-)
jq -e '.verification_attempt.result.verdict == "pass"' <<< "$verify"
import * as Inttegro from '@inttegro/inttegro-sdk'
const inttegro = new Inttegro.InttegroClient({
apiKey: process.env.INTTEGRO_API_KEY!,
})
const recipient = '<USER_PHONE_E164>'
const otpCodeFromUser = '<OTP_ENTERED_BY_USER>'
const initiated = await inttegro.otp.initiate({
requestMeta: { idempotencyKey: "verify-user-123" },
recipient,
sender: "MyApp",
serviceName: "MyApp",
tokenAlphabetType: "numeric",
tokenSize: 6,
})
if (!initiated.id) {
throw new Error('The OTP transaction did not include an ID')
}
const verified = await inttegro.otp.verify({
recipient,
token: otpCodeFromUser,
transactionId: initiated.id,
})
if (verified.verificationAttempt?.result?.verdict !== 'pass') {
throw new Error('The supplied OTP did not match')
}
package main
import (
"context"
"log"
"os"
inttegro "github.com/zebodotdev/inttegro-sdk-go/v4"
)
func main() {
ctx := context.Background()
client := inttegro.NewClient(os.Getenv("INTTEGRO_API_KEY"))
recipient := "<USER_PHONE_E164>"
otpCodeFromUser := "<OTP_ENTERED_BY_USER>"
initiated, err := client.Otp.Initiate(ctx, map[string]any{
"request_meta": map[string]any{"idempotency_key": "verify-user-123"},
"recipient": recipient,
"sender": "MyApp",
"service_name": "MyApp",
"token_alphabet_type": "numeric",
"token_size": 6,
})
if err != nil {
log.Fatal(err)
}
transaction := initiated["transaction"].(map[string]any)
verified, err := client.Otp.Verify(ctx, map[string]any{
"recipient": recipient,
"token": otpCodeFromUser,
"transaction_id": transaction["id"],
})
if err != nil {
log.Fatal(err)
}
attempt := verified["verification_attempt"].(map[string]any)
result := attempt["result"].(map[string]any)
if result["verdict"] != "pass" {
log.Fatal("the supplied OTP did not match")
}
}
import os
import inttegro
client = inttegro.InttegroClient(api_key=os.environ["INTTEGRO_API_KEY"])
recipient = "<USER_PHONE_E164>"
otp_code_from_user = "<OTP_ENTERED_BY_USER>"
initiated = client.otp.initiate(
inttegro.otp.InitiateRequest(
recipient=recipient,
sender="MyApp",
service_name="MyApp",
token_alphabet_type="numeric",
token_size=6,
)
)
verified = client.otp.verify(
inttegro.otp.VerifyRequest(
recipient=recipient,
token=otp_code_from_user,
transaction_id=initiated.id,
)
)
if verified.verification_attempt.result.verdict != "pass":
raise RuntimeError("the supplied OTP did not match")
<?php
use Inttegro\Client;
$client = new Client($_ENV['INTTEGRO_API_KEY']);
$recipient = '<USER_PHONE_E164>';
$otpCodeFromUser = '<OTP_ENTERED_BY_USER>';
$initiated = $client->otp->initiate([
'request_meta' => ['idempotency_key' => 'verify-user-123'],
'recipient' => $recipient,
'sender' => 'MyApp',
'service_name' => 'MyApp',
'token_alphabet_type' => 'numeric',
'token_size' => 6,
]);
$verified = $client->otp->verify([
'recipient' => $recipient,
'token' => $otpCodeFromUser,
'transaction_id' => $initiated->id,
]);
if ($verified->verificationAttempt->result->verdict !== 'pass') {
throw new RuntimeException('The supplied OTP did not match');
}
require "inttegro"
client = Inttegro::Client.new(api_key: ENV.fetch("INTTEGRO_API_KEY"))
recipient = "<USER_PHONE_E164>"
otp_code_from_user = "<OTP_ENTERED_BY_USER>"
initiated = client.otp.initiate(
request_meta: { idempotency_key: "verify-user-123" },
recipient: recipient,
sender: "MyApp",
service_name: "MyApp",
token_alphabet_type: "numeric",
token_size: 6,
)
verified = client.otp.verify(
recipient: recipient,
token: otp_code_from_user,
transaction_id: initiated.id,
)
raise "the supplied OTP did not match" unless verified.verification_attempt.result.verdict == "pass"
import com.inttegro.Client;
import com.inttegro.RequestMeta;
import com.inttegro.otp.InitiateOtpParams;
import com.inttegro.otp.OtpAlphabetType;
import com.inttegro.otp.OtpVerificationVerdict;
import com.inttegro.otp.VerifyOtpParams;
public class Example {
public static void main(String[] args) throws Exception {
var client = new Client(System.getenv("INTTEGRO_API_KEY"));
var recipient = "<USER_PHONE_E164>";
var otpCodeFromUser = "<OTP_ENTERED_BY_USER>";
var initiated = client.otp().initiate(
InitiateOtpParams.builder()
.requestMeta(RequestMeta.withIdempotencyKey("verify-user-123"))
.recipient(recipient)
.sender("MyApp")
.serviceName("MyApp")
.tokenAlphabetType(OtpAlphabetType.NUMERIC)
.tokenSize(6)
.build()
);
var verified = client.otp().verify(
VerifyOtpParams.builder()
.recipient(recipient)
.token(otpCodeFromUser)
.transactionId(initiated.id)
.build()
);
if (verified.verificationAttempt.result.verdict != OtpVerificationVerdict.PASS) {
throw new IllegalStateException("The supplied OTP did not match");
}
}
}
using Inttegro;
using var inttegro = new InttegroClient(
Environment.GetEnvironmentVariable("INTTEGRO_API_KEY")!
);
var recipient = "<USER_PHONE_E164>";
var otpCodeFromUser = "<OTP_ENTERED_BY_USER>";
var initiated = await inttegro.Otp.InitiateAsync(new {
request_meta = new { idempotency_key = "verify-user-123" },
recipient,
sender = "MyApp",
service_name = "MyApp",
token_alphabet_type = "numeric",
token_size = 6,
});
var verified = await inttegro.Otp.VerifyAsync(new {
recipient,
token = otpCodeFromUser,
transaction_id = initiated.Id,
});
if (verified.VerificationAttempt?.Result?["verdict"]?.ToString() != "pass")
{
throw new InvalidOperationException("The supplied OTP did not match");
}
Do not log the submitted token or verification response body. If the verdict is fail, keep the protected action pending and let the user retry only within your attempt policy. Use Lookup OTP transaction to refresh the transaction state, or Cancel OTP transaction when the flow is abandoned.
Related resources
- Initiate OTP transaction - Complete initiation contract
- Verify OTP - Verification request and attempt result
- Lookup OTP transaction - Current transaction status
- Cancel OTP transaction - Invalidate an active transaction