Verify Passkeys quickstart
Private Beta
This document could change. Twilio might add or update features before the product becomes Generally Available. Beta products don't have a Service Level Agreement (SLA). To learn more about Twilio beta support, see beta product support.
The Verify API lets you add Passkeys into your existing authentication flows. The Verify API supports Passkey registration, public key storage, and authentication flows.
Verify Passkeys offers client-side supported SDKs for iOS and Android. This adds a low-friction, secure, cost-effective, device approval factor into your own mobile application.
Twilio belongs to the Fast IDentity Online (FIDO) alliance that created the Passkeys standard.
This tutorial covers Verify Passkeys service configuration and biometric Passkeys registration and authentication. This includes the following tasks:
- Create a Verify
Servicefor passkeys. - Generate a Passkey in your browser console.
- Register a Passkey
Factoron the Twilio server. - Save your Passkey to your password manager.
- Create an authentication challenge from the server.
- Sign the challenge with the Passkey and authenticate the signature of your user on the Twilio server.
To verify users at login, transaction, and other sensitive actions, follow this quickstart on Verify Passkeys.
Info
This quickstart alternates between server-side and client-side steps. To make the server-side requests, use the cURL command or an API client like Postman. As Passkeys requests bind to specific domains requests and require user interaction, you make your requests in your browser's developer tools.
A Passkey binds to an app or website domain. During Passkey creation and challenge verification, the underlying WebAuthn API checks the domain in the browser URL.
To prevent issues with a production environment, create a Verify Service instance. Replace the Relying Party parameters with your application name and domains. If you like to update an existing service, check the Service API for more details.
POST /v2/Services
| Name | Type | Necessity | Description |
|---|---|---|---|
Name | String | Required | Human-readable description of the Verify service. Maximum length: 255 characters. |
Passkeys | object | Required | Metadata for the Passkey. |
Passkeys.RelyingParty.id | String | Required | Relying party identifier expressed as the origin domain without a scheme and port. |
Passkeys.RelyingParty.name | String | Required | Description that the authenticator displays during the registration and authentication process. |
Passkeys.RelyingParty.origins | Array[String] | Required | List of Relying Party Server Origins or App IDs that your service accepts. |
Passkeys.AuthenticatorAttachment | String | Required | Flag that indicates a requirement to attach only to a certain type of authenticator. Accepts: platform, cross-platform, any |
Passkeys.DiscoverableCredentials | String | Required | Flag that indicates the level of preference for discoverable credentials. Accepts: required, preferred, discouraged |
Passkeys.UserVerification | String | Required | Flag that indicates whether authentication requires user identity verification. Accepts: required, preferred, discouraged |
-
Make a request to the
Servicesresource. Pass all required parameters.1curl -L 'https://verify.twilio.com/v2/Services' \2-H 'Content-Type: application/x-www-form-urlencoded' \3-H 'Authorization: ••••••' \4--data-urlencode 'Name=Fancy Service' \5--data-urlencode 'Passkeys.RelyingParty.Id=example.com' \6--data-urlencode 'Passkeys.RelyingParty.Name=My domain' \7--data-urlencode 'Passkeys.RelyingParty.Origins=example.com,login.example.com' \8--data-urlencode 'Passkeys.AuthenticatorAttachment=platform' \9--data-urlencode 'Passkeys.DiscoverableCredentials=preferred' \10--data-urlencode 'Passkeys.UserVerification=preferred' -
If the response includes a
passkeysJSON object that matches your request, you have succeeded in starting a Verify service.1{2"sid": "VAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",3"account_sid": "ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",4"friendly_name": "Fancy Service",5...6"passkeys": {7"relying_party": {8"id": "example.com",9"name": "My domain",10"origins": [11"example.com",12"login.example.com"13]14},15"authenticator_attachment": "platform",16"discoverable_credentials": "preferred",17"user_verification": "preferred"18},19...20}
To register a Passkey, complete three tasks:
- On the Twilio server: Create a Passkey factor.
- In your browser client: Register a Passkey with the factor details
- On the Twilio server: Verify Passkey registration
A Passkey factor refers to the authentication component that the Passkey uses to verify your identity. This could be something you know, like a personal identification number (PIN), or something you have, like a fingerprint.
To start registering a Passkey, call the Twilio Factors subresource with the following parameters.
POST /v2/Services/{service_sid}/Passkeys/Factors
| Name | Type | Necessity | Description | PII |
|---|---|---|---|---|
friendly_name | String | Required | Human-readable description of the Factor. Maximum length: 255 characters. | No |
identity | String | Optional | Unique external identifier for this Service. Your external system should generate an immutable, non-PII value between 8 and 64 characters in length. Acceptable values include your user's UUID, GUID, or SID. It can only contain hyphen (-) separated hexadecimal digits. If omitted in the request, the API creates this value. | Yes |
config.authenticator_attachment | String | Optional | Flag that indicates a requirement to attach only to a certain type of authenticator. Accepts: platform, cross-platform, anyDefault: Per Service configuration | No |
config.discoverable_credentials | String | Optional | Flag that indicates the level of preference for discoverable credentials. Accepts: required, preferred, discouragedDefault: Per Service configuration | No |
config.user_verification | String | Optional | Flag that indicates whether user identity verification is required. Accepts: required, preferred, discouragedDefault: Per Service configuration | No |
-
Make
POSTrequest to theFactorssubresource. Pass thefriendly_nameandidentitykeys and values as a JSON object payload.1// Download the helper library from https://www.twilio.com/docs/node/install2const twilio = require("twilio"); // Or, for ESM: import twilio from "twilio";34// Find your Account SID and Auth Token at twilio.com/console5// and set the environment variables. See http://twil.io/secure6const accountSid = process.env.TWILIO_ACCOUNT_SID;7const authToken = process.env.TWILIO_AUTH_TOKEN;8const client = twilio(accountSid, authToken);910async function createNewFactorPasskey() {11const newFactor = await client.verify.v212.services("VAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")13.newFactors.create({14friendly_name: "friendly_name",15identity: "identity",16});1718console.log(newFactor.binding);19}2021createNewFactorPasskey();Response
1{2"sid": "YFaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",3"account_sid": "ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",4"service_sid": "VAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",5"entity_sid": "YEaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",6"identity": "identity",7"binding": null,8"date_created": "2015-07-30T20:00:00Z",9"date_updated": "2015-07-30T20:00:00Z",10"friendly_name": "friendly_name",11"status": "unverified",12"factor_type": "passkeys",13"config": {14"relying_party": {15"id": "example.com",16"name": "Example",17"origins": [18"https://example.com"19]20},21"authenticator_attachment": "platform",22"discoverable_credentials": "preferred",23"user_verification": "preferred"24},25"options": {26"publicKey": {27"rp": {28"id": "example.com",29"name": "Example"30},31"user": {32"id": "WUU0ZmQzYWFmNGU0NTMyNGQwZjNlMTM0NjA3YjIxOTEyYg",33"name": "friendly_name",34"displayName": "friendly_name"35},36"challenge": "WUYwNDhkMWE3ZWMzYTJhNjk3MDA1OWMyNzY2YmJjN2UwZg",37"pubKeyCredParams": {38"type": "public-key",39"alg": -740},41"timeout": 600000,42"excludeCredentials": [],43"authenticatorSelection": {44"authenticatorAttachment": "platform",45"requireResidentKey": false,46"residentKey": "preferred",47"userVerification": "preferred"48},49"attestation": "none"50}51},52"metadata": null,53"url": "https://verify.twilio.com/v2/Services/VAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Entities/ff483d1ff591898a9942916050d2ca3f/Factors/YFaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"54} -
Copy the entire response. This JSON object becomes the
optionvariable value in the Set the Passkey factor response as a variable example.
To create a Passkey in your browser, pass the entire Create a Passkey factor response example into your browser console.
-
Open your browser.
-
Go to the domain returned in
config.relying_party.idin the Create a Passkey factor response example. -
Open the browser's developer tools and open your browser console. Perform the remaining steps in the browser console.
-
Paste the following code into the console to load the
webauthn-jsonlibrary.1const { create, get, parseCreationOptionsFromJSON, parseRequestOptionsFromJSON, supported } = await2import('https://cdn.jsdelivr.net/npm/@github/webauthn-json/dist/esm/webauthn-json.browser-ponyfill.js') -
Create a variable to store the entire Create a Passkey factor response example, then paste the response as that variable's value.
let { options } = '<paste the entire Create a Passkey factor response example>'; -
Copy the following code.
1// converts challenge and user.id to ArrayBuffers2let creationOptions = parseCreationOptionsFromJSON(options);3// wrapper for navigator.credentials.create4let credential = await create(creationOptions); -
Paste the code into your browser console then execute it. This generates a Passkey credential.
View an example of the generated credential object -
Follow the prompts in your browser to register a Passkey with the password manager of your choice.
-
Copy the JSON response body to your clipboard with the following command.
copy(credential);
To finish registering the Passkey, pass the copied credential as a JSON object to the VerifyFactor subresource.
POST /v2/Services/{service_sid}/Passkeys/VerifyFactor
| Name | Type | Necessity | Description | PII |
|---|---|---|---|---|
id | String | Required | Base64-encoded, URL-safe representation of keyId. | No |
keyId | String | Required | Globally unique identifier for this PublicKeyCredential. | No |
authenticatorAttachment | String | Required | Mechanism by which the WebAuthn implementation is attached to the authenticator at the time the associated create(parseCreationOptionsFromJSON()) or get(parseRequestOptionsFromJSON()) call completes.Accepts: platform, cross-platform | No |
type | String | Required | Credential types the API supports. These values version the AuthenticatorAssertion and AuthenticatorAttestation structures according to the type of the authenticator.Accepts: public-key | No |
response | object | Required | Result of a WebAuthn credential registration as specified in AuthenticatorAttestationResponse. | No |
response.clientDataJSON | String | Required | JSON-compatible serialization of the data passed from the browser to the authenticator that generated this credential. | No |
response.attestationObject | String | Required | Authenticator data and an attestation statement for a new key pair that the authenticator generated. | No |
response.transports | Array[String] | Optional | Hints as to the methods the client could use to communicate with the relevant authenticator of the public key credential to retrieve. Accepts: usb, nfc, ble, smart-card, internal, hybrid | No |
-
Make a
POSTrequest to theVerifyFactorsubresource. Pass the properties of thecredentialyou copied from your browser console as a JSON object payload.1// Download the helper library from https://www.twilio.com/docs/node/install2const twilio = require("twilio"); // Or, for ESM: import twilio from "twilio";34// Find your Account SID and Auth Token at twilio.com/console5// and set the environment variables. See http://twil.io/secure6const accountSid = process.env.TWILIO_ACCOUNT_SID;7const authToken = process.env.TWILIO_AUTH_TOKEN;8const client = twilio(accountSid, authToken);910async function updatePasskeysFactor() {11const newVerifyFactor = await client.verify.v212.services("VAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")13.newVerifyFactors.update({14id: "iYCpoVaY3omxc8twDSNEaF4JjVlvbvjOgQGV",15response: {16attestationObject: "attestationObject",17clientDataJSON: "clientDataJSON",18transports: ["usb"],19},20});2122console.log(newVerifyFactor.sid);23}2425updatePasskeysFactor();Response
1{2"sid": "YFaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",3"account_sid": "ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",4"service_sid": "VAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",5"entity_sid": "YEaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",6"identity": "ff483d1ff591898a9942916050d2ca3f",7"date_created": "2015-07-30T20:00:00Z",8"date_updated": "2015-07-30T20:00:00Z",9"friendly_name": "friendly_name",10"status": "verified",11"factor_type": "passkeys",12"config": {13"relying_party": {14"id": "example.com",15"name": "Example",16"origins": [17"https://example.com"18]19},20"authenticator_attachment": "platform",21"discoverable_credentials": "preferred",22"user_verification": "preferred"23},24"metadata": null,25"url": "https://verify.twilio.com/v2/Services/VAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Entities/ff483d1ff591898a9942916050d2ca3f/Factors/YFaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"26} -
If the response returns
"status": "verified", you have succeeded in creating and verifying a Passkey.
To authenticate with your Passkey, complete three tasks:
- On the Twilio server: Create an authentication challenge.
- In your browser client: Fetch the Passkey with the challenge data. The browser signs the challenge with the Passkey.
- On the Twilio server: Validate the signature and approve the authentication challenge.
POST /v2/Services/{service_sid}/Passkeys/Challenges
| Name | Type | Necessity | Description | PII |
|---|---|---|---|---|
identity | String | Required | Unique identifier for a person or end user. | Yes |
-
Make a
POSTrequest to theChallengessubresource. Use the sameidentityvalue from creating the Passkey factor.1// Download the helper library from https://www.twilio.com/docs/node/install2const twilio = require("twilio"); // Or, for ESM: import twilio from "twilio";34// Find your Account SID and Auth Token at twilio.com/console5// and set the environment variables. See http://twil.io/secure6const accountSid = process.env.TWILIO_ACCOUNT_SID;7const authToken = process.env.TWILIO_AUTH_TOKEN;8const client = twilio(accountSid, authToken);910async function createChallengePasskeys() {11const newChallenge = await client.verify.v212.services("VAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")13.newChallenge()14.create({15identity: "identity",16});1718console.log(newChallenge.options);19}2021createChallengePasskeys();Response
1{2"sid": "YCaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",3"account_sid": "ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",4"service_sid": "VAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",5"entity_sid": "",6"identity": "identity",7"factor_sid": "",8"factor_type": "passkeys",9"status": "pending",10"date_created": "2025-07-30T20:00:00Z",11"date_updated": "2025-07-30T20:00:00Z",12"date_responded": null,13"details": null,14"expiration_date": null,15"hidden_details": null,16"links": null,17"metadata": null,18"responded_reason": "none",19"url": "https://verify.twilio.com/v2/Services/VAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Passkeys/Challenges",20"options": {21"publicKey": {22"rp": {23"id": "example.com",24"name": "Example"25},26"user": {27"id": "WUU0ZmQzYWFmNGU0NTMyNGQwZjNlMTM0NjA3YjIxOTEyYg",28"name": "friendly_name",29"displayName": "friendly_name"30},31"challenge": "WUYwNDhkMWE3ZWMzYTJhNjk3MDA1OWMyNzY2YmJjN2UwZg",32"pubKeyCredParams": {33"type": "public-key",34"alg": -735},36"timeout": 600000,37"excludeCredentials": [],38"authenticatorSelection": {39"authenticatorAttachment": "platform",40"requireResidentKey": false,41"residentKey": "preferred",42"userVerification": "preferred"43},44"attestation": "none"45}46}47} -
Copy the response for use in the Fetch the Passkey from your browser section.
To fetch the Passkey from your browser, pass the entire response from the Create a Passkey authentication challenge response example into your browser console.
This requires using the same browser session. If you closed the browser console, open a new one, then reload the webauthn-json helper functions.
-
Open your browser.
-
Go to the domain found in
publicKey.rpidin the Create a Passkey authentication challenge response example. -
Open the browser's developer tools and open your browser console. Perform the remaining steps in the browser console.
-
Create a variable to store the entire Create a Passkey authentication challenge response example, then paste the response as that variable's value.
let { options } = '<paste the entire Create a Passkey authentication challenge response example>'; -
Copy the following code.
1// converts challenge and user.id to ArrayBuffers2let requestOptions = parseRequestOptionsFromJSON(options);3// wrapper for navigator.credentials.create4let credential = await get(requestOptions); -
Paste the code into your browser console then execute it. This generates a credential that needs server-side validation.
-
Follow the prompts from your password manager.
-
Copy the JSON response body to your clipboard with the following command.
copy(credential);
To finish authenticating your Passkey challenge, pass the response from the Fetch the Passkey request example request as a JSON object to the ApproveChallenge subresource.
POST /v2/Services/{service_sid}/Passkeys/ApproveChallenge
| Name | Type | Necessity | Description | PII |
|---|---|---|---|---|
id | String | Required | Base64-encoded, URL-safe representation of keyId. | No |
keyId | String | Required | Globally unique identifier for this PublicKeyCredential. | No |
authenticatorAttachment | String | Optional | Mechanism by which the WebAuthn implementation attaches to the authenticator at the time the associated create(parseCreationOptionsFromJSON()) or get(parseRequestOptionsFromJSON()) call completes.Accepts: platform, cross-platform | No |
type | String | Required | Credential types the API supports. These values version the AuthenticatorAssertion and AuthenticatorAttestation structures according to the type of the authenticator.Accepts: public-key | No |
response | object | Required | Result of a WebAuthn credential registration as specified in AuthenticatorAttestationResponse. | No |
response.clientDataJSON | String | Optional | JSON-compatible serialization of the data passed from the browser to the authenticator that generated this credential. | No |
response.signature | String | Optional | Assertion signature over authenticatorData and clientDataJSON. | No |
response.userHandle | String | Optional | User handle stored in the authenticator as a Base64-encoded, URL-safe contact ID. | No |
response.authenticatorData | String | Optional | Serialized data encoded in the Client to Authenticator Protocol (CTAP2) canonical Concise Binary Object Representation (CBOR) encoding form. | No |
-
Make a
POSTrequest to theApproveChallengesubresource. Pass the properties of thecredentialyou copied from your browser console as a JSON object payload.1// Download the helper library from https://www.twilio.com/docs/node/install2const twilio = require("twilio"); // Or, for ESM: import twilio from "twilio";34// Find your Account SID and Auth Token at twilio.com/console5// and set the environment variables. See http://twil.io/secure6const accountSid = process.env.TWILIO_ACCOUNT_SID;7const authToken = process.env.TWILIO_AUTH_TOKEN;8const client = twilio(accountSid, authToken);910async function updateChallengePasskeys() {11const approveChallenge = await client.verify.v212.services("VAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")13.approveChallenge.update({14id: "fr4561ff591898a9942916050d2ca3f",15rawId: "rawId",16authenticatorAttachment: "platform",17response: {18authenticatorData: "authenticatorData",19clientDataJSON: "clientDataJSON",20signature: "signature",21userHandle: "userHandle",22},23});2425console.log(approveChallenge.options);26}2728updateChallengePasskeys();Response
1{2"sid": "YCaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",3"account_sid": "ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",4"service_sid": "VAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",5"entity_sid": "",6"identity": "",7"factor_sid": "",8"factor_type": "passkeys",9"status": "approved",10"date_created": "2025-07-30T20:00:00Z",11"date_updated": "2025-07-30T20:00:00Z",12"date_responded": null,13"details": null,14"expiration_date": null,15"hidden_details": null,16"links": null,17"metadata": null,18"responded_reason": "none",19"url": "https://verify.twilio.com/v2/Services/VAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Passkeys/ApproveChallenge",20"options": {21"publicKey": {22"rp": {23"id": "example.com",24"name": "Example"25},26"user": {27"id": "WUU0ZmQzYWFmNGU0NTMyNGQwZjNlMTM0NjA3YjIxOTEyYg",28"name": "friendly_name",29"displayName": "friendly_name"30},31"challenge": "WUYwNDhkMWE3ZWMzYTJhNjk3MDA1OWMyNzY2YmJjN2UwZg",32"pubKeyCredParams": {33"type": "public-key",34"alg": -735},36"timeout": 600000,37"excludeCredentials": [],38"authenticatorSelection": {39"authenticatorAttachment": "platform",40"requireResidentKey": false,41"residentKey": "preferred",42"userVerification": "preferred"43},44"attestation": "none"45}46}47} -
If the response returns
"status": "approved", you have succeeded in creating and verifying a Passkey.