Documentation

Delivery tracking

Provider webhooks update per-recipient delivery rows; bounces and complaints update the contact and feed CRM activities.

Once a broadcast leaves AppEngine, the providers (SES, SendGrid, Mailgun, Resend, Twilio, FCM, APNs) take over. Each one POSTs status updates back via webhooks, and AppEngine maps them onto the broadcast_delivery rows so the campaign report is always current.

What gets tracked

Per recipient, per channel, the pipeline writes one broadcast_delivery row at send time and updates it as the provider reports back:

{
  broadcastId,
  recipient: '[email protected]',
  contactId: 'contact-...',
  channel: 'email' | 'sms' | 'whatsapp' | 'push',
  status: 'queued' | 'sent' | 'delivered' | 'opened' | 'clicked'
        | 'bounced' | 'complained' | 'failed' | 'unsubscribed',
  providerMessageId: 'sg-msg-...',
  events: [
    { type: 'sent', at, providerEvent },
    { type: 'delivered', at },
    { type: 'opened', at, ip, userAgent },
    { type: 'clicked', at, url },
    ...
  ],
  errorReason?: string,
  cost?: { value: number, unit: string }
}

status is the latest milestone; events[] is the full chronology. Reports usually filter by status; analytics drill into events.

Email webhooks

Each provider has its own POST endpoint at /connect/webhook/{vendor}/{serviceId?}:

ProviderPath
SendGrid/connect/webhook/sendgrid
Mailgun/connect/webhook/mailgun
AWS SES/connect/webhook/ses/{orgId}
Resend/connect/webhook/resend

Configure the URL in the provider dashboard once; AppEngine handles signature verification and event mapping.

Event types

The mapping into AppEngine's normalized events[] array:

Provider eventNormalized type
processed, sent, acceptedsent
delivered, deliverydelivered
openopened
clickclicked
bounce (hard)bounced
bounce (soft)event recorded, status not changed
dropped, failed, rejectedfailed
spamreport, complaintcomplained
unsubscribe, group_unsubscribeunsubscribed

SMS / WhatsApp webhooks

Twilio POSTs status callbacks to:

POST/connect/webhook/twilio/statusNo auth

Statuses: queued | sending | sent | delivered | undelivered | failed. The handler resolves the message via MessageSid, finds the matching broadcast_delivery row, and updates it.

For WhatsApp, additional read-receipt events (read, received) are mapped into the same row's events[] so you can see when the recipient opened the message.

Push webhooks

FCM and APNs don't have rich webhooks. Status is read at send time:

  • Successful sends → status: 'sent'
  • messaging/registration-token-not-registeredstatus: 'bounced', push token marked inactive
  • Other errors → status: 'failed' with errorReason

Open and click tracking for push relies on the app reporting back through POST /repository/create/analytics_event when the user taps the notification.

Bounce handling

When a row goes bounced, the pipeline:

  1. Marks the recipient contact's email (or phone) as bounced so future broadcasts skip it
  2. Increments the sender account's bounceRate
  3. Records a CRM activity ("Email bounced", with reason)
  4. If bounceRate > 3%, flags the sender account so the rotation strategy avoids it

Hard bounces are permanent (mailbox doesn't exist, blocked). Soft bounces (mailbox full, rate limited) record an event but don't change status — the next send to the same recipient is allowed.

Complaint handling

A complained row is the hardest signal a recipient sends — they hit "Spam" in their mail client. The pipeline:

  1. Marks the contact as optedOut: true on every channel
  2. Records a CRM activity ("Spam complaint")
  3. Increments the sender account's complaintRate
  4. Adds the contact to the org's suppression list — no future broadcasts can be sent to them on any channel without an explicit override

Unsubscribe handling

Email broadcasts auto-include {{unsubscribeUrl}}. Clicking it:

  1. Records a broadcast_delivery event of type unsubscribed
  2. Removes the contact from the broadcast's recipient list
  3. Sets optedOut: true on the contact for the relevant channel
  4. Surfaces an unsubscribe activity in CRM

For SMS, Twilio handles STOP / UNSUBSCRIBE / CANCEL natively. AppEngine syncs the opt-out into the contact record so subsequent automations skip the contact too.

Reading status

GET/broadcast/{id}/reportJWT

Per-recipient log with optional filter:

curl "https://appengine.appmint.io/broadcast/{id}/report?status=bounced&pageSize=200" \
  -H "orgid: my-org" -H "Authorization: Bearer <jwt>"
GET/broadcast/activityJWT

Cross-broadcast activity feed — useful for an admin dashboard. Filters: startDate, endDate, event, recipient, limit.

GET/broadcast/activity/{messageId}JWT

Detail view for a single send — the full provider event log.

GET/broadcast/statsJWT

Aggregated counters across the org. Supports aggregatedBy: 'day' | 'week' | 'month' for time-series charts.

GET/broadcast/stats/summaryJWT

The 30-day summary card for the dashboard.

Re-sending failed

There is no built-in "resend failed" action. Pull the bounced or failed rows via /broadcast/{id}/report?status=failed, generate a manual recipient list, and create a new broadcast targeting that list. Build it once as an automation if you need it routinely.

Don't import bounced contacts back

A typical mistake: re-importing a contact CSV that re-introduces previously-bounced addresses. The contact-level bounced flag stops them from getting messaged, but only if you preserve it on import. Match by email and skip rather than overwrite.