
Migrating From Twilio to Prelude: A Technical Guide
A step-by-step technical guide for replacing Twilio Messaging & Twilio Verify with Prelude’s Verify API

Quentin Le Bras
CPO
Replacing your OTP provider doesn't have to be a multi-sprint project. Prelude's unified Verify API maps directly onto Twilio Verify's concepts. Preludes provides a single REST endpoint that manages the entire OTP lifecycle, from fraud detection to multi-channel routing. Most teams ship a working replacement in a single sprint.
|
What Changes
At a high level, you're replacing two Twilio surfaces: Programmable Messaging and Twilio Verify with a single Prelude API. The table below maps every Twilio concept to its Prelude equivalent.
Concept | Twilio Verify | Prelude Verify |
|---|---|---|
Service category | Twilio Programmable Messaging + Twilio Verify | Prelude Verify API (single service) |
API base URL | api.twilio.com/2010-04-01/Accounts/{SID} | api.prelude.dev |
Authentication | HTTP Basic (AccountSID:AuthToken) | Bearer token in Authorization header |
Send OTP | POST /Services/{ServiceSID}/Verifications | POST /v2/verification |
Check OTP | POST /Services/{ServiceSID}/VerificationChecks | POST /v2/verification/check |
Retry OTP | POST /Services/{ServiceSID}/Verifications (repeat) | POST /v2/verification (same number, within window) |
Verification status | pending / approved / canceled | success / retry / blocked / challenged |
Service / namespace | Verify Service (ServiceSID) | Implicit per API key (configured in Dashboard) |
Fraud prevention | Twilio Fraud Guard (add-on) | Built-in ML, enabled by default |
Channels | SMS, WhatsApp, call, email | SMS, WhatsApp, Viber, Zalo, RCS, voice, email |
Sender / brand | Messaging Service or phone number | Configured by contacting with customer success (Sender ID) |
Test numbers | Magic numbers per Verify Service | Test numbers in Dashboard (Verify > Configure > Numbers) |
Webhooks | StatusCallback URL per request | callback_url per request; signature via RSASSA-PSS |
SDKs | twilio-node, twilio-python, etc. | npm @prelude.so/sdk, pip prelude_python_sdk, go-sdk, Java, Ruby |
Authentication
Twilio (before)
Twilio uses HTTP Basic Authentication with your Account SID as the username and Auth Token as the password.
# Twilio – HTTP Basic Auth curl -X POST https://verify.twilio.com/v2/Services/{ServiceSID}/Verifications \ -u $TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN \ -d 'To=+14155552671' \ -d 'Channel=sms'
# Twilio – HTTP Basic Auth curl -X POST https://verify.twilio.com/v2/Services/{ServiceSID}/Verifications \ -u $TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN \ -d 'To=+14155552671' \ -d 'Channel=sms'
Prelude (after)
Prelude uses a Bearer token. Generate an API key in the Dashboard under Settings > API Keys.
# Prelude – Bearer Token curl -X POST https://api.prelude.dev/v2/verification \ -H “Authorization: Bearer $PRELUDE_API_KEY” \ -H 'Content-Type: application/json' \ -d '{"target": {"type": "phone_number", "value": "+14155552671"}}'
# Prelude – Bearer Token curl -X POST https://api.prelude.dev/v2/verification \ -H “Authorization: Bearer $PRELUDE_API_KEY” \ -H 'Content-Type: application/json' \ -d '{"target": {"type": "phone_number", "value": "+14155552671"}}'
Action required: Store your Prelude API key in an environment variable (e.g. PRELUDE_API_KEY). Never commit it to source control. Remove |
Sending a Verification (OTP)
Twilio Verify – before
// Node.js – Twilio Verify const client = require('twilio')(accountSid, authToken); const verification = await client.verify.v2 .services(verifySid) .verifications .create({ to: '+14155552671', channel: 'sms' }); console.log(verification.status); // 'pending'
// Node.js – Twilio Verify const client = require('twilio')(accountSid, authToken); const verification = await client.verify.v2 .services(verifySid) .verifications .create({ to: '+14155552671', channel: 'sms' }); console.log(verification.status); // 'pending'
Prelude – after
// Node.js – Prelude SDK import Prelude from '@prelude.so/sdk'; const client = new Prelude({ apiToken: process.env.PRELUDE_API_KEY }); const verification = await client.verification.create({ target: { type: 'phone_number', value: '+14155552671' }, // Optional: forward digital signals to better tackle fraud signals: { ip: '203.0.113.42', // end-user public IP device_id: 'abc123-unique', // stable device identifier device_platform: 'ios', device_model: 'iPhone 15', }, }); console.log(verification.id); // 'vrf_01...' console.log(verification.status); // 'success' | 'blocked' | 'challenged'
// Node.js – Prelude SDK import Prelude from '@prelude.so/sdk'; const client = new Prelude({ apiToken: process.env.PRELUDE_API_KEY }); const verification = await client.verification.create({ target: { type: 'phone_number', value: '+14155552671' }, // Optional: forward digital signals to better tackle fraud signals: { ip: '203.0.113.42', // end-user public IP device_id: 'abc123-unique', // stable device identifier device_platform: 'ios', device_model: 'iPhone 15', }, }); console.log(verification.id); // 'vrf_01...' console.log(verification.status); // 'success' | 'blocked' | 'challenged'
Status values
Prelude returns a richer set of statuses when creating a verification:
Status | Meaning |
|---|---|
| New verification window opened; code sent to user. |
| Same phone number called again within the window; new attempt sent. |
| Request flagged as fraudulent; no code sent, no charge of message fee. |
| Suspicious traffic; delivery restricted to non-SMS channels only (must be enabled by Prelude support). |
Checking a Verification Code
Twilio Verify – before
// Node.js – Twilio Verify check const check = await client.verify.v2 .services(verifySid) .verificationChecks .create({ to: '+14155552671', code: '123456' }); if (check.status === 'approved') { // grant access }
// Node.js – Twilio Verify check const check = await client.verify.v2 .services(verifySid) .verificationChecks .create({ to: '+14155552671', code: '123456' }); if (check.status === 'approved') { // grant access }
Prelude – after
// Node.js – Prelude check const check = await client.verification.check({ target: { type: 'phone_number', value: '+14155552671' }, code: '123456', }); if (check.status === 'success') { // grant access } // Possible statuses: 'success' | 'failure' | ‘expired_or_not_found’
// Node.js – Prelude check const check = await client.verification.check({ target: { type: 'phone_number', value: '+14155552671' }, code: '123456', }); if (check.status === 'success') { // grant access } // Possible statuses: 'success' | 'failure' | ‘expired_or_not_found’
Handling Retries
In Twilio Verify you initiate a retry by creating a new verification against the same phone number within the service’s retry window. Prelude works identically—call POST /v2/verification again with the same phone number and Prelude will recognise the open verification window and issue a new attempt rather than a fresh verification.
// Prelude – retry (same call as create) const retry = await client.verification.create({ target: { type: 'phone_number', value: '+14155552671' }, }); // retry.status === 'retry' when inside the window // Returns 429 Too Many Requests if max attempts reached // or if called before the minimum inter-retry delay
// Prelude – retry (same call as create) const retry = await client.verification.create({ target: { type: 'phone_number', value: '+14155552671' }, }); // retry.status === 'retry' when inside the window // Returns 429 Too Many Requests if max attempts reached // or if called before the minimum inter-retry delay
You can configure the maximum number of attempts and the verification window duration from the Prelude Dashboard.
Fraud Prevention
Twilio Fraud Guard vs. Prelude built-in
Twilio Fraud Guard is an optional add-on. Prelude’s fraud prevention is on by default for every request, powered by ML models and heuristics trained on tens of millions of data points across the Prelude network.
Signals to pass
The more signals you provide, the more effective fraud detection becomes. The table below shows the estimated conversion uplift per signal:
Signal field | Estimated uplift • Notes |
|---|---|
| +50% • Public IPv4 or IPv6 of the end-user device. If behind a proxy use X-Forwarded-For / CF-Connecting-IP. |
| +40% • Stable unique ID per device (Android: ANDROID_ID, iOS: identifierForVendor). |
| +35% • 'ios' or 'android'. |
| +30% • Auto-inferred when using Prelude Frontend SDKs; pass manually if you terminate TLS. |
| +20% • Device model string. |
| +20% • Operating system version. |
| +10% • Your application version. |
Using Frontend SDKs for richer signals and network fingerprinting
Install one of the Prelude Frontend SDKs (Android, iOS, Web, React Native, Flutter) to automatically collect device signals. The SDK automatically captures signals and provides you with a dispatch_id that you pass to the backend verification call.
// Web SDK example – get dispatch_id on the frontend import { dispatchSignals } from "@prelude.so/js-sdk/signals"; const dispatchId = await dispatchSignals(<your-prelude-sdk-key>); // Backend – pass dispatch_id to link device signals const verification = await client.verification.create({ target: { type
// Web SDK example – get dispatch_id on the frontend import { dispatchSignals } from "@prelude.so/js-sdk/signals"; const dispatchId = await dispatchSignals(<your-prelude-sdk-key>); // Backend – pass dispatch_id to link device signals const verification = await client.verification.create({ target: { type
Channels & Multi-Routing
Twilio Verify requires you to specify a channel ('sms', 'whatsapp', 'call', 'email') explicitly. Prelude automatically picks the best channel and route based on cost, conversion rate, and fraud signals—removing the need for you to manage carrier or channel selection logic.
No action required: Remove the channel field from your payloads. Prelude’s multi-routing engine handles channel selection automatically. You can still configure preferred channels from the Dashboard if needed. |
Message Content & Customisation
Prelude sends the OTP in the format:
|
The message is auto-translated into 32 locales based on the phone number’s country code. Available customisation options (configured in Dashboard or per-request):
Brand suffix – e.g. “12345 is your verification code for {company name}.”
Security suffix – e.g. “Do not share it.”
Custom locale – via options.locale (BCP-47 format).
Code length – 4–8 digits set in the Dashboard, can be overridden via options.code_size.
Custom OTP code – bring your own code via options.custom_code (subject to approval).
PSD2 template – built-in template for PSD2 transactions via options.template_id: 'prelude:psd2'.
Webhooks
Twilio – before
Twilio sends a StatusCallback POST to the URL you specify on the Verify Service or per-request.
Prelude – after
Specify a callback_url per verification request.
const verification = await client.verification.create({ target: { type: 'phone_number', value: '+14155552671', }, options: { callback_url: 'https://your-app.example.com/webhooks/prelude', }, });
const verification = await client.verification.create({ target: { type: 'phone_number', value: '+14155552671', }, options: { callback_url: 'https://your-app.example.com/webhooks/prelude', }, });
Event types
Event type | Description |
|---|---|
| A verification was created and billed. |
| An OTP attempt was sent to the user. |
| Delivery status update received from carrier. |
Signature verification
Prelude signs every webhook with RSASSA-PSS on the SHA-256 hash of the payload. The signature is in the X-Webhook-Signature header, prefixed with rsassa-pss-sha256=. Generate a signing key in the Dashboard.
IP allowlist
Whitelist these Prelude egress IPs on your firewall:
34.252.67.20952.30.192.16134.248.153.151
Testing Your Integration
Twilio magic numbers vs. Prelude test numbers
Both platforms provide special phone numbers for automated testing that do not incur charges. In Prelude, test numbers are configured in the Dashboard under Verify API > Configure > Numbers.
For each test number you define a fixed code. Only that code is accepted by the Check endpoint, allowing you to script pass/fail scenarios in your test suite.
Best practice: Use test numbers in CI/CD pipelines. Add them to your Dashboard before running integration tests. |
SDK Installation & Initialisation
Node.js
# Remove Twilio npm uninstall twilio # Install Prelude npm add @prelude.so/sdk
# Remove Twilio npm uninstall twilio # Install Prelude npm add @prelude.so/sdk
import Prelude from '@prelude.so/sdk'; const client = new Prelude({ apiToken: process.env.PRELUDE_API_KEY, });
import Prelude from '@prelude.so/sdk'; const client = new Prelude({ apiToken: process.env.PRELUDE_API_KEY, });
Python
# Remove Twilio pip uninstall twilio # Install Prelude pip install prelude_python_sdk
# Remove Twilio pip uninstall twilio # Install Prelude pip install prelude_python_sdk
from prelude_python_sdk import Prelude import os client = Prelude(api_token=os.environ['PRELUDE_API_KEY'])
from prelude_python_sdk import Prelude import os client = Prelude(api_token=os.environ['PRELUDE_API_KEY'])
Go
go get github.com/prelude-so/go-sdk
go get github.com/prelude-so/go-sdk
import ( "github.com/prelude-so/go-sdk" "github.com/prelude-so/go-sdk/option" client := prelude.NewClient( option.WithAPIToken(os.Getenv("PRELUDE_API_KEY")), )
import ( "github.com/prelude-so/go-sdk" "github.com/prelude-so/go-sdk/option" client := prelude.NewClient( option.WithAPIToken(os.Getenv("PRELUDE_API_KEY")), )
Java / Kotlin
// build.gradle implementation("so.prelude.sdk:prelude-java:0.2.0") // Initialise PreludeClient client = PreludeOkHttpClient.fromEnv(); // Set API_TOKEN environment variable
// build.gradle implementation("so.prelude.sdk:prelude-java:0.2.0") // Initialise PreludeClient client = PreludeOkHttpClient.fromEnv(); // Set API_TOKEN environment variable
Ruby
# Gemfile gem 'prelude_sdk' # Initialise require 'prelude_sdk' prelude = PreludeSDK::Client.new(api_token: ENV['API_TOKEN'])
# Gemfile gem 'prelude_sdk' # Initialise require 'prelude_sdk' prelude = PreludeSDK::Client.new(api_token: ENV['API_TOKEN'])
Migration Checklist
Work through each item below to complete the migration:
Phase 1 – Setup
Create a Prelude account at app.prelude.so and log in to the Dashboard.
Generate an API key in Dashboard > Configure > API Keys.
Add PRELUDE_API_KEY to your secrets manager / environment variables.
Install the Prelude SDK for your language (see Section 12).
Configure test numbers in Dashboard > Verify API > Configure > Numbers.
Phase 2 – Code Changes
Replace Twilio SDK initialisation with Prelude (see Section 12).
Replace
verifications.create(...)withclient.verification.create(...).Replace
verificationChecks.create(...)withclient.verification.check(...).Update status checks from
'approved'to'success'and'pending'to'success'.Remove
channelparameter – Prelude selects the best channel automatically.Remove
ServiceSIDreferences.Add fraud signals (
signals.ip,signals.device_id, etc.) to verification create calls.Update webhook handler for Prelude event types (see Section 10.3).
Implement webhook signature verification (RSASSA-PSS, see Section 10.4).
Phase 3 – Testing
Run a full end-to-end test on staging with a real phone number.
Verify webhook delivery and payload parsing.
Confirm blocked/fraud scenarios return the expected status.
Phase 4 – Rollout
Deploy to production (feature flag or canary recommended).
Monitor conversion rates and block rates in the Prelude Dashboard.
Remove legacy Twilio credentials from secrets manager.
Error Code Reference
Prelude uses standard HTTP status codes. The response body contains a JSON object with code and message fields.
HTTP Status / Code | Description |
|---|---|
| Maximum retry attempts reached for this verification window. |
| Retry requested before the minimum inter-retry delay. |
| Maximum code-check attempts reached. |
| Phone number is not a valid E.164 number. |
| Invalid or missing API key. |
To see full list of errors on |
Support & Resources
Documentation: https://docs.prelude.so
Dashboard: https://app.prelude.so
Status page: https://status.prelude.so
Support email: support@prelude.so
Why should we migrate from Twilio to Prelude?
There are four main reasons teams make the switch:
Lower costs. Businesses typically see 30–40% lower monthly SMS spend, largely because Prelude's fraud prevention intercepts fake traffic before it reaches carriers — you don't pay for fraudulent messages.
Higher conversion. Automated multi-channel routing picks the best carrier and channel per destination, improving delivery rates. Teams typically see 20–30% higher OTP conversion.
Fraud prevention included. Twilio Fraud Guard is an opt-in add-on. Prelude's ML-based fraud detection is on by default for every request, trained on tens of millions of data points across the network.
Simpler integration. Two Twilio services (Programmable Messaging + Verify) become one Prelude API. No ServiceSID, no channel selection logic, no carrier management — the API surface is significantly smaller.
The migration itself is low-risk: Prelude's API maps directly onto Twilio Verify's concepts, and most teams complete a working replacement in a single sprint using a feature flag rollout. See how Finfrog, a French lending fintech, cut SMS costs by 45% after switching: Finfrog case study
We're happy with Twilio. Is it worth the disruption?
The migration overhead is low, typically a few days of engineering time so even a modest improvement in conversion or cost pays back quickly. The strongest case for switching is if any of these apply:
You're seeing meaningful SMS fraud or abuse on your verification flow.
Your OTP delivery rates vary noticeably by country or carrier.
You're paying for Twilio Fraud Guard as a separate add-on.
You want to support channels beyond SMS (WhatsApp, Viber, RCS, Zalo) without managing routing logic yourself.
A canary rollout lets you measure the impact directly before committing — run 5–10% of traffic through Prelude and compare conversion and block rates side by side in the Dashboard. Finfrog used exactly this approach and switched over fully in a matter of days: read the case study
How long does the migration take?
Most backend teams complete the migration in a single sprint. The core code changes- swapping the SDK, updating the send and check calls, and remapping status values- take a few hours. Additional time is needed for webhook signature verification and adding fraud signals, but those are incremental improvements you can ship after the initial cutover.
Can I run Twilio and Prelude in parallel during a phased rollout?
Yes, and it's the recommended approach. Wrap the verification call behind a feature flag and route a small percentage of traffic to Prelude first. Monitor conversion and block rates in the Prelude Dashboard, then gradually increase the rollout. Keep your Twilio credentials active until you've fully cut over and confirmed metrics look healthy.
Is fraud prevention on by default, or do I need to enable it?
It's on by default for every request, unlike Twilio Fraud Guard, which is an opt-in add-on. Prelude's ML models run on every verification automatically. You don't need to configure anything to benefit from baseline protection. Passing device signals (ip, device_id, device_platform) incrementally improves accuracy on top of the default baseline.
Is there a real-time dashboard for monitoring delivery and block rates?
Yes. The Prelude Dashboard provides live visibility into conversion rates, block rates, delivery status breakdowns, and channel distribution. During and after your production rollout, monitor these metrics to confirm performance is in line with expectations before decommissioning your Twilio setup.
Replacing your OTP provider doesn't have to be a multi-sprint project. Prelude's unified Verify API maps directly onto Twilio Verify's concepts. Preludes provides a single REST endpoint that manages the entire OTP lifecycle, from fraud detection to multi-channel routing. Most teams ship a working replacement in a single sprint.
|
What Changes
At a high level, you're replacing two Twilio surfaces: Programmable Messaging and Twilio Verify with a single Prelude API. The table below maps every Twilio concept to its Prelude equivalent.
Concept | Twilio Verify | Prelude Verify |
|---|---|---|
Service category | Twilio Programmable Messaging + Twilio Verify | Prelude Verify API (single service) |
API base URL | api.twilio.com/2010-04-01/Accounts/{SID} | api.prelude.dev |
Authentication | HTTP Basic (AccountSID:AuthToken) | Bearer token in Authorization header |
Send OTP | POST /Services/{ServiceSID}/Verifications | POST /v2/verification |
Check OTP | POST /Services/{ServiceSID}/VerificationChecks | POST /v2/verification/check |
Retry OTP | POST /Services/{ServiceSID}/Verifications (repeat) | POST /v2/verification (same number, within window) |
Verification status | pending / approved / canceled | success / retry / blocked / challenged |
Service / namespace | Verify Service (ServiceSID) | Implicit per API key (configured in Dashboard) |
Fraud prevention | Twilio Fraud Guard (add-on) | Built-in ML, enabled by default |
Channels | SMS, WhatsApp, call, email | SMS, WhatsApp, Viber, Zalo, RCS, voice, email |
Sender / brand | Messaging Service or phone number | Configured by contacting with customer success (Sender ID) |
Test numbers | Magic numbers per Verify Service | Test numbers in Dashboard (Verify > Configure > Numbers) |
Webhooks | StatusCallback URL per request | callback_url per request; signature via RSASSA-PSS |
SDKs | twilio-node, twilio-python, etc. | npm @prelude.so/sdk, pip prelude_python_sdk, go-sdk, Java, Ruby |
Authentication
Twilio (before)
Twilio uses HTTP Basic Authentication with your Account SID as the username and Auth Token as the password.
# Twilio – HTTP Basic Auth curl -X POST https://verify.twilio.com/v2/Services/{ServiceSID}/Verifications \ -u $TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN \ -d 'To=+14155552671' \ -d 'Channel=sms'
Prelude (after)
Prelude uses a Bearer token. Generate an API key in the Dashboard under Settings > API Keys.
# Prelude – Bearer Token curl -X POST https://api.prelude.dev/v2/verification \ -H “Authorization: Bearer $PRELUDE_API_KEY” \ -H 'Content-Type: application/json' \ -d '{"target": {"type": "phone_number", "value": "+14155552671"}}'
Action required: Store your Prelude API key in an environment variable (e.g. PRELUDE_API_KEY). Never commit it to source control. Remove |
Sending a Verification (OTP)
Twilio Verify – before
// Node.js – Twilio Verify const client = require('twilio')(accountSid, authToken); const verification = await client.verify.v2 .services(verifySid) .verifications .create({ to: '+14155552671', channel: 'sms' }); console.log(verification.status); // 'pending'
Prelude – after
// Node.js – Prelude SDK import Prelude from '@prelude.so/sdk'; const client = new Prelude({ apiToken: process.env.PRELUDE_API_KEY }); const verification = await client.verification.create({ target: { type: 'phone_number', value: '+14155552671' }, // Optional: forward digital signals to better tackle fraud signals: { ip: '203.0.113.42', // end-user public IP device_id: 'abc123-unique', // stable device identifier device_platform: 'ios', device_model: 'iPhone 15', }, }); console.log(verification.id); // 'vrf_01...' console.log(verification.status); // 'success' | 'blocked' | 'challenged'
Status values
Prelude returns a richer set of statuses when creating a verification:
Status | Meaning |
|---|---|
| New verification window opened; code sent to user. |
| Same phone number called again within the window; new attempt sent. |
| Request flagged as fraudulent; no code sent, no charge of message fee. |
| Suspicious traffic; delivery restricted to non-SMS channels only (must be enabled by Prelude support). |
Checking a Verification Code
Twilio Verify – before
// Node.js – Twilio Verify check const check = await client.verify.v2 .services(verifySid) .verificationChecks .create({ to: '+14155552671', code: '123456' }); if (check.status === 'approved') { // grant access }
Prelude – after
// Node.js – Prelude check const check = await client.verification.check({ target: { type: 'phone_number', value: '+14155552671' }, code: '123456', }); if (check.status === 'success') { // grant access } // Possible statuses: 'success' | 'failure' | ‘expired_or_not_found’
Handling Retries
In Twilio Verify you initiate a retry by creating a new verification against the same phone number within the service’s retry window. Prelude works identically—call POST /v2/verification again with the same phone number and Prelude will recognise the open verification window and issue a new attempt rather than a fresh verification.
// Prelude – retry (same call as create) const retry = await client.verification.create({ target: { type: 'phone_number', value: '+14155552671' }, }); // retry.status === 'retry' when inside the window // Returns 429 Too Many Requests if max attempts reached // or if called before the minimum inter-retry delay
You can configure the maximum number of attempts and the verification window duration from the Prelude Dashboard.
Fraud Prevention
Twilio Fraud Guard vs. Prelude built-in
Twilio Fraud Guard is an optional add-on. Prelude’s fraud prevention is on by default for every request, powered by ML models and heuristics trained on tens of millions of data points across the Prelude network.
Signals to pass
The more signals you provide, the more effective fraud detection becomes. The table below shows the estimated conversion uplift per signal:
Signal field | Estimated uplift • Notes |
|---|---|
| +50% • Public IPv4 or IPv6 of the end-user device. If behind a proxy use X-Forwarded-For / CF-Connecting-IP. |
| +40% • Stable unique ID per device (Android: ANDROID_ID, iOS: identifierForVendor). |
| +35% • 'ios' or 'android'. |
| +30% • Auto-inferred when using Prelude Frontend SDKs; pass manually if you terminate TLS. |
| +20% • Device model string. |
| +20% • Operating system version. |
| +10% • Your application version. |
Using Frontend SDKs for richer signals and network fingerprinting
Install one of the Prelude Frontend SDKs (Android, iOS, Web, React Native, Flutter) to automatically collect device signals. The SDK automatically captures signals and provides you with a dispatch_id that you pass to the backend verification call.
// Web SDK example – get dispatch_id on the frontend import { dispatchSignals } from "@prelude.so/js-sdk/signals"; const dispatchId = await dispatchSignals(<your-prelude-sdk-key>); // Backend – pass dispatch_id to link device signals const verification = await client.verification.create({ target: { type
Channels & Multi-Routing
Twilio Verify requires you to specify a channel ('sms', 'whatsapp', 'call', 'email') explicitly. Prelude automatically picks the best channel and route based on cost, conversion rate, and fraud signals—removing the need for you to manage carrier or channel selection logic.
No action required: Remove the channel field from your payloads. Prelude’s multi-routing engine handles channel selection automatically. You can still configure preferred channels from the Dashboard if needed. |
Message Content & Customisation
Prelude sends the OTP in the format:
|
The message is auto-translated into 32 locales based on the phone number’s country code. Available customisation options (configured in Dashboard or per-request):
Brand suffix – e.g. “12345 is your verification code for {company name}.”
Security suffix – e.g. “Do not share it.”
Custom locale – via options.locale (BCP-47 format).
Code length – 4–8 digits set in the Dashboard, can be overridden via options.code_size.
Custom OTP code – bring your own code via options.custom_code (subject to approval).
PSD2 template – built-in template for PSD2 transactions via options.template_id: 'prelude:psd2'.
Webhooks
Twilio – before
Twilio sends a StatusCallback POST to the URL you specify on the Verify Service or per-request.
Prelude – after
Specify a callback_url per verification request.
const verification = await client.verification.create({ target: { type: 'phone_number', value: '+14155552671', }, options: { callback_url: 'https://your-app.example.com/webhooks/prelude', }, });
Event types
Event type | Description |
|---|---|
| A verification was created and billed. |
| An OTP attempt was sent to the user. |
| Delivery status update received from carrier. |
Signature verification
Prelude signs every webhook with RSASSA-PSS on the SHA-256 hash of the payload. The signature is in the X-Webhook-Signature header, prefixed with rsassa-pss-sha256=. Generate a signing key in the Dashboard.
IP allowlist
Whitelist these Prelude egress IPs on your firewall:
34.252.67.20952.30.192.16134.248.153.151
Testing Your Integration
Twilio magic numbers vs. Prelude test numbers
Both platforms provide special phone numbers for automated testing that do not incur charges. In Prelude, test numbers are configured in the Dashboard under Verify API > Configure > Numbers.
For each test number you define a fixed code. Only that code is accepted by the Check endpoint, allowing you to script pass/fail scenarios in your test suite.
Best practice: Use test numbers in CI/CD pipelines. Add them to your Dashboard before running integration tests. |
SDK Installation & Initialisation
Node.js
# Remove Twilio npm uninstall twilio # Install Prelude npm add @prelude.so/sdk
import Prelude from '@prelude.so/sdk'; const client = new Prelude({ apiToken: process.env.PRELUDE_API_KEY, });
Python
# Remove Twilio pip uninstall twilio # Install Prelude pip install prelude_python_sdk
from prelude_python_sdk import Prelude import os client = Prelude(api_token=os.environ['PRELUDE_API_KEY'])
Go
go get github.com/prelude-so/go-sdk
import ( "github.com/prelude-so/go-sdk" "github.com/prelude-so/go-sdk/option" client := prelude.NewClient( option.WithAPIToken(os.Getenv("PRELUDE_API_KEY")), )
Java / Kotlin
// build.gradle implementation("so.prelude.sdk:prelude-java:0.2.0") // Initialise PreludeClient client = PreludeOkHttpClient.fromEnv(); // Set API_TOKEN environment variable
Ruby
# Gemfile gem 'prelude_sdk' # Initialise require 'prelude_sdk' prelude = PreludeSDK::Client.new(api_token: ENV['API_TOKEN'])
Migration Checklist
Work through each item below to complete the migration:
Phase 1 – Setup
Create a Prelude account at app.prelude.so and log in to the Dashboard.
Generate an API key in Dashboard > Configure > API Keys.
Add PRELUDE_API_KEY to your secrets manager / environment variables.
Install the Prelude SDK for your language (see Section 12).
Configure test numbers in Dashboard > Verify API > Configure > Numbers.
Phase 2 – Code Changes
Replace Twilio SDK initialisation with Prelude (see Section 12).
Replace
verifications.create(...)withclient.verification.create(...).Replace
verificationChecks.create(...)withclient.verification.check(...).Update status checks from
'approved'to'success'and'pending'to'success'.Remove
channelparameter – Prelude selects the best channel automatically.Remove
ServiceSIDreferences.Add fraud signals (
signals.ip,signals.device_id, etc.) to verification create calls.Update webhook handler for Prelude event types (see Section 10.3).
Implement webhook signature verification (RSASSA-PSS, see Section 10.4).
Phase 3 – Testing
Run a full end-to-end test on staging with a real phone number.
Verify webhook delivery and payload parsing.
Confirm blocked/fraud scenarios return the expected status.
Phase 4 – Rollout
Deploy to production (feature flag or canary recommended).
Monitor conversion rates and block rates in the Prelude Dashboard.
Remove legacy Twilio credentials from secrets manager.
Error Code Reference
Prelude uses standard HTTP status codes. The response body contains a JSON object with code and message fields.
HTTP Status / Code | Description |
|---|---|
| Maximum retry attempts reached for this verification window. |
| Retry requested before the minimum inter-retry delay. |
| Maximum code-check attempts reached. |
| Phone number is not a valid E.164 number. |
| Invalid or missing API key. |
To see full list of errors on |
Support & Resources
Documentation: https://docs.prelude.so
Dashboard: https://app.prelude.so
Status page: https://status.prelude.so
Support email: support@prelude.so
Why should we migrate from Twilio to Prelude?
There are four main reasons teams make the switch:
Lower costs. Businesses typically see 30–40% lower monthly SMS spend, largely because Prelude's fraud prevention intercepts fake traffic before it reaches carriers — you don't pay for fraudulent messages.
Higher conversion. Automated multi-channel routing picks the best carrier and channel per destination, improving delivery rates. Teams typically see 20–30% higher OTP conversion.
Fraud prevention included. Twilio Fraud Guard is an opt-in add-on. Prelude's ML-based fraud detection is on by default for every request, trained on tens of millions of data points across the network.
Simpler integration. Two Twilio services (Programmable Messaging + Verify) become one Prelude API. No ServiceSID, no channel selection logic, no carrier management — the API surface is significantly smaller.
The migration itself is low-risk: Prelude's API maps directly onto Twilio Verify's concepts, and most teams complete a working replacement in a single sprint using a feature flag rollout. See how Finfrog, a French lending fintech, cut SMS costs by 45% after switching: Finfrog case study
We're happy with Twilio. Is it worth the disruption?
The migration overhead is low, typically a few days of engineering time so even a modest improvement in conversion or cost pays back quickly. The strongest case for switching is if any of these apply:
You're seeing meaningful SMS fraud or abuse on your verification flow.
Your OTP delivery rates vary noticeably by country or carrier.
You're paying for Twilio Fraud Guard as a separate add-on.
You want to support channels beyond SMS (WhatsApp, Viber, RCS, Zalo) without managing routing logic yourself.
A canary rollout lets you measure the impact directly before committing — run 5–10% of traffic through Prelude and compare conversion and block rates side by side in the Dashboard. Finfrog used exactly this approach and switched over fully in a matter of days: read the case study
How long does the migration take?
Most backend teams complete the migration in a single sprint. The core code changes- swapping the SDK, updating the send and check calls, and remapping status values- take a few hours. Additional time is needed for webhook signature verification and adding fraud signals, but those are incremental improvements you can ship after the initial cutover.
Can I run Twilio and Prelude in parallel during a phased rollout?
Yes, and it's the recommended approach. Wrap the verification call behind a feature flag and route a small percentage of traffic to Prelude first. Monitor conversion and block rates in the Prelude Dashboard, then gradually increase the rollout. Keep your Twilio credentials active until you've fully cut over and confirmed metrics look healthy.
Is fraud prevention on by default, or do I need to enable it?
It's on by default for every request, unlike Twilio Fraud Guard, which is an opt-in add-on. Prelude's ML models run on every verification automatically. You don't need to configure anything to benefit from baseline protection. Passing device signals (ip, device_id, device_platform) incrementally improves accuracy on top of the default baseline.
Is there a real-time dashboard for monitoring delivery and block rates?
Yes. The Prelude Dashboard provides live visibility into conversion rates, block rates, delivery status breakdowns, and channel distribution. During and after your production rollout, monitor these metrics to confirm performance is in line with expectations before decommissioning your Twilio setup.
Start optimizing your auth flow
Send verification text-messages anywhere in the world with the best price, the best deliverability and no spam.
