Skip to content

Tracked Sending API

Added in Epic HEKA-240 (v0.2.0)

Tracked sending endpoints create MessageRecord entries with wamid linkage for every template sent, enabling delivery status tracking via Meta webhooks.

Individual tracked send

Send a single template to one contact within a campaign context.

POST /campaigns/{campaign_id}/send-template

Request

{
  "wa_id": "5219671234567",
  "template_name": "amate_recordatorio_hoy_v2",
  "language": "es_MX",
  "parameters": ["4:00 pm", "Luna"],
  "request_id": "req-uuid-unique",
  "header_location": {
    "lat": 16.7370359,
    "lng": -92.6376543,
    "name": "Escuela Ámate",
    "address": "Hermilio López 14-A"
  }
}

Response

{
  "wamid": "wamid.HBgNNTI...",
  "status": "sent",
  "request_id": "req-uuid-unique"
}

Features

  • Idempotency — duplicate request_id returns status: "duplicate" without re-sending
  • Rate limiting — respects WhatsAppRateLimiter (80 msg/s default)
  • Daily limit — enforces per-phone-number daily template send cap (1,000 default)
  • Header variants — supports header_location, header_media, and button_url (mutually exclusive with location)
  • Error mapping — Meta Graph API errors return HTTP 502 with SenderError details

Status values

Status Meaning
sent Template sent, wamid assigned
duplicate Same request_id already sent, returns original wamid

Batch tracked send

Send personalized templates to multiple contacts in one request.

POST /campaigns/{campaign_id}/send-template/batch

Request

{
  "template_name": "amate_recordatorio_hoy_v2",
  "language": "es_MX",
  "header_location": {
    "lat": 16.7370359,
    "lng": -92.6376543,
    "name": "Escuela Ámate",
    "address": "Hermilio López 14-A"
  },
  "dry_run": false,
  "contacts": [
    {
      "wa_id": "5219671234567",
      "parameters": ["4:00 pm", "Luna"],
      "request_id": "batch-001-luna"
    },
    {
      "wa_id": "5219679876543",
      "parameters": ["6:00 pm", "Ana"],
      "request_id": "batch-001-ana"
    }
  ]
}

Response

{
  "results": [
    {
      "wa_id": "5219671234567",
      "request_id": "batch-001-luna",
      "status": "sent",
      "wamid": "wamid.HBgNNTI..."
    },
    {
      "wa_id": "5219679876543",
      "request_id": "batch-001-ana",
      "status": "sent",
      "wamid": "wamid.HBgNNTI..."
    }
  ],
  "summary": {
    "total": 2,
    "sent": 2,
    "failed": 0,
    "skipped": 0
  }
}

Per-contact status values

Status Meaning
sent Template sent successfully
duplicate Same request_id already sent
failed Meta Graph API returned an error
rate_limited Rate limiter rejected the send
daily_limit_reached Daily template send cap exceeded
dry_run Preview only, nothing sent

Features

  • Per-contact personalization — each contact gets unique parameters
  • Error isolation — one contact's failure doesn't affect others
  • Dry run — set dry_run: true to preview without sending
  • Circuit breaker — once daily limit is reached, remaining contacts are immediately marked daily_limit_reached without additional DB queries

Delivery tracking

Status webhooks from Meta automatically correlate with tracked sends via wamid. The status lifecycle is:

sent → delivered → read
  └──→ failed (with error_code)

Query delivery statuses:

statuses = repo.get_statuses_by_wamid("wamid.HBgNNTI...")
# Returns: [StatusRecord(status="sent", ...), StatusRecord(status="delivered", ...)]

The message_statuses table has an index on wamid for efficient lookups.


LOCATION header

Templates with LOCATION headers include a map pin in the message. The header is mutually exclusive with IMAGE/VIDEO headers.

{
  "header_location": {
    "lat": 16.7370359,
    "lng": -92.6376543,
    "name": "Escuela Ámate",
    "address": "Hermilio López 14-A"
  }
}

Mutual exclusivity

header_location and header_media cannot be used in the same request. The sender raises ValueError at send time if both are provided.