Twilio to Prelude Migration

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.

Why migrate? Businesses switching to Prelude typically see 20–30% higher conversion rates and 30–40% lower monthly SMS costs, plus built-in ML-based fraud prevention that requires no custom tooling.

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 TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and VERIFY_SERVICE_SID once migration is complete.

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

success

New verification window opened; code sent to user.

retry

Same phone number called again within the window; new attempt sent.

blocked

Request flagged as fraudulent; no code sent, no charge of message fee.

challenged

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

signals.ip

+50%  •  Public IPv4 or IPv6 of the end-user device. If behind a proxy use X-Forwarded-For / CF-Connecting-IP.

signals.device_id

+40%  •  Stable unique ID per device (Android: ANDROID_ID, iOS: identifierForVendor).

signals.device_platform

+35%  •  'ios' or 'android'.

signals.ja4_fingerprint

+30%  •  Auto-inferred when using Prelude Frontend SDKs; pass manually if you terminate TLS.

signals.device_model

+20%  •  Device model string.

signals.os_version

+20%  •  Operating system version.

signals.app_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:

12345 is your verification code.

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

verify.authentication

A verification was created and billed.

verify.attempt

An OTP attempt was sent to the user.

verify.delivery_status

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.209

  • 52.30.192.161

  • 34.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(...) with client.verification.create(...).

  • Replace verificationChecks.create(...) with client.verification.check(...).

  • Update status checks from 'approved' to 'success' and 'pending' to 'success'.

  • Remove channel parameter – Prelude selects the best channel automatically.

  • Remove ServiceSID references.

  • 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

429 too_many_attempts

Maximum retry attempts reached for this verification window.

429 premature_retry

Retry requested before the minimum inter-retry delay.

429 too_many_checks

Maximum code-check attempts reached.

400 invalid_phone_number

Phone number is not a valid E.164 number.

401 unauthorized

Invalid or missing API key.

To see full list of errors on 4xx

https://docs.prelude.so/introduction/errors

Support & Resources

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 (ipdevice_iddevice_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.

Why migrate? Businesses switching to Prelude typically see 20–30% higher conversion rates and 30–40% lower monthly SMS costs, plus built-in ML-based fraud prevention that requires no custom tooling.

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 TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and VERIFY_SERVICE_SID once migration is complete.

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

success

New verification window opened; code sent to user.

retry

Same phone number called again within the window; new attempt sent.

blocked

Request flagged as fraudulent; no code sent, no charge of message fee.

challenged

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

signals.ip

+50%  •  Public IPv4 or IPv6 of the end-user device. If behind a proxy use X-Forwarded-For / CF-Connecting-IP.

signals.device_id

+40%  •  Stable unique ID per device (Android: ANDROID_ID, iOS: identifierForVendor).

signals.device_platform

+35%  •  'ios' or 'android'.

signals.ja4_fingerprint

+30%  •  Auto-inferred when using Prelude Frontend SDKs; pass manually if you terminate TLS.

signals.device_model

+20%  •  Device model string.

signals.os_version

+20%  •  Operating system version.

signals.app_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:

12345 is your verification code.

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

verify.authentication

A verification was created and billed.

verify.attempt

An OTP attempt was sent to the user.

verify.delivery_status

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.209

  • 52.30.192.161

  • 34.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(...) with client.verification.create(...).

  • Replace verificationChecks.create(...) with client.verification.check(...).

  • Update status checks from 'approved' to 'success' and 'pending' to 'success'.

  • Remove channel parameter – Prelude selects the best channel automatically.

  • Remove ServiceSID references.

  • 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

429 too_many_attempts

Maximum retry attempts reached for this verification window.

429 premature_retry

Retry requested before the minimum inter-retry delay.

429 too_many_checks

Maximum code-check attempts reached.

400 invalid_phone_number

Phone number is not a valid E.164 number.

401 unauthorized

Invalid or missing API key.

To see full list of errors on 4xx

https://docs.prelude.so/introduction/errors

Support & Resources

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 (ipdevice_iddevice_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.