Twilio A2P Provider
🎯 Overview
The A2P (Application-to-Person) Provider (Providers/a2p.js) manages A2P 10DLC brand and campaign registration required for high-volume SMS messaging to US phone numbers.
Source: external/Integrations/Twilio/Providers/a2p.js
What is A2P 10DLC?
A2P 10DLC is a system in the United States for long code (10-digit) phone numbers that send Application-to-Person (A2P) messages. Carriers require businesses to register their brand and campaigns to reduce spam and improve deliverability.
Key Capabilities:
- Fetch customer profile policies
- Create customer profile bundles (TrustHub)
- Manage end-user objects (business/individual info)
- Create and manage address objects
- Save registration submission data
- Support for Starter and Secondary customer profiles
Why A2P Registration?
- Required: US carriers require A2P registration for business SMS
- Deliverability: Registered brands have better message delivery rates
- Compliance: Avoid message blocking and account suspension
- Throughput: Higher message-per-second limits with registration
🔌 Provider Functions
getStarterCustomerProfilePolicy()
Fetch the Starter Customer Profile Policy from TrustHub.
Signature:
const getStarterCustomerProfilePolicy = async(metadata, id);
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
metadata | Object | ✅ | Contains twilio_creds: {sid, token} and auth |
id | String | ✅ | Policy SID (default: RN806dd6cd175f314e1f96a9727ee271f4) |
Constants:
STARTER_CUSTOMER_PROFILE_POLICY_ID:'RN806dd6cd175f314e1f96a9727ee271f4'SECONDARY_CUSTOMER_PROFILE_POLICY_ID:'RNdfbf3fae0e1107f8aded0e7cead80bf5'
Returns:
Promise<{
sid: string;
friendlyName: string;
requirements: Object;
}>;
Example Usage:
const a2pProvider = require('./Providers/a2p');
// Get Starter profile policy
const policy = await a2pProvider.getStarterCustomerProfilePolicy(
{
twilio_creds: {
sid: accountSID,
token: authToken,
},
},
'RN806dd6cd175f314e1f96a9727ee271f4',
);
console.log('Policy:', policy.friendlyName);
console.log('Requirements:', policy.requirements);
saveRegistrationSubmission()
Save A2P registration submission data to DashClicks account.
Signature:
const saveRegistrationSubmission = async(metadata, data);
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
metadata | Object | ✅ | Contains twilio_creds: {sid, token} and type |
data | Object | ✅ | Registration submission payload |
Business Logic:
- Updates DashClicks account with:
twilio_account.a2p.paid: truetwilio_account.a2p.type: metadata.typetwilio_account.a2p.submission_data: data
Example Usage:
await a2pProvider.saveRegistrationSubmission(
{
twilio_creds: { sid: accountSID, token: authToken },
type: 'starter', // or 'secondary'
},
{
businessName: 'DashClicks LLC',
ein: '12-3456789',
website: 'https://dashclicks.com',
submittedAt: new Date(),
},
);
createCustomerProfile()
Create a Customer Profile Bundle in TrustHub.
Signature:
const createCustomerProfile = async(metadata, data, test);
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
metadata | Object | ✅ | Contains twilio_creds: {sid, token} |
data | Object | ✅ | Bundle data: {friendlyName, email, policySid, statusCallback} |
test | Boolean | ❌ | If true, skips database update (for testing) |
data Object:
| Field | Type | Required | Description |
|---|---|---|---|
friendlyName | String | ✅ | Display name for bundle |
email | String | ✅ | Contact email |
policySid | String | ✅ | Policy SID (Starter or Secondary) |
statusCallback | String | ❌ | Webhook URL for status updates |
Returns:
Promise<{
sid: string; // Bundle SID (BU...)
accountSid: string;
policySid: string;
friendlyName: string;
email: string;
status: string; // "draft", "pending-review", "in-review", "twilio-approved"
validUntil: Date;
dateCreated: Date;
dateUpdated: Date;
}>;
Business Logic:
- Create Customer Profile Bundle
- Uses TrustHub API
- Store in Database (unless test mode)
- Saves to
twilio_account.a2p.customer_profile_bundle
- Saves to
Example Usage:
// Create customer profile bundle
const bundle = await a2pProvider.createCustomerProfile(
{
twilio_creds: { sid: accountSID, token: authToken },
},
{
friendlyName: 'DashClicks Business Profile',
email: 'support@dashclicks.com',
policySid: 'RN806dd6cd175f314e1f96a9727ee271f4', // Starter policy
statusCallback: 'https://api.dashclicks.com/webhooks/a2p-status',
},
false, // Production mode
);
console.log('Bundle SID:', bundle.sid);
console.log('Status:', bundle.status); // draft
createEndUserObject()
Create an end-user object (business or individual information).
Signature:
const createEndUserObject = async(metadata, data, test);
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
metadata | Object | ✅ | Contains twilio_creds: {sid, token} |
data | Object | ✅ | End user data: {attributes, friendlyName, type} |
test | Boolean | ❌ | If true, skips database update |
data Object:
| Field | Type | Required | Description |
|---|---|---|---|
friendlyName | String | ✅ | Display name |
type | String | ✅ | End user type: 'customer_profile_business_information' or 'authorized_representative_1' |
attributes | Object | ✅ | Business/individual details |
attributes Object (Business):
{
business_name: 'DashClicks LLC',
business_type: 'llc', // llc, corporation, partnership, etc.
business_registration_number: '12345',
business_identity: 'direct_customer', // direct_customer or isvreseller
social_media_profile_urls: ['https://twitter.com/dashclicks'],
website_url: 'https://dashclicks.com',
business_regions_of_operation: 'US',
phone_number: '+12125551234',
email: 'business@dashclicks.com',
business_registration_identifier: 'ein', // ein or tax_id
business_identity_number: '12-3456789'
}
Returns:
Promise<{
sid: string; // End user SID (IT...)
accountSid: string;
friendlyName: string;
type: string;
attributes: Object;
dateCreated: Date;
dateUpdated: Date;
}>;
Business Logic:
- Create End User via TrustHub
- Push to Database Array (unless test mode)
- Adds to
twilio_account.a2p.end_usersarray
- Adds to
Example Usage:
// Create business end-user
const endUser = await a2pProvider.createEndUserObject(
{
twilio_creds: { sid: accountSID, token: authToken },
},
{
friendlyName: 'DashClicks Business Info',
type: 'customer_profile_business_information',
attributes: {
business_name: 'DashClicks LLC',
business_type: 'llc',
business_registration_number: '12345',
business_identity: 'direct_customer',
website_url: 'https://dashclicks.com',
business_regions_of_operation: 'US',
phone_number: '+12125551234',
email: 'support@dashclicks.com',
business_registration_identifier: 'ein',
business_identity_number: '12-3456789',
},
},
false,
);
console.log('End User SID:', endUser.sid);
updateEndUserObject()
Update an existing end-user object.
Signature:
const updateEndUserObject = async(metadata, id, data, test);
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
metadata | Object | ✅ | Contains twilio_creds: {sid, token} |
id | String | ✅ | End user SID (IT...) |
data | Object | ✅ | Updated data |
test | Boolean | ❌ | If true, skips database update |
Example Usage:
// Update business information
const updated = await a2pProvider.updateEndUserObject(
metadata,
'IT1234567890abcdef',
{
attributes: {
website_url: 'https://new-domain.com',
phone_number: '+12125559876',
},
},
false,
);
deleteEndUserObject()
Delete an end-user object.
Signature:
const deleteEndUserObject = async(metadata, id);
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
metadata | Object | ✅ | Contains twilio_creds: {sid, token} |
id | String | ✅ | End user SID to delete |
Example Usage:
await a2pProvider.deleteEndUserObject(metadata, 'IT1234567890abcdef');
createAddressObject()
Create an address object for customer profile.
Signature:
const createAddressObject = async(metadata, data, test);
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
metadata | Object | ✅ | Contains twilio_creds: {sid, token} |
data | Object | ✅ | Address data |
test | Boolean | ❌ | If true, skips database update |
data Object:
{
customerName: 'DashClicks LLC',
street: '123 Main Street',
streetSecondary: 'Suite 100', // Optional
city: 'New York',
region: 'NY',
postalCode: '10001',
isoCountry: 'US'
}
Returns:
Promise<{
sid: string; // Address SID (AD...)
accountSid: string;
customerName: string;
street: string;
city: string;
region: string;
postalCode: string;
isoCountry: string;
dateCreated: Date;
dateUpdated: Date;
}>;
Business Logic:
- Create Address via Twilio Addresses API
- Store in Database (unless test mode)
- Saves to
twilio_account.a2p.customer_profile_address
- Saves to
Example Usage:
// Create business address
const address = await a2pProvider.createAddressObject(
{
twilio_creds: { sid: accountSID, token: authToken },
},
{
customerName: 'DashClicks LLC',
street: '123 Main Street',
streetSecondary: 'Suite 100',
city: 'New York',
region: 'NY',
postalCode: '10001',
isoCountry: 'US',
},
false,
);
console.log('Address SID:', address.sid);
updateAddressObject()
Update an existing address object.
Signature:
const updateAddressObject = async(metadata, id, data, test);
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
metadata | Object | ✅ | Contains twilio_creds: {sid, token} |
id | String | ✅ | Address SID (AD...) |
data | Object | ✅ | Updated address data |
test | Boolean | ❌ | If true, skips database update |
Example Usage:
// Update address
const updated = await a2pProvider.updateAddressObject(
metadata,
'AD1234567890abcdef',
{
street: '456 New Street',
postalCode: '10002',
},
false,
);
💡 Integration Examples
Complete A2P Registration Flow
async function registerA2PBrand(accountId) {
const account = await Account.findById(accountId);
const metadata = {
twilio_creds: {
sid: account.twilio_account.sid,
token: account.twilio_account.authToken,
},
type: 'starter',
};
// Step 1: Create address
const address = await a2pProvider.createAddressObject(
metadata,
{
customerName: account.businessName,
street: account.address.street,
city: account.address.city,
region: account.address.state,
postalCode: account.address.zip,
isoCountry: 'US',
},
false,
);
// Step 2: Create end user (business info)
const endUser = await a2pProvider.createEndUserObject(
metadata,
{
friendlyName: `${account.businessName} - Business Info`,
type: 'customer_profile_business_information',
attributes: {
business_name: account.businessName,
business_type: account.businessType || 'llc',
business_registration_number: account.registrationNumber,
business_identity: 'direct_customer',
website_url: account.website,
business_regions_of_operation: 'US',
phone_number: account.phone,
email: account.email,
business_registration_identifier: 'ein',
business_identity_number: account.ein,
},
},
false,
);
// Step 3: Create customer profile bundle
const bundle = await a2pProvider.createCustomerProfile(
metadata,
{
friendlyName: `${account.businessName} A2P Bundle`,
email: account.email,
policySid: 'RN806dd6cd175f314e1f96a9727ee271f4', // Starter policy
statusCallback: `${process.env.API_URL}/webhooks/a2p-status/${accountId}`,
},
false,
);
// Step 4: Save registration data
await a2pProvider.saveRegistrationSubmission(metadata, {
addressSid: address.sid,
endUserSid: endUser.sid,
bundleSid: bundle.sid,
registeredAt: new Date(),
status: bundle.status,
});
return {
address,
endUser,
bundle,
};
}
Check A2P Registration Status
async function checkA2PStatus(accountId) {
const account = await Account.findById(accountId);
const a2p = account?.twilio_account?.a2p;
if (!a2p?.customer_profile_bundle) {
return {
registered: false,
status: 'not_started',
message: 'A2P registration not started',
};
}
return {
registered: a2p.paid || false,
status: a2p.customer_profile_bundle.status,
bundleSid: a2p.customer_profile_bundle.sid,
type: a2p.type,
hasAddress: !!a2p.customer_profile_address,
hasEndUsers: a2p.end_users?.length > 0,
submittedData: a2p.submission_data,
};
}
A2P Status Webhook Handler
// Handle status callback from Twilio
router.post('/webhooks/a2p-status/:accountId', async (req, res) => {
const { accountId } = req.params;
const { BundleSid, Status } = req.body;
console.log(`A2P Bundle ${BundleSid} status: ${Status}`);
// Update bundle status in database
await Account.updateOne(
{
_id: accountId,
'twilio_account.a2p.customer_profile_bundle.sid': BundleSid,
},
{
$set: {
'twilio_account.a2p.customer_profile_bundle.status': Status,
},
},
);
// Handle status changes
if (Status === 'twilio-approved') {
// A2P approved - can now send high-volume SMS
await notifyA2PApproved(accountId);
} else if (Status === 'twilio-rejected') {
// A2P rejected - notify user
await notifyA2PRejected(accountId, req.body.RejectionReason);
}
res.sendStatus(200);
});
🚨 Error Handling
Common Errors
1. Missing Required Fields
Error: Required field 'business_name' is missing
- Cause: Incomplete end user attributes
- Solution: Provide all required business information
2. Invalid EIN Format
Error: Invalid business_identity_number format
- Cause: EIN not in XX-XXXXXXX format
- Solution: Format EIN as 12-3456789
3. Bundle Already Exists
Error: A bundle already exists for this policy
- Cause: Attempting to create duplicate bundle
- Solution: Update existing bundle or delete old one
🔗 Related Documentation
- Twilio Integration Overview - Main Twilio integration
- Regulatory Compliance - General compliance
- Address Provider - Address management
- SMS Provider - SMS with A2P registration
External Resources: