A2P SDK
Guides

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 });
}

Next Steps

On this page