Integrating Twilio Verify for OTPs in a Spring Boot Application
Learn how to send and verify one‑time passwords with Twilio Verify in a Spring Boot app. The guide covers configuration, code snippets, error handling, and cost‑monitoring steps.
19 Jul 2025, 19:31 UTC

Desired Outcome
By the end of this article you will be able to:
- Configure a Twilio Verify service in the Twilio console.
- Send a one‑time password (OTP) via SMS or Voice from a Spring Boot application.
- Validate the OTP entered by a user and handle common error scenarios.
- Check delivery status and cost in the Twilio console and monitor usage.
Prerequisites
- A Twilio account with a verified phone number and an active Verify service. Note: The service SID is required.
- Java 17+ and Maven or Gradle for building the Spring Boot project.
- Spring Boot 3.x (or 2.7.x) with the
spring-boot-starter-webandspring-boot-starter-validationdependencies. - Environment variables or a secrets manager to provide
TWILIO_ACCOUNT_SIDandTWILIO_AUTH_TOKENat runtime. - Network access to Twilio’s REST endpoints (https://verify.twilio.com).
Step 1: Create a Verify Service in the Console
1. Log in to the Twilio Console.
2. Navigate to Verify > Services and click Create new service.
3. Give the service a name (e.g., myapp-verify) and click Save.
4. Copy the generated Service SID for later use.
Step 2: Add Twilio Dependencies
For Maven:
<dependency>
<groupId>com.twilio.sdk</groupId>
<artifactId>twilio</artifactId>
<version>9.28.0</version>
</dependency>
For Gradle:
implementation("com.twilio.sdk:twilio:9.28.0")
Step 3: Configure Credentials in application.yml
twilio:
account-sid: ${TWILIO_ACCOUNT_SID}
auth-token: ${TWILIO_AUTH_TOKEN}
verify-service-sid: ${TWILIO_VERIFY_SERVICE_SID}
Use a secrets manager or Docker secrets to inject these variables at runtime. Never commit them to source control.
Step 4: Create a Verify Service Bean
Encapsulate Twilio API calls in a Spring @Service so that they can be reused and unit‑tested.
@Service
public class TwilioVerifyService {
private final VerifyService verifyService;
public TwilioVerifyService(@Value("${twilio.account-sid}") String accountSid,
@Value("${twilio.auth-token}") String authToken,
@Value("${twilio.verify-service-sid}") String serviceSid) {
Twilio.init(accountSid, authToken);
this.verifyService = VerifyService.creator(serviceSid).create();
}
public String sendOtp(String phoneNumber, String channel) {
Verification verification = Verification.creator(
verifyService.getSid(),
phoneNumber,
channel) // "sms" or "call"
.create();
return verification.getSid();
}
public Verification checkOtp(String verificationSid, String code) {
return VerificationCheck.creator(
verifyService.getSid(),
code)
.setVerificationSid(verificationSid)
.create();
}
}
Key points:
- The
Verification.creatorcall returns aVerificationobject containing thesidandstatus(pending, approved, denied). - Use
VerificationCheck.creatorto validate the code entered by the user. - Handle
ApiExceptionfor HTTP errors such as 400 (bad request) or 429 (rate limit).
Step 5: Expose REST Endpoints
Define a simple controller to trigger OTP sending and verification. Use DTOs for request validation.
@RestController
@RequestMapping("/auth")
public class AuthController {
private final TwilioVerifyService verifyService;
public AuthController(TwilioVerifyService verifyService) {
this.verifyService = verifyService;
}
@PostMapping("/send-otp")
public ResponseEntity<String> sendOtp(@RequestBody @Valid SendOtpRequest req) {
String sid = verifyService.sendOtp(req.getPhoneNumber(), req.getChannel());
return ResponseEntity.ok(sid);
}
@PostMapping("/check-otp")
public ResponseEntity<Boolean> checkOtp(@RequestBody @Valid CheckOtpRequest req) {
Verification check = verifyService.checkOtp(req.getVerificationSid(), req.getCode());
boolean approved = "approved".equalsIgnoreCase(check.getStatus());
return ResponseEntity.ok(approved);
}
}
@Data @NoArgsConstructor @AllArgsConstructor
class SendOtpRequest {
@NotBlank
private String phoneNumber;
@NotBlank
private String channel; // "sms" or "call"
}
@Data @NoArgsConstructor @AllArgsConstructor
class CheckOtpRequest {
@NotBlank
private String verificationSid;
@NotBlank
private String code;
}
Security note: The verificationSid should be stored by the client (e.g., in local storage) and sent back only over HTTPS. Do not expose the Twilio API directly to browsers.
Step 6: Handle Common Errors
Wrap API calls in try‑catch blocks and translate Twilio error codes to user‑friendly messages.
try {
Verification verification = verifyService.sendOtp(...);
} catch (ApiException e) {
switch (e.getStatusCode()) {
case 400: // Bad request
// e.g., invalid phone number
break;
case 429: // Rate limit
// Inform user to wait
break;
default:
// Generic error
}
}
Step 7: Verify Delivery and Cost in Twilio Console
- Navigate to Verify > Services and click your service.
- Under Verifications, locate the
verificationSidused in your request. - Check the Status column:
approvedindicates successful delivery and code entry. - Under SMS or Voice tabs, review the Cost column to see the per‑message charge for the destination country.
- Use the API Explorer or
curlto fetch a verification’s status programmatically:curl -X GET "https://verify.twilio.com/v2/Services/{ServiceSid}/Verifications/{VerificationSid}" \ -u {AccountSid}:{AuthToken}
Step 8: Unit Testing with a Mocked Twilio Client
Use Mockito or WireMock to simulate Twilio responses without incurring real costs.
@ExtendWith(MockitoExtension.class)
class TwilioVerifyServiceTest {
@Mock
private VerifyService mockService;
@InjectMocks
private TwilioVerifyService verifyService;
@Test
void sendOtp_success() {
Verification mockVerification = mock(Verification.class);
when(mockVerification.getSid()).thenReturn("VAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
when(mockService.create()).thenReturn(mockVerification);
String sid = verifyService.sendOtp("+15551234567", "sms");
assertEquals("VAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", sid);
}
}
Expected Checks and Recovery Options
- Check 1: HTTP 200 response – indicates the request reached Twilio. Log the
verificationSidfor later reference. - Check 2: Status in console – should be
pendingimmediately after send, thenapprovedordeniedafter user input. - Check 3: Cost per message – compare against your budget. If costs exceed expectations, consider using
callchannel or a different Twilio region. - Recovery: 429 Too Many Requests – implement exponential back‑off and notify the user to retry after a short pause.
- Recovery: 40004 Invalid Phone Number – validate the phone number format client‑side using
libphonenumberbefore sending. - Recovery: 20404 Resource Not Found – verify that the
serviceSidandverificationSidare correct and that the Verify service is active.
Practical Verification Checklist
| Checklist Item | How to Verify |
|---|---|
| OTP sent via SMS | Receive the message on the target device. |
| OTP validated correctly | Enter the code in the app and confirm the API returns approved. |
| Cost matches console | Check the console’s cost column for the message. |
| Rate limit handling | Trigger multiple sends in quick succession and observe 429 responses. |
Limitations
- Twilio Verify enforces a minimum OTP length (typically 6 digits). Adjust your client UI accordingly.
- OTP expiry is governed by Twilio’s default (usually 5 minutes). If your use case requires a different window, configure it in the Verify service settings.
- SMS delivery can be delayed by carrier networks; consider fallback to Voice calls for time‑critical flows.
- Costs are per message and vary by destination country. Monitor the Twilio usage dashboard to avoid unexpected charges.
Conclusion
By following this guide you can reliably integrate Twilio Verify into a Spring Boot application, handle common errors gracefully, and monitor delivery and cost in real time. The modular service design makes unit testing straightforward and keeps your credentials secure.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.