WhatsApp Business API

WhatsApp Business API

Send an approved WhatsApp template to a customer with one HTTPS request: media headers, dynamic buttons, contact tags, idempotency and signed delivery status callbacks.

Base URL
https://app.agentive.co.in/api/v1
Format
JSON over HTTPS
Auth
Bearer campaign token

Introduction

The WhatsApp Business API sends an approved WhatsApp template to one recipient per request, from your own code. Use it for the messages your system already knows when to send: order and shipping updates, appointment reminders, payment confirmations, renewal notices.

Every request goes through an API campaign that you create once in the Agentive Chat dashboard. The campaign fixes the template, the sending number and which variables come from the request, so your code only sends the values that change. Broadcasts to a list and campaigns that fire on Agentive events stay in the dashboard; this API is for sends that your own system triggers.

If your Agentive Voice account is connected, call events become triggers too, so a missed call can send a template with no code at all. See When Agentive Voice is connected.

Billing
Each delivered message is billed in INR at the rate for its WhatsApp conversation category (marketing, utility or authentication) on your rate card, and debited from your Agentive wallet. API requests are free, and a request that is rejected before WhatsApp accepts it is never billed. The category and whether the message was billable are echoed in the pricing object of your status callbacks.

Before you start

  • A WhatsApp number connected to an Agentive agent (Agent > WhatsApp).
  • A template approved by WhatsApp, synced under Agent > WhatsApp > Templates.
  • An API campaign that is live, with its token copied. The quick start walks through it.

Platform conventions

Agentive ships two API families: the Voice APIs (Voice Broadcast, AI Voice Agent, Voice OTP and telephony webhooks) and the WhatsApp Business API. They share a brand, a dashboard account and a set of habits, but they are separate products and they do not agree on everything. This section is the one place that says what is common and, where they differ, exactly how.

Base URLs

Voice APIsWhatsApp Business API
Base URLhttps://voice.agentive.co.in/api/public/v1https://app.agentive.co.in/api/v1
CoversVoice Broadcast, AI Voice Agent, Voice OTP and the endpoints webhook events point back at. All of them share one base, one credential pair and one envelope.Template sends for one API campaign. Issued and managed from the Chat dashboard.
TransportHTTPS only, JSON in and JSON out.HTTPS only, JSON in and JSON out.

Authentication

Voice APIsWhatsApp Business API
CredentialOne publishable key (pk_live_...) and one secret (sk_live_...) per account.One token (agcamp_...) per API campaign, so a leaked token can only send that campaign.
How to send itx-api-key and x-api-secret headers, or HTTP Basic with the publishable key as the username and the secret as the password.Authorization: Bearer <token>.
ScopeAccount-wide. Every endpoint on the Voice base accepts it.Campaign-wide. The template and number are fixed on the campaign.
Treat asBoth keys are confidential. The publishable key names your account to anyone who holds it, so keep it with the secret, on your server only.A secret. Keep it on your server only.
RotationFrom the API Access tab of the Voice dashboard. The old secret stops working at once.From the campaign page in the Chat dashboard. The old token stops working at once.

Error envelope

Voice APIsWhatsApp Business API
Shape{ status, code: number, error_code: string, error, details, unique_id, reference_id }{ error, code: string, details: object }
What to branch onerror_code. The numeric code mirrors the HTTP status and stays for compatibility.code, which is itself the machine string.
Human sentencedetails, repeated in error so a handler written for the WhatsApp shape reads a Voice error too.error.
Extra contextNamed fields beside the envelope (lines, lines_in_use, attempts_left, retry_after_sec, balance_inr).A details object (missing, retryAfter, meta, validation).

401 and 403

Voice APIsWhatsApp Business API
401Credentials missing, unknown, or rejected. The status never confirms whether a key exists.Token missing or wrong.
403Credentials accepted, but the account is not active or the product is not enabled.Token accepted, but the campaign is not live or the number is not connected.
404Not found, or not yours. A foreign id is never a 403, because that would confirm it exists.Not found, or not yours. Same rule.

Idempotency

Voice APIsWhatsApp Business API
KeyIdempotency-Key request header, 1 to 128 visible characters. A longer key is refused with HTTP 400.externalId in the body (the Idempotency-Key header is accepted too), up to 160 characters.
Window24 hours per account.The life of the campaign.
A repeatReplays the stored response. The one exception is a 5xx raised after the call was already handed to the network: that reply is stored as call_state_unknown with a sentence appended telling you to check the call before retrying. A 400 or 404 is stored for the 24 hours too, so once you fix the request or the dashboard setting, send it with a new key.HTTP 200 with duplicate: true and the earlier delivery's current status.
Replay markerIdempotent-Replayed: true, plus the original idempotency-replayed: true header.duplicate: true in the body.
Same key, different bodyHTTP 422 with idempotency_conflict.The first send stands; the second is reported as a duplicate.
Still in flightHTTP 409 with idempotency_in_progress and Retry-After.Not applicable.

Rate limits and headers

Voice APIsWhatsApp Business API
BudgetPer account: 120 reads a minute and 120 writes a minute, counted separately, so polling can never starve call placement.Per campaign token: 300 requests a minute over a sliding window.
Limit headersRateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, with X-RateLimit-* aliases of the same three values.None.
How long to waitRetry-After on every 429 and every 409, mirrored in the body as retry_after_sec.Retry-After on rate_limited; details.retryAfter (IST) on quiet_hours.
Capacity refusalHTTP 429 concurrency_limit when the account has no free line, with lines, lines_in_use and retry_after_sec in the body and a Retry-After of 5 seconds. Call-placing responses also carry X-Agentive-Lines and X-Agentive-Lines-In-Use.Not applicable; there are no lines to exhaust.

Webhook signing

Voice APIsWhatsApp Business API
Header to verifyX-Agentive-Signature-V2: t=<unix seconds>,v2=<hex>X-Agentive-Signature: <hex>, with no prefix.
What is signedHMAC-SHA256, with the signing secret, of t, a dot, then the exact raw body.HMAC-SHA256 of the exact raw body with the endpoint's signing secret.
FreshnessReject a t more than 300 seconds from your own clock. The timestamp is inside the signed material, so a captured delivery cannot be sent again later.Nothing time-bound is signed. De-duplicate on the event id for good, and accept deliveries over HTTPS only.
ComparisonConstant time, on bytes. Reject on any mismatch, and reject a delivery with no version 2 header.Constant time, on bytes. Reject on any mismatch.
Two different signatures
The Voice APIs sign the timestamp together with the body, and you verify only X-Agentive-Signature-V2. Voice deliveries also carry the older body-only X-Agentive-Signature for receivers built before version 2; do not accept it on its own, because it proves nothing about when a body was sent. The WhatsApp signature is the body alone with no prefix. A shared handler needs one verifier per product.

Webhook delivery headers

Voice APIsWhatsApp Business API
Event nameX-Agentive-EventX-Agentive-Event
Event idX-Agentive-Event-Id, the same value as id in the body. Shared by every endpoint that receives the event.X-Agentive-Delivery, the same value as id in the body. Despite the name it is the event id, not a per-endpoint delivery id.
Delivery idX-Agentive-Delivery-Id (dlv_...), one per event per endpoint, stable across every retry.None. X-Agentive-Delivery is the Chat product's name for the event id above.
Attempt numberX-Agentive-Attempt, 1-based, counting through the whole retry schedule.None.
Attempt timeX-Agentive-Timestamp, Unix seconds at the moment of this attempt, the same value as t in the version 2 signature. Check the age against the signed t, never against this header alone.X-Agentive-Timestamp, the same timestamp as the envelope.
User agentAgentive-Webhooks/2026-06-01Agentive-Webhooks/2026-05-08, the Chat payload version.

Webhook retries

Voice APIsWhatsApp Business API
AttemptsNine: three fast, then six spread out.Three.
Schedule0.5 s and 2 s between the first three, then 1 min, 5 min, 30 min, 2 h, 6 h and 12 h.0 s, 2 s and 8 s.
Total windowAbout 24 hours, after which the delivery is marked failed.About 10 seconds.
Timeout per attempt6 seconds.10 seconds.
What you seeThe dashboard lists recent deliveries per endpoint with the event, the latest result, the number of tries and the time in IST.The campaign's Deliveries tab shows the message and its current status.

Timestamps and amounts

Voice APIsWhatsApp Business API
Time zoneIndian Standard Time, ISO 8601 with a +05:30 offset. The field is named per product: created_at_ist on a call, expires_at_ist on a verification, timestamp_ist on a webhook envelope.timestamp on the webhook envelope is UTC ISO 8601. Schedules and quiet hours are set and reported in IST.
UTC alongsidecreated_at_utc on a call, and occurred_at (UTC) plus created (Unix seconds) on a webhook envelope.The envelope timestamp is already UTC.
MoneyIndian Rupees, always. balance_inr is a number, never a formatted string.Indian Rupees, always. Amounts appear in the dashboard, not in the send API.

Versioning and field case

Voice APIsWhatsApp Business API
API versionIn the path: /api/public/v1.In the path: /api/v1.
Payload versionapi_version on every webhook event. Currently 2026-06-01.apiVersion on every webhook event. Currently 2026-05-08.
CompatibilityFields are added, never removed or redefined. Ignore fields you do not recognise.Fields are added, never removed or redefined. Ignore fields you do not recognise.
Field casesnake_case throughout, request and response.camelCase throughout, request and response.

Security

  • Every credential is a secret. That includes the Voice publishable key, whatever its name suggests: treat it exactly like the secret. Keep all keys on your server, in an environment variable or a secrets store, never in a browser, a mobile app, a spreadsheet or source control.
  • Send them only to the API. Never forward a key or a secret to any other host, and do not let an HTTP client carry them across a redirect to another host.
  • Rotate at once if one leaks. Rotate the Voice pair from the API Access tab of the Voice dashboard and a WhatsApp token from its campaign page. The old value stops working immediately, so update your server in the same step.
  • HTTPS only. Write https:// in the base URL in your code. A request sent over plain HTTP has already exposed its credentials before any redirect can protect it.
  • No address allow-listing today. Requests are not restricted to your server's IP addresses, and webhooks are not sent from a fixed list of addresses. Authenticate every webhook by its signature, never by where it came from.
Report a vulnerability
If you find a security problem in the APIs, the webhooks or the dashboard, write to hello@agentive.co.in with the steps to reproduce it. Please do not test against accounts that are not yours. Our contact details are also published at /.well-known/security.txt.
Reading this as one platform
The habits that hold across both products: HTTPS only, JSON both ways, a stable machine code on every error, an idempotency key on every request a retry could repeat, HMAC-SHA256 on every webhook, Indian Rupees and Indian Standard Time. The habits that do not: the credential header, the envelope key names, the field case, the signature scheme and the retry schedule. Write one transport layer per product, then share everything above it.

Base URL

Base URL
https://app.agentive.co.in/api/v1

Every path on this page is relative to this base. HTTPS only, JSON in and JSON out. This is the Agentive Chat app; the voice APIs use a different base, listed on their own pages.

Authentication

Each API campaign has its own token, prefixed agcamp_. It is shown once when the campaign goes live and can be rotated at any time from the campaign page (Agent > WhatsApp > Campaigns > your campaign). A token only sends for the campaign it belongs to, so a leaked token can never reach another template or number.

Send it on every request in the Authorization header:

cURL
curl https://app.agentive.co.in/api/v1/whatsapp/campaigns/cmf3k9d2a0001x7q2m4n8p1r6/send \
  -H "Authorization: Bearer agcamp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "to": "+919876543210", "templateParams": ["Asha"] }'

Tokens are compared in constant time. A missing or wrong token returns HTTP 401 with one of two codes:

StatusCodeWhen
401missing_tokenNo Authorization header, or one that is not Bearer <token>.
401invalid_tokenThe token is unknown, was rotated, or belongs to a different campaign than the one in the URL.
JSON
{
  "error": "Send the campaign token as Authorization: Bearer <token>.",
  "code": "missing_token"
}
Keep the token on your server
Never ship a campaign token in a browser, a mobile app or a public repository. Store it as an environment variable and call the API from your backend. If a token leaks, rotate it from the campaign page: the old token stops working immediately and the new one is shown once.

Response format

Every response is JSON. A send that was accepted returns HTTP 200 with success: true:

200 OK
{
  "success": true,
  "duplicate": false,
  "status": "accepted",
  "messageId": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSNkY3QjE4RkY0RjA2NDU2QjAxAA==",
  "deliveryId": "cmf3kd8y10004x7q2h1k3v9pz",
  "contactId": "cmf1a2b3c0009x7q2d4e5f6g7",
  "warnings": []
}

Anything else returns the matching HTTP status (400, 401, 404, 409, 429 or 502) and one error shape:

400 Bad Request
{
  "error": "templateParams has 2 values but the template body needs 3.",
  "code": "missing_template_values",
  "details": { "missing": ["body_3"] }
}

Error fields

FieldTypeDescription
errorstring
A sentence you can show in a log or a support ticket.
codestring
A stable machine code. Branch on this, never on the text. The full list is under Errors.
detailsobject
Present when there is more to say: missing for missing_template_values, retryAfter for quiet_hours, meta for meta_rejected, the offending fields for validation.
Accepted is not delivered
HTTP 200 means WhatsApp accepted the message for delivery. Delivery, read and reply come later on the status callback and on the campaign's Deliveries tab.

Errors

Every error carries a stable code. The table lists all of them with what to do. Most 4xx codes are yours to fix before resending; the 429 codes are timing; the 502 codes come from WhatsApp and are safe to retry with the same externalId.

StatusCodeWhenWhat to do
400invalid_jsonThe body is not valid JSON, or the Content-Type is not application/json.Send a JSON object with the Content-Type header set. Checked before anything else, including the token.
400validationA field has the wrong type or exceeds its limit. details lists the offending fields.Fix the named fields against the request body table and resend.
400invalid_recipientto is not a number the API can resolve: too short, 11 digits, or a country code without a full number.Send E.164 (+919876543210) or a 10 digit Indian mobile number.
400missing_template_valuesA body, header or button variable the campaign expects from the request was not sent. details.missing names the keys, for example body_3.Send one templateParams entry per body variable and a value for every dynamic button.
400media_requiredThe template has a media header, the request has no media, and no header media is saved on the campaign.Send media.url or media.id, or save a default file on the campaign in the dashboard.
400whatsapp_not_connectedThe agent's WhatsApp number is disconnected or its access has expired.Reconnect the number under Agent > WhatsApp, then retry with the same externalId.
401missing_tokenNo Authorization header, or one that is not Bearer <token>.Send Authorization: Bearer agcamp_… on every request.
401invalid_tokenThe token is unknown, was rotated, or belongs to a different campaign than the one in the URL.Copy the current token from the campaign page and check the campaignId in the URL.
404campaign_not_liveThe campaign is a draft, paused, deleted, or is not an API campaign.Open the campaign in the dashboard and set it live.
404template_not_foundThe campaign's template was deleted or is no longer approved.Sync templates under Agent > WhatsApp > Templates and pick an approved one on the campaign.
409opted_outThe recipient has opted out of messages from this number.Do not retry. The delivery is recorded as skipped and nothing was sent.
429rate_limitedMore than 300 requests in a minute on this token. The Retry-After header says how many seconds to wait.Wait for Retry-After, then resend with the same externalId.
429frequency_capThe recipient has reached the marketing message limit for the last 24 hours.Do not retry today. The delivery is recorded as skipped and nothing was sent.
429quiet_hoursThe request arrived inside the campaign's quiet hours. details.retryAfter is an ISO 8601 timestamp (IST offset) of when sending resumes.Queue the send until retryAfter and resend with the same externalId.
502meta_rejectedWhatsApp rejected the message. details.meta carries the platform's code and message.Look up the code in the WhatsApp error reference. The idempotency key is released, so a corrected request can reuse the same externalId.
502send_failedWhatsApp could not be reached, or timed out before accepting the message.Retry after a short pause with the same externalId; the key is released on this failure too.

Example error bodies

502 meta_rejected
{
  "error": "(#131026) Message undeliverable",
  "code": "meta_rejected",
  "details": {
    "meta": { "code": 131026, "message": "Message undeliverable" }
  }
}
429 quiet_hours
{
  "error": "Quiet hours are on. Sending resumes at 6 Sep 2026, 9:00 am IST.",
  "code": "quiet_hours",
  "details": { "retryAfter": "2026-09-06T09:00:00+05:30" }
}

Rate limits

Requests are limited per token to 300 requests per minute over a sliding window. One campaign's traffic never throttles another. Over the limit you receive HTTP 429 with rate_limited and a Retry-After header in seconds:

HTTP
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json

{
  "error": "Too many requests for this token. Retry after 12 seconds.",
  "code": "rate_limited"
}

Two more 429 codes are about the recipient rather than your request rate, and they are not retried the same way:

  • frequency_cap: WhatsApp limits how many marketing messages one person can receive in 24 hours. The delivery is recorded as skipped, nothing was sent, and there is nothing to retry today.
  • quiet_hours: the campaign has quiet hours and the request arrived inside them. Nothing was sent. Queue it until details.retryAfter (IST) and resend with the same externalId.
Sending in volume
Push sends through a queue with a handful of workers, back off on Retry-After, and never retry opted_out or frequency_cap. For a one-off list of thousands, a broadcast from the dashboard with a CSV is simpler than looping over the API.

Endpoints

One endpoint sends a message. The older trigger URL still works and shares the same handler, so both accept the same body and return the same responses.

Send a message

POST/whatsapp/campaigns/{campaignId}/send

Sends the campaign's template to one recipient. Creates or matches the contact by phone number, applies tags and attributes, checks opt-out and caps, then hands the message to WhatsApp. Returns as soon as WhatsApp accepts it.

Headers

HeaderTypeRequiredDescription
AuthorizationstringRequired
Bearer followed by the campaign token. See Authentication.
Example: Bearer agcamp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-TypestringRequired
Must be application/json. Any other body type is rejected with invalid_json.
Example: application/json
Idempotency-KeystringOptional
Optional. Same meaning as externalId in the body; send one or the other. See Idempotency.
Example: order-1142-shipped

Path parameters

ParameterTypeRequiredDescription
campaignIdstringRequired
The API campaign id, copied from the campaign page in the dashboard. The token in the Authorization header must belong to this campaign.
Example: cmf3k9d2a0001x7q2m4n8p1r6

Request body

FieldTypeRequiredDescription
tostringRequired
The recipient. E.164 with the country code, or a 10 digit Indian mobile number (a leading 0, 91 or +91 is understood). Alias: destination. Anything ambiguous, such as 11 digits, is refused with invalid_recipient rather than guessed.
Example: +919876543210
namestringOptional
Display name saved on the contact. Up to 120 characters. Alias: userName.
Example: Asha Verma
emailstringOptional
Email saved on the contact.
Example: asha@example.in
templateParamsstring[]Optional
Positional body variables in order: templateParams[0] fills {{1}}. The count must match the variables the campaign expects from the request, or the send fails with missing_template_values. See Template variables.
Example: ["Asha", "ORD-1142", "Friday, 12 September"]
headerTextstringOptional
The value for a text header variable, when the template header has one.
Example: Order ORD-1142
mediaobjectOptional
The file for an image, video or document header: { "url", "filename" } with a public https URL (up to 2048 characters, filename for documents), or { "id" } with a media id already uploaded to your number. See Media requirements.
Example: { "url": "https://cdn.example.in/invoices/ORD-1142.pdf", "filename": "Invoice.pdf" }
buttonsobject | string[]Optional
Dynamic button values keyed by button index ("0" is the first button). The template decides the meaning: a URL button takes its dynamic suffix or the full URL, a COPY_CODE button the coupon code, a QUICK_REPLY button the payload you get back when it is tapped. A positional array is accepted. See Buttons.
Example: { "0": "orders/ORD-1142" }
valuesobjectOptional
Advanced keyed overrides: body_N, header_N, button_i_url_N. A key here wins over the positional fields. See Keyed overrides.
Example: { "body_1": "Asha", "button_0_url_1": "orders/ORD-1142" }
tagsstring[]Optional
Tags added to the contact. Up to 30, each up to 40 characters, created automatically if new. See Tags and attributes.
Example: ["customer", "shipped"]
attributesobjectOptional
Custom fields merged onto the contact. Keys become snake_case, values are stored as strings (up to 200 characters), up to 40 keys.
Example: { "city": "Pune", "orderValue": "2499" }
sourcestringOptional
Where the contact came from, recorded when the contact is new. Up to 80 characters. Defaults to API campaign.
Example: shopify
externalIdstringOptional
Your idempotency key for this send, up to 160 characters, unique per campaign. The same key never sends twice. Alias: idempotencyKey. See Idempotency.
Example: order-1142-shipped
callbackDatastringOptional
Any string up to 512 characters. Stored with the delivery and echoed unchanged on every whatsapp.message.status callback, so you can route the status without a lookup.
Example: order=ORD-1142

Request

curl https://app.agentive.co.in/api/v1/whatsapp/campaigns/cmf3k9d2a0001x7q2m4n8p1r6/send \
  -H "Authorization: Bearer agcamp_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+919876543210",
    "name": "Asha Verma",
    "templateParams": ["Asha", "ORD-1142", "Friday, 12 September"],
    "buttons": { "0": "orders/ORD-1142" },
    "tags": ["customer", "shipped"],
    "attributes": { "city": "Pune", "orderValue": "2499" },
    "source": "shopify",
    "externalId": "order-1142-shipped",
    "callbackData": "order=ORD-1142"
  }'

Response (200)

JSON
{
  "success": true,
  "duplicate": false,
  "status": "accepted",
  "messageId": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSNkY3QjE4RkY0RjA2NDU2QjAxAA==",
  "deliveryId": "cmf3kd8y10004x7q2h1k3v9pz",
  "contactId": "cmf1a2b3c0009x7q2d4e5f6g7",
  "warnings": []
}

Response fields

FieldTypeDescription
successboolean
Always true on HTTP 200. Errors use the error shape instead.
duplicateboolean
true when the externalId was already used and nothing new was sent. The rest of the response then describes the earlier delivery.
statusstring
accepted for a fresh send. On a duplicate, the earlier delivery's current status: accepted, sent, delivered, read, replied, clicked, failed or skipped.
messageIdstring | null
The WhatsApp message id (wamid.…). The same id appears on every status callback for this message.
deliveryIdstring
Agentive's id for this delivery. Shown on the campaign's Deliveries tab and on status callbacks.
contactIdstring
The contact the message went to, created or matched by phone number.
warningsstring[]
Non-fatal notices, for example a tag that was trimmed or an override key the template does not use. Empty when there is nothing to say.

Errors

StatusCodeWhen
400invalid_jsonThe body is not valid JSON, or the Content-Type is not application/json.
400validationA field has the wrong type or exceeds its limit. details lists the offending fields.
400invalid_recipientto is not a number the API can resolve: too short, 11 digits, or a country code without a full number.
400missing_template_valuesA body, header or button variable the campaign expects from the request was not sent. details.missing names the keys, for example body_3.
400media_requiredThe template has a media header, the request has no media, and no header media is saved on the campaign.
400whatsapp_not_connectedThe agent's WhatsApp number is disconnected or its access has expired.
401missing_tokenNo Authorization header, or one that is not Bearer <token>.
401invalid_tokenThe token is unknown, was rotated, or belongs to a different campaign than the one in the URL.
404campaign_not_liveThe campaign is a draft, paused, deleted, or is not an API campaign.
404template_not_foundThe campaign's template was deleted or is no longer approved.
409opted_outThe recipient has opted out of messages from this number.
429rate_limitedMore than 300 requests in a minute on this token. The Retry-After header says how many seconds to wait.
429frequency_capThe recipient has reached the marketing message limit for the last 24 hours.
429quiet_hoursThe request arrived inside the campaign's quiet hours. details.retryAfter is an ISO 8601 timestamp (IST offset) of when sending resumes.
502meta_rejectedWhatsApp rejected the message. details.meta carries the platform's code and message.
502send_failedWhatsApp could not be reached, or timed out before accepting the message.

A repeat of an externalId that already sent returns HTTP 200 with duplicate: true and the earlier delivery's current status. Nothing is sent again.

200 OK (duplicate)
{
  "success": true,
  "duplicate": true,
  "status": "delivered",
  "messageId": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSNkY3QjE4RkY0RjA2NDU2QjAxAA==",
  "deliveryId": "cmf3kd8y10004x7q2h1k3v9pz",
  "contactId": "cmf1a2b3c0009x7q2d4e5f6g7",
  "warnings": []
}

Legacy trigger endpoint

POST/api/chatbots/{chatbotId}/whatsapp/campaigns/{campaignId}/trigger

Legacy, still supported. This path lives on https://app.agentive.co.in directly, not under the /api/v1 base, and needs the agent's chatbotId in the URL as well as the campaign id. Same headers, same body, same responses and errors as Send a message.

Path parameters

ParameterTypeRequiredDescription
chatbotIdstringRequired
The id of the agent that owns the campaign, from the agent's URL in the dashboard.
Example: cmk2p4x1a0000x7q2b9c3d4e5
campaignIdstringRequired
The API campaign id, copied from the campaign page in the dashboard. The token in the Authorization header must belong to this campaign.
Example: cmf3k9d2a0001x7q2m4n8p1r6

Request

cURL
curl https://app.agentive.co.in/api/chatbots/cmk2p4x1a0000x7q2b9c3d4e5/whatsapp/campaigns/cmf3k9d2a0001x7q2m4n8p1r6/trigger \
  -H "Authorization: Bearer agcamp_..." \
  -H "Content-Type: application/json" \
  -d '{ "destination": "9876543210", "userName": "Asha Verma", "templateParams": ["Asha", "ORD-1142", "Friday, 12 September"] }'
Use the canonical endpoint for new work
Existing integrations do not need to change. New code should call /whatsapp/campaigns/{campaignId}/send on the base URL: it is shorter, does not expose the agent id, and is the path this reference documents.

Template variables

A WhatsApp template has numbered placeholders, {{1}} to {{n}}, in its body, sometimes in its header, and in any dynamic button. When you create the API campaign you decide, per variable, whether it has a fixed value or comes from the request. Only the request-mapped ones need to be sent.

Template: order_shipped (en)
Header:  DOCUMENT
Body:    Hi {{1}}, your order {{2}} has shipped and should reach you by {{3}}.
Footer:  Reply STOP to opt out.
Buttons: [0] URL "Track order"  https://shop.example.in/{{1}}
         [1] QUICK_REPLY "Need help?"

The request that fills every variable of that template:

Request body
{
  "to": "+919876543210",
  "templateParams": ["Asha", "ORD-1142", "Friday, 12 September"],
  "media": {
    "url": "https://cdn.example.in/invoices/ORD-1142.pdf",
    "filename": "Invoice ORD-1142.pdf"
  },
  "buttons": { "0": "orders/ORD-1142", "1": "HELP_ORD-1142" }
}

Body variables

templateParams is positional: templateParams[0] fills {{1}}, templateParams[1] fills {{2}}, and so on. Send exactly as many values as the campaign expects from the request; fewer fails with missing_template_values and details.missing names the gap, for example body_3. Values are strings; numbers are converted.

What WhatsApp rejects inside a variable
A variable value cannot contain a newline, a tab, or more than four spaces in a row, and the rendered message must stay within the template's 1024 character body limit. Such sends come back as meta_rejected.

Header

A text header with a variable takes headerText. An image, video or document header takes media, described under Media requirements. A header without a variable needs nothing.

Text header
{
  "to": "+919876543210",
  "headerText": "Order ORD-1142",
  "templateParams": ["Asha", "Friday, 12 September"]
}

Buttons

buttons is an object keyed by button index, counting from "0" in the order the buttons appear on the template. What the value means depends on the type of the button at that index:

Button typeValue you sendExample
URLThe dynamic suffix appended to the button's base URL, or the full URL. A button with a static URL needs nothing.orders/ORD-1142
COPY_CODEThe coupon code the recipient copies with one tap.DIWALI20
QUICK_REPLYThe payload returned to you when the button is tapped. Optional; defaults to the button text.HELP_ORD-1142

A positional array is accepted in place of the object, with index 0 first. Only dynamic buttons need a value.

JSON
// Keyed by button index. The template decides what each value means.
{ "buttons": { "0": "orders/ORD-1142", "1": "HELP_ORD-1142" } }

// The same request in positional form.
{ "buttons": ["orders/ORD-1142", "HELP_ORD-1142"] }

// A URL button accepts either the dynamic suffix or the full URL.
{ "buttons": { "0": "https://shop.example.in/orders/ORD-1142" } }

// A COPY_CODE button takes the coupon code the recipient copies.
{ "buttons": { "0": "DIWALI20" } }

Keyed overrides

values addresses every variable by an explicit key: body_N for body variable N, header_N for a text header variable, button_i_url_N for variable N of the URL button at index i. A key in values wins over templateParams, headerText and buttons. It exists for integrations that already build keyed maps; new code should use the positional fields.

JSON
{
  "to": "+919876543210",
  "values": {
    "body_1": "Asha",
    "body_2": "ORD-1142",
    "body_3": "Friday, 12 September",
    "header_1": "Order ORD-1142",
    "button_0_url_1": "orders/ORD-1142"
  }
}

Media requirements

A template with an image, video or document header needs a file on every send. Give WhatsApp a public https URL it can fetch at send time, or the id of a file already uploaded to your number.

Header typeFormatsMax sizeNotes
IMAGEJPEG, PNG5 MBStatic images only; no animation or transparency.
VIDEOMP4 (H.264 video, AAC audio)16 MBA single audio stream.
DOCUMENTPDF100 MBSend media.filename; the recipient sees it as the file name.
JSON
// Image or video header: a public https URL.
{ "media": { "url": "https://cdn.example.in/offers/diwali-2026.jpg" } }

// Document header: add the filename the recipient sees.
{ "media": { "url": "https://cdn.example.in/invoices/ORD-1142.pdf", "filename": "Invoice ORD-1142.pdf" } }

// A media id you already uploaded to your WhatsApp number.
{ "media": { "id": "1234567890123456" } }
  • The URL must be https, at most 2048 characters, and reachable without a login. A signed URL that stays valid for 10 minutes is fine; the file is fetched once, when the message is sent.
  • The response Content-Type must match the file (for example application/pdf); a mismatched type is rejected by WhatsApp as meta_rejected.
  • If you saved a default file on the campaign in the dashboard, omit media and that file is used. A media-header template with neither is a 400 media_required.
  • The legacy key values.header_media_url still works and means the same as media.url.

Tags and attributes

Every send creates or matches a contact by phone number, so the request can enrich it at the same time. Tags and attributes are filterable in the dashboard and usable as audience filters for broadcasts and retargeting.

Tags

  • Up to 30 tags per contact, each up to 40 characters. Unknown tags are created automatically.
  • Tags are matched case-insensitively and deduplicated; the first spelling wins, so Customer and customer are one tag.
  • Tags are added to the contact. A send never removes a tag the contact already has.

Attributes

  • Up to 40 keys per contact. Keys become snake_case: orderValue and Order Value both store as order_value.
  • Values are stored as strings, up to 200 characters. Numbers and booleans are converted; a null value is ignored.
  • Attributes are merged: a key you send overwrites that key, keys you do not send are kept.
JSON
// Request
{
  "tags": ["Customer", "shipped", "customer"],
  "attributes": { "city": "Pune", "orderValue": 2499, "Last Order": "ORD-1142", "notes": null }
}

// Stored on the contact
{
  "tags": ["Customer", "shipped"],
  "attributes": { "city": "Pune", "order_value": "2499", "last_order": "ORD-1142" }
}

name and email fill the contact record the same way, and source is recorded as the contact's first source when the contact is new.

Idempotency

Set externalId (or the Idempotency-Key header) on every send that a retry could repeat. Pick it from your own system: the object plus the event, such as order-1142-shipped. Keys are scoped to the campaign and may be up to 160 characters.

  • The same externalId never sends twice. A repeat returns HTTP 200 with duplicate: true and the earlier delivery's current status, whether that is accepted, delivered or failed.
  • A send that WhatsApp rejects (meta_rejected or send_failed) releases the key. Fix the problem and retry with the same externalId; the retry sends fresh and the failed attempt stays in the history.
  • A request rejected before the send (validation, missing values, wrong token, quiet hours) never consumes the key.
  • Without an externalId, every request sends a new message. Network retries then mean duplicate messages to the customer.
200 OK (duplicate)
{
  "success": true,
  "duplicate": true,
  "status": "delivered",
  "messageId": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSNkY3QjE4RkY0RjA2NDU2QjAxAA==",
  "deliveryId": "cmf3kd8y10004x7q2h1k3v9pz",
  "contactId": "cmf1a2b3c0009x7q2d4e5f6g7",
  "warnings": []
}

Status callbacks

Delivery status arrives on a webhook instead of polling. Add an endpoint under Agent > Webhooks, subscribe it to whatsapp.message.status, and copy its signing secret. One event is delivered for each status change of a message: sent, delivered, read, replied, clicked, failed.

Correlate on externalId, callbackData, or messageId from the send response. Statuses can arrive out of order (read before delivered when a phone comes back online), so treat the furthest state you have seen as current rather than the latest event.

Envelope

FieldTypeDescription
idstring
Unique per delivery attempt group (evt_…). Retries reuse it, so store it to skip duplicates.
eventstring
whatsapp.message.status for every event on this page.
apiVersionstring
The payload version of the Chat webhooks.
chatbotIdstring
The agent that owns the campaign and the WhatsApp number.
chatbotobject
{ id, name, orgId } of that agent.
timestampstring
ISO 8601, UTC. When the event was created.
dataobject
The event payload, described below.
Webhook event

whatsapp.message.status

Fires whenWhatsApp reports a message as sent, delivered or read; the recipient replies or taps a tracked link; or the send fails after it was accepted.

Fields in data

FieldTypeDescription
messageIdstring | null
The WhatsApp message id from the send response. null when the message never reached WhatsApp.
deliveryIdstring
The deliveryId from the send response.
campaignIdstring
The campaign the message belongs to.
campaignNamestring
The campaign's name in the dashboard.
campaignTypestring
api for messages sent through this API. The same event fires for broadcast and ongoing campaigns, so filter on this or on campaignId.
tostring
The recipient as digits with the country code, no plus sign.
Example: 919876543210
statusstring
sent, delivered, read, replied, clicked or failed.
externalIdstring | null
The externalId you sent, or null.
callbackDatastring | null
The callbackData you sent, echoed unchanged, or null.
timestampstring
ISO 8601, UTC. When this status was recorded.
errorobject
Only on failed: { code, title } with the WhatsApp error code (a number, or null for a transport failure) and a short title.
pricingobject
Present once WhatsApp reports it: { category, billable } with the conversation category (marketing, utility or authentication) and whether the message was billed.
Sample event
{
  "id": "evt_5f1c9a2b7d3e4f60a1b2c3d4",
  "event": "whatsapp.message.status",
  "apiVersion": "2026-05-08",
  "chatbotId": "cmk2p4x1a0000x7q2b9c3d4e5",
  "chatbot": {
    "id": "cmk2p4x1a0000x7q2b9c3d4e5",
    "name": "Orders assistant",
    "orgId": "cmk2p3w9z0000x7q2a1b2c3d4"
  },
  "timestamp": "2026-09-05T10:42:17.913Z",
  "data": {
    "messageId": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSNkY3QjE4RkY0RjA2NDU2QjAxAA==",
    "deliveryId": "cmf3kd8y10004x7q2h1k3v9pz",
    "campaignId": "cmf3k9d2a0001x7q2m4n8p1r6",
    "campaignName": "Order shipped (API)",
    "campaignType": "api",
    "to": "919876543210",
    "status": "delivered",
    "externalId": "order-1142-shipped",
    "callbackData": "order=ORD-1142",
    "timestamp": "2026-09-05T10:42:17.913Z",
    "pricing": { "category": "utility", "billable": true }
  }
}

Failed message: data

JSON
"data": {
  "messageId": null,
  "deliveryId": "cmf3kd8y10004x7q2h1k3v9pz",
  "campaignId": "cmf3k9d2a0001x7q2m4n8p1r6",
  "campaignName": "Order shipped (API)",
  "campaignType": "api",
  "to": "919876543210",
  "status": "failed",
  "externalId": "order-1142-shipped",
  "callbackData": "order=ORD-1142",
  "timestamp": "2026-09-05T10:42:19.204Z",
  "error": { "code": 131026, "title": "Message undeliverable" }
}

Delivery headers

HeaderDescription
Content-Typeapplication/json
X-Agentive-SignatureHMAC-SHA256 of the raw body with your webhook secret, hex encoded. No prefix.
X-Agentive-EventThe event name, for routing before you parse the body.
X-Agentive-DeliveryThe same id as the envelope's id field. Despite the name it is the event id, not a per-endpoint delivery id. The Voice APIs send the same value as X-Agentive-Event-Id.
X-Agentive-TimestampThe same timestamp as the envelope.
User-AgentAgentive-Webhooks/<apiVersion>.
X-Agentive-Delivery-IdNot sent here. On the Voice APIs this is a separate per-endpoint delivery id that is stable across retries; on this product, retries of one event carry the same X-Agentive-Delivery instead.
X-Agentive-AttemptNot sent here. There is no attempt counter on this product; the three attempts of one event are indistinguishable from each other.

Reply with any 2xx within 10 seconds. A non-2xx reply or a timeout is retried twice more, after 2 seconds and after 8 seconds, with the same id. Do your processing after you have responded if it is slow.

Verify the signature

Verify every delivery before you trust it. The signature is the HMAC-SHA256 of the exact raw request body with the endpoint's signing secret, hex encoded, sent as is:

Header
X-Agentive-Signature: 4f1d2c9b8a7e6f5d4c3b2a1908f7e6d5c4b3a291807f6e5d4c3b2a19080706e9

Read the raw bytes before any JSON parsing, compute the digest, and compare in constant time:

Node.js
import express from "express";
import crypto from "node:crypto";

const app = express();

// Read the RAW body: the signature covers the exact bytes we sent.
app.post(
  "/webhooks/agentive",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const secret = process.env.AGENTIVE_WEBHOOK_SECRET;
    if (!secret) return res.status(500).send("signing secret not set"); // never verify with an empty key
    const given = Buffer.from(req.header("X-Agentive-Signature") ?? "", "utf8");
    const expected = Buffer.from(
      crypto.createHmac("sha256", secret).update(req.body).digest("hex"),
      "utf8"
    );

    // Compare byte lengths first: timingSafeEqual throws on unequal lengths.
    const ok =
      given.length === expected.length && crypto.timingSafeEqual(given, expected);
    if (!ok) return res.status(401).send("bad signature");

    const evt = JSON.parse(req.body.toString("utf8"));

    // Deliveries retry, so skip an evt.id you have already processed.
    if (evt.event === "whatsapp.message.status") {
      const { status, externalId, callbackData, error } = evt.data;
      console.log(externalId, status, callbackData, error?.title);
    }

    res.sendStatus(200);
  }
);

app.listen(3000);
In PHP and Python
Compute hash_hmac("sha256", $rawBody, $secret) in PHP or hmac.new(secret, raw, hashlib.sha256).hexdigest() in Python, and compare with hash_equals or hmac.compare_digest. Refuse to run with an empty secret. Do not copy the Telephony Webhooks handlers as they are: the Voice APIs sign a timestamp together with the body, and this product signs the body alone.
Guard against a replayed body
This signature covers the body only, and X-Agentive-Timestamp is not signed, so a delivery someone captured would still verify if sent again later. Keep a permanent record of every event id you have handled and ignore repeats, and accept deliveries over HTTPS only, so they cannot be read off the network in the first place.

Quick start

From an approved template to a delivered message.

  1. Create an API campaign
    In the Agentive Chat dashboard open your agent, then WhatsApp > Campaigns > New campaign > API campaign. Pick an approved template, choose which variables come from the request (and fix the rest), and go live.
  2. Copy the token
    The campaign page shows the token once. Store it as an environment variable on your server, for example AGENTIVE_CAMPAIGN_TOKEN, together with the campaign id from the same page. Lost it? Rotate from the campaign page and copy the new one.
  3. Send your first message
    Replace the placeholders and send to your own number. Send one templateParams entry per request-mapped body variable.
    cURL
    curl https://app.agentive.co.in/api/v1/whatsapp/campaigns/<campaignId>/send \
      -H "Authorization: Bearer <agcamp_token>" \
      -H "Content-Type: application/json" \
      -d '{
        "to": "+91XXXXXXXXXX",
        "templateParams": ["Asha"],
        "externalId": "first-test-1"
      }'
  4. Verify delivery
    The response carries status: "accepted" and a messageId. Open the campaign's Deliveries tab to watch it move through sent, delivered and read, or subscribe a webhook to whatsapp.message.status to receive the same states in your own system.

Before you go live

  • Set externalId on every send a retry could repeat.
  • Honour Retry-After on rate_limited and details.retryAfter on quiet_hours.
  • Never retry opted_out or frequency_cap; nothing was sent and the contact is protected.
  • Verify X-Agentive-Signature on every callback and skip an id you have already processed.
  • Keep the campaign token on the server; rotate it if it ever leaves.

Migrating from another WhatsApp provider

The request is deliberately close to what the popular Indian WhatsApp APIs accept, so most integrations move by changing the URL, the auth header and a few field names. The aliases destination, userName and idempotencyKey are accepted as is.

Tools with an account key and a flat body

Typical field in other toolsAgentive fieldNote
apiKeyAuthorization: Bearer <token>A token per campaign instead of one account key.
campaignNamecampaignId in the URLThe campaign is addressed by id, not by name in the body.
destinationtodestination still works as an alias.
userNamenameuserName still works as an alias.
templateParamstemplateParams
media.urlmedia.urlAdd media.filename for documents.
buttonsbuttonsKeyed by button index, or a positional array.
attributesattributesKeys become snake_case.
tagstags
sourcesource

Tools with a split number and value arrays

Typical field in other toolsAgentive fieldNote
countryCode + phoneNumbertoOne field: +919876543210, or the 10 digits.
bodyValuestemplateParams
headerValues[0]media.url or headerTextmedia.url for a media header, headerText for a text header.
buttonValuesbuttons
callbackDatacallbackDataEchoed on every status callback.
traitsattributesKeys become snake_case.
template.name + languageCodenot neededThe campaign fixes the template and language.
What changes beyond field names
  • One token per campaign, sent as Authorization: Bearer, instead of an account-wide key in the body.
  • The campaign is addressed by id in the URL. The template and its language are fixed on the campaign, so they are not in the body.
  • The response is { success, duplicate, status, messageId, deliveryId, contactId, warnings }, and errors always carry a machine code.
  • Delivery status comes on the signed whatsapp.message.status webhook, with your callbackData echoed.

When Agentive Voice is connected

Connecting your Agentive Voice account to Agentive Chat adds call events as triggers for Ongoing WhatsApp campaigns, so a missed call, a finished call, a voicemail or a callback request can send an approved template on its own. Everything is set up from the Voice panel, and none of it needs code.

Set up in the dashboard, not over HTTP
These triggers are campaign settings, not an endpoint you call. You pick the event, the conditions and the template on an Ongoing campaign, and the call itself is what fires the send. Nothing on the rest of this page changes.

How to connect

  1. Open Integrations in the Voice panel
    Sign in to your Agentive Voice dashboard and open Integrations.
  2. Choose Agentive Chat
    Sign in once with your Agentive Chat admin email and password. The password is used to verify the account and is not stored.
  3. Check that call events are linked
    They are linked as part of connecting. If the Integrations page shows Call events not linked, use Link call events to retry.
  4. Create an Ongoing campaign
    In WhatsApp > Campaigns create an Ongoing campaign, pick a Voice event as its trigger, add conditions if you want to narrow it, and map the template variables to the event data below.
Ongoing campaigns have no audience step
The event picks the contact. The customer on the call is matched to a contact by number, and the message goes to that one person.

Voice trigger events

Eight events appear in the Ongoing trigger list once Voice is connected. One call fires both the any-direction event and its directional twin, so subscribe at the grain you want rather than at both, or the customer receives two messages for one call.

Event keyNameWhen it firesCondition fieldsVariables
voice.call.missedMissed Call (any direction)Any unanswered call, inbound or outbound. Covers both a customer you missed and a customer who did not pick up.
  • direction
  • endReason
  • durationSec
  • trigger.callerNumber
  • trigger.direction
  • trigger.agentName
  • trigger.missedAt
voice.call.missed.inboundInbound Missed CallA customer called in and nobody answered. The classic instant follow-up on a missed call.
  • endReason
  • durationSec
  • trigger.callerNumber
  • trigger.agentName
  • trigger.missedAt
voice.call.missed.outboundOutbound No-AnswerYou called a customer and they did not pick up. Follow up with a message saying you tried to reach them.
  • endReason
  • durationSec
  • trigger.callerNumber
  • trigger.agentName
  • trigger.missedAt
voice.call.completedCall Completed (any direction)Any answered call that has ended, inbound or outbound.
  • direction
  • durationSec
  • trigger.callerNumber
  • trigger.durationSec
  • trigger.callTime
  • trigger.agentName
voice.call.completed.inboundInbound Call CompletedA customer call was answered and has ended. Send a summary, a feedback ask or the next steps.
  • durationSec
  • trigger.callerNumber
  • trigger.durationSec
  • trigger.callTime
  • trigger.agentName
voice.call.completed.outboundOutbound Call CompletedAn outbound call you placed connected and has ended. Send a recap or what happens next.
  • durationSec
  • trigger.callerNumber
  • trigger.durationSec
  • trigger.callTime
  • trigger.agentName
voice.voicemail.leftVoicemail LeftA customer left a voicemail after not reaching anyone. Acknowledge it and promise a callback.
  • durationSec
  • trigger.callerNumber
  • trigger.durationSec
  • trigger.missedAt
voice.callback.requestedCallback RequestedA call was marked for a callback, or the customer asked to be called back. Confirm the time over WhatsApp.
  • direction
  • trigger.callerNumber
  • trigger.callbackAt
  • trigger.callTime
  • trigger.durationSec
  • trigger.agentName

Conditions are an AND list over the event data, so endReason is no_answer together with durationSec of at least 10 fires only for a call that rang for ten seconds and was never picked up.

Event data available to conditions and variables

The table above lists the conditions and the template variables each event offers, and both are chosen from those lists in the campaign builder. The fields below are the call data those choices resolve against, so you can see what a variable such as trigger.callerNumber will put in the message. A variable mapped to a field the event does not carry stops the send, as described below.

Event data

FieldTypeDescription
phonestring
The customer's number, and what the campaign matches a contact on. It is the far end of the call: the caller on an inbound call, the number you dialled on an outbound one.
Example: +919876543210
callerNumberstring
The same number, exposed for templates as trigger.callerNumber.
Example: +919876543210
directionstring
inbound when the customer called you, outbound when you called the customer.
Example: inbound
durationSecinteger
Seconds. Talk time on an answered call, ring time on an unanswered one, and the length of the message on a voicemail.
Example: 120
endReasonstring
Why an unanswered call ended, normalised to one of no_answer, busy, rejected, canceled, failed, voicemail. On the missed and voicemail events only.
Example: no_answer
agentNamestring
The person or line that handled the call. Blank when it cannot be determined, and a template variable mapped to it then fails the send rather than guessing a name, so map it only when the line is known.
Example: Priya
callIdstring
The call's unique id, the same one the call carries in the Voice panel. Useful for tying a message back to a call in your own reporting.
Example: 9f2c1d6a-3e4f-40b9-8d7c-2a1e5f6b8c0d
callTypestring
What kind of call it was, when the Voice account reports one, for example voicemail.
Example: voicemail
campaignIdstring
The outbound calling campaign the call belonged to, when it came from one. Blank for an organic call.
Example: 482
campaignTypestring
The kind of that calling campaign, carried in the event data alongside the campaign id.
Example: human_pool
callTimestring
When the call happened. ISO 8601 timestamp; the Voice and Chat panels show the same moment in IST.
Example: 2026-09-05T09:41:12.000Z
missedAtstring
When the call was missed, or when the voicemail was left. ISO 8601 timestamp; the Voice and Chat panels show the same moment in IST. On the missed and voicemail events only.
Example: 2026-09-05T09:41:12.000Z
callbackAtstring
The callback time that was asked for. ISO 8601 timestamp; the Voice and Chat panels show the same moment in IST. On Callback Requested only, and blank when no time was set.
Example: 2026-09-06T05:30:00.000Z
A variable that resolves empty stops the send
A template variable mapped to event data that turns out blank fails the send rather than falling back to something else, so the customer never receives a message with a gap in it. Map only fields the event you chose actually carries.

Delay, cooldown and quiet hours

A Voice-triggered campaign obeys the same controls as every other Ongoing campaign. A delay holds the message for a set time after the call, which is useful when someone may call the customer straight back. A cooldown limits one contact to a single message from that campaign per set number of days, so a customer who rings three times in a morning is messaged once.

Quiet hours and the marketing frequency cap apply as they do to a broadcast, and both cover marketing templates only: utility and authentication templates are never held. A marketing message that lands inside quiet hours is held and sent when the window opens; a contact who has already reached the 24 hour marketing cap is skipped for that event. See Rate limits for what each one means.

Next steps