Webhooks
Webhooks
The SDK provides parseWebhookEvent() to parse Twilio status callback webhooks into typed event objects.
Setup
Configure statusCallback URLs when creating profiles, trust products, brands, and campaigns:
const result = await flows.directStandard.step1_createCustomerProfile(client, {
...input,
statusCallback: 'https://example.com/webhooks/a2p',
});Parsing Webhooks
Use parseWebhookEvent() to parse the webhook body:
import { parseWebhookEvent } from '@warp-message/a2p-sdk';
export async function POST(req: Request) {
const body = await req.json();
const event = parseWebhookEvent(body);
switch (event.type) {
case 'customer_profile.status_changed':
console.log('Profile', event.sid, 'is now', event.status);
if (event.failureReason) {
console.error('Failure:', event.failureReason);
}
break;
case 'brand.status_changed':
console.log('Brand', event.sid, 'is now', event.status);
if (event.trustScore) {
console.log('Trust score:', event.trustScore);
}
break;
case 'campaign.status_changed':
console.log('Campaign', event.sid, 'is now', event.status);
console.log('Messaging Service:', event.messagingServiceSid);
break;
case 'unknown':
console.warn('Unknown webhook:', event.rawBody);
break;
}
return new Response('OK', { status: 200 });
}Event Types
The SDK returns a discriminated union of event types:
CustomerProfileEvent
{
type: 'customer_profile.status_changed';
sid: string; // Starts with BU
status: ProfileStatus;
failureReason?: string;
}TrustProductEvent
{
type: 'trust_product.status_changed';
sid: string; // Starts with BU
status: ProfileStatus;
failureReason?: string;
}ProfileOrTrustProductEvent
{
type: 'profile_or_trust_product.status_changed';
sid: string; // BU prefix (ambiguous)
status: ProfileStatus;
failureReason?: string;
}Returned when the SDK cannot disambiguate between Customer Profile and Trust Product (both use BU SID prefix). Check your internal state to determine which resource this is.
BrandEvent
{
type: 'brand.status_changed';
sid: string; // Starts with BN
status: BrandStatus;
trustScore?: number; // 0-100, only when approved
failureReason?: string;
}CampaignEvent
{
type: 'campaign.status_changed';
sid: string; // Starts with QE
messagingServiceSid: string;
status: CampaignStatus;
failureReason?: string;
}UnknownEvent
{
type: 'unknown';
rawBody: Record<string, unknown>;
}Returned when the webhook doesn't match any recognized format. Use rawBody for debugging.
Gotchas
BU SID Ambiguity
Both Customer Profiles and Trust Products use the BU SID prefix. The SDK uses heuristics to disambiguate, but if it can't determine the type, it returns profile_or_trust_product.status_changed.
Solution: Track resource types in your database by SID so you know which type each BU SID is.
Dual Field Casing
Twilio webhooks can use either PascalCase (ResourceSid, Status) or snake_case (resource_sid, status). The SDK checks both formats automatically.
Webhook Reliability
Webhooks may be delivered out of order, duplicated, or missed entirely. Always:
- Use idempotency keys or deduplicate by SID + status
- Fall back to polling with
refreshRegistrationState()if webhooks don't arrive - Store webhook timestamps to detect stale updates
Security
Validate webhook signatures using Twilio's signature validation:
import twilio from 'twilio';
const signature = req.headers.get('x-twilio-signature');
const url = 'https://example.com/webhooks/a2p';
const params = await req.json();
const isValid = twilio.validateRequest(
process.env.TWILIO_AUTH_TOKEN!,
signature!,
url,
params
);
if (!isValid) {
return new Response('Invalid signature', { status: 403 });
}