Developers
API documentation
A token-authenticated REST API for messages, templates, contacts, campaigns, flows and per-message delivery status and cost. Create an API key under Developer tools in your account, then authenticate every call with Authorization: Bearer <token>.
Full samples in PHP, Node.js, Python, Java and Ruby are available in the in-app documentation after you create an account.
Machine-readable reference: OpenAPI 3.1 (JSON) · Postman collection
Before you start
Authentication
Every request carries an API key in the Authorization header as a Bearer token. Keys are created under Developer tools > API keys and shown once.
A key has scopes: read (every GET), send (the send endpoints, campaigns and flow starts) and manage (creating, changing and deleting contacts, groups, canned replies, webhooks and templates, and the inbox actions). A key without the scope gets 403 naming the missing scope.
A key can expire: an expired key gets 401 with "This API key has expired". A missing or unknown token is 401 as well.
A sandbox key validates every request, records the message with status "sandbox" and sends nothing to WhatsApp - build against the real API without a WhatsApp line or a cost.
Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>
Rate limit
60 requests per minute per API key; requests without a key are counted per IP address.
Every answer carries X-RateLimit-Limit and X-RateLimit-Remaining. Over the limit the answer is 429 with statusCode 429, a message and a Retry-After header in seconds.
GET /api/usage reports the limit under rate_limit.per_minute. /api and /api/v2 share one bucket per key.
HTTP/1.1 429 Too Many Requests
Retry-After: 42
{"statusCode": 429, "message": "Too many requests. Try again in 42 seconds."}
Idempotency
Send an Idempotency-Key header (1 to 128 characters, unique per operation) on the POST endpoints that create something: the sends, campaigns, contacts, groups and canned replies.
The same key with the same request within 24 hours returns the stored answer with the header Idempotent-Replayed: true, and nothing runs again. A client that retries after a timeout no longer sends twice.
The same key with a different request is 422; the same key while the first request is still running is 409. Only 2xx and 4xx answers are stored, so a 5xx can be retried with the same key.
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7
Dates and timezones
Every date the API returns is UTC in ISO-8601 with its offset, whatever timezone the organization uses in the dashboard.
Dates you send (scheduled_at, since) are read as UTC unless they carry an offset.
"created_at": "2026-10-05T09:30:00+00:00"
Errors
Every answer is JSON with statusCode and message; a refused payload adds errors, a map of field name to the list of problems.
400 invalid input; 401 missing, invalid or expired key; 403 scope, subscription, plan limit or account under review; 404 unknown uuid, or one that belongs to another workspace; 409 conflict (a campaign transition that is not possible, an Idempotency-Key still in flight); 422 refused by a rule (the 24-hour window, STOP, template variables); 429 rate limit; 500 our fault.
The inbox actions (assign, close and reopen on /api/conversations/{contact_uuid}) answer 409 while ticketing is off under Settings > Tickets; reading the conversation and adding a note work either way.
On /api (v1) a refusal by WhatsApp itself is a 200 with data.success false and the reason in data.data.error (code, message, type). On /api/v2 the same refusal is an HTTP 422 - see Versioning.
{"statusCode": 400, "message": "The given data was invalid.", "errors": {"phone": ["The phone number is not valid."]}}
Pagination
The list endpoints take page (from 1) and per_page (default 10, max 100; /api/messages defaults to 25) and are ordered newest first.
Contacts, groups, canned replies, templates, campaigns and campaign logs answer data, links and meta (current_page, last_page, per_page, total). /api/messages answers statusCode, data and meta (page, per_page, total, last_page).
A conversation is paged by cursor instead: GET /api/contacts/{uuid}/messages and GET /api/conversations/{contact_uuid} answer newest first with next_before; pass it as before to get the older messages.
Sending a template
A template send has to carry exactly as many parameters as the approved template has placeholders; Meta refuses a mismatch with error 132000.
GET /api/templates/{uuid} returns example_payload: a complete body for POST /api/send/template with the right number of parameters for the header, the body and the URL buttons - replace the values and send it.
POST /api/templates submits a template to Meta from a flat JSON shape and answers with Meta's reason when it is refused; POST /api/templates/sync pulls the templates from Meta after a change in Meta Business Manager.
GET https://whapier.com/api/templates/{{uuid}}
{"data": {"name": "order_update", "variables": {"header": 0, "body": 1, "buttons": 0}, "example_payload": {"phone": "+40721000001", "template": {"name": "order_update", "language": {"code": "en"}, "components": [{"type": "body", "parameters": [{"type": "text", "text": "Ana"}]}]}}}}
Webhooks
Outbound webhooks are set up under Developer tools > Webhooks or over the API (POST /api/webhooks): an https URL and the events to receive. The signing secret is generated for you and shown once. Every delivery is a POST with a JSON body carrying event and data.
The X-Whapier-Signature header is "sha256=" followed by the HMAC-SHA256 of the raw request body with that secret, in hex. Recompute it over the bytes you received and compare before trusting the payload.
Answer 2xx within 15 seconds. A delivery that fails is retried twice more, after 60 seconds and after 5 minutes; after the third failure the event is dropped.
The events: message.received and message.sent; message.status when Meta reports a message as delivered, read or failed (with the failure and the campaign it came from); campaign.finished with the final counts; ticket.created and ticket.closed; contact.opted_out and contact.opted_in; contact, group and autoreply created, updated and deleted; template.status.update, template.quality.update and account.update as Meta sends them. GET /api/webhooks/events lists them with a description and POST /api/webhooks/{uuid}/test delivers a sample.
X-Whapier-Signature: sha256=5d41402abc4b2a76b9719d911017c592...
{"event": "message.status", "data": {"uuid": "64968a32-fdd9-4c1d-8941-b8e538036616", "wam_id": "wamid.HBgM...", "status": "delivered", "timestamp": "2026-10-05T09:30:00+00:00", "failure": null, "contact": {"uuid": "81a2d165-41e2-43f2-847e-d69e08cba0e0", "phone": "+40721000001"}, "campaign": null}}
{"event": "campaign.finished", "data": {"uuid": "3f1c2d0e-5b7a-4c8d-9e0f-1a2b3c4d5e6f", "name": "October offers", "status": "completed", "counts": {"contacts": 120, "sent": 118, "delivered": 110, "read": 64, "failed": 2}, "finished_at": "2026-10-05T09:30:00+00:00"}}
message.received
message.sent
message.status
message.status.update
contact.created
contact.updated
contact.deleted
contact.opted_out
contact.opted_in
group.created
group.updated
group.deleted
autoreply.created
autoreply.updated
autoreply.deleted
campaign.finished
ticket.created
ticket.closed
template.status.update
template.quality.update
account.update
Versioning: v1 and v2
/api (version 1) stays supported as it is. /api/v2 serves the same endpoints, payloads and fields with one difference: the HTTP status carries the outcome.
On v2 a refusal by WhatsApp on a send is an HTTP 422 whose body has statusCode 422, the message, error (WhatsApp's code, message and type) and data (the v1 answer), instead of a 200 with data.success false. Whenever statusCode in a body differs from the HTTP status, the HTTP status follows statusCode.
Everything else - authentication, scopes, rate limit, idempotency, dates, pagination - is identical, and the same key works on both.
POST https://whapier.com/api/v2/send
{"statusCode": 422, "message": "Template name does not exist", "error": {"code": 132001, "message": "Template name does not exist", "type": "OAuthException"}, "data": {"success": false, "message": "Template name does not exist", "data": {"error": {"code": 132001}}}}
Verify
GET
/api/verify
Verify API key
# Confirms the Bearer token is valid and the organization subscription is active before calling other endpoints. # HTTP 200: token OK. 401: missing/invalid token. 403: subscription inactive. curl -X GET \ https://whapier.com/api/verify \ -H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \ -H 'Accept: application/json'
Contacts
GET
/api/contacts
Get contact list
# Optional query: ?page=1&per_page=10 (per_page max 100). # Newest first. Response: data (contact objects), links and meta (current_page, last_page, per_page, total). # A contact: id, uuid, phone, first_name, last_name, email, address, metadata, groups (uuid, name), source, opted_in_at, opted_out_at, created_at, updated_at. # Search filters, combined with AND: phone (any format the organization's country can read; 400 when it is not a number), email (exact, case-insensitive), q (2 to 128 characters, matched against name, email and number), group (a contact-group uuid; 400 when unknown), opted_out (true | false), updated_since (a date, UTC), source (api, manual, import, entry point...). curl -X GET \ "https://whapier.com/api/contacts?page=1&per_page=10" \ -H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \ -H 'Accept: application/json'
GET
/api/contacts/{uuid}
Get one contact
# The contact object of Get contact list plus unread_messages, last_message_at and ticket (status, assigned_to: name, email).
# ticket is null while ticketing is off under Settings > Tickets, or when the conversation has no ticket yet. 404 when the uuid is unknown, deleted or belongs to another workspace.
curl -X GET \
https://whapier.com/api/contacts/{{uuid}} \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Accept: application/json'
POST
/api/contacts
Add contact
# JSON body fields:
# phone (required) — international number; validated and stored as E.164. Must be unique per organization (400 if duplicate).
# first_name, last_name, email (optional) — omit, null, or "" for contacts without names.
# street, city, state, zip, country (optional) — stored in address JSON.
# metadata (optional) — arbitrary JSON-serializable value.
# group (optional) — array of contact-group UUID strings.
# marketing_opt_in (optional, boolean) — true records marketing consent on the contact (GDPR consent trail, source: api); false records an opt-out. Omit to leave consent unchanged.
# Responses: 200 success { statusCode, id, message }; 400 validation; 403 subscription inactive or contact limit reached.
# Accepts an Idempotency-Key header (see Before you start).
curl -X POST \
https://whapier.com/api/contacts \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"phone": "+40721000001",
"first_name": "Ana",
"last_name": "Pop",
"email": "ana@example.com",
"group": [
"d746e4c5-db24-4fb2-be03-590421344d69"
],
"marketing_opt_in": true
}'
POST
/api/contacts/bulk
Bulk create or update contacts
# contacts (required, 1 to 500 objects) with the fields of Add contact per row. A row whose phone the workspace already has is updated with the fields present (as Edit contact does); a new number is created.
# Each row stands alone: one bad row fails alone and the others go through. Response: { statusCode, created, updated, failed: [ { index, phone, errors } ], limit_reached, message }.
# Creating stops at the plan's contact limit: limit_reached is true and the rows that did not fit are listed in failed. Updates never count against the limit. Accepts an Idempotency-Key header.
curl -X POST \
https://whapier.com/api/contacts/bulk \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"contacts": [
{
"phone": "+40721000001",
"first_name": "Ana",
"last_name": "Pop",
"email": "ana@example.com"
},
{
"phone": "0721 000 002",
"first_name": "Ion",
"group": [
"d746e4c5-db24-4fb2-be03-590421344d69"
]
},
{
"phone": "not-a-phone",
"first_name": "Nobody"
}
]
}'
PUT
/api/contacts/{uuid}
Edit contact
# Same JSON fields as Add contact. Path {uuid} is the contact id returned when created.
# phone is optional on an update; if you change it, the new number must not belong to another contact (400 duplicate).
# Updating with the same phone as this contact is allowed.
# Fields you leave out keep their value; send a field as null or "" to clear it. metadata keys you send are merged into the stored ones, the others are kept.
# marketing_opt_in (optional, boolean) — true records marketing consent (source: api), false records an opt-out; omitted leaves consent unchanged.
# 404 when the uuid is unknown, deleted or belongs to another workspace.
curl -X PUT \
https://whapier.com/api/contacts/{{uuid}} \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"first_name": "Ana",
"last_name": "Popescu",
"metadata": {
"plan": "platinum"
}
}'
DELETE
/api/contacts/{uuid}
Delete contact
# Soft-deletes the contact; it leaves every list at once. 404 when the uuid is unknown or already deleted.
# Response: { statusCode, id, message }.
curl -X DELETE \
https://whapier.com/api/contacts/{{uuid}} \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Accept: application/json'
Contact groups
GET
/api/contact-groups
Get contact group list
# Optional query: ?page=1&per_page=10 (per_page max 100). # Each group: uuid, name, contacts_count (live members), created_at, updated_at. curl -X GET \ "https://whapier.com/api/contact-groups?page=1&per_page=10" \ -H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \ -H 'Accept: application/json'
POST
/api/contact-groups
Add contact group
# name (required) must be unique among the organization's groups (400 if taken).
# Response: { statusCode, id, message }. Accepts an Idempotency-Key header.
curl -X POST \
https://whapier.com/api/contact-groups \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"name": "Lead 5"
}'
PUT
/api/contact-groups/{uuid}
Edit contact group
# Same body as Add contact group. 404 for an unknown uuid.
curl -X PUT \
https://whapier.com/api/contact-groups/{{uuid}} \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"name": "Lead 5"
}'
DELETE
/api/contact-groups/{uuid}
Delete contact group
# Removes the group and its memberships; the contacts themselves are kept. 404 for an unknown uuid.
curl -X DELETE \
https://whapier.com/api/contact-groups/{{uuid}} \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Accept: application/json'
Automated replies
GET
/api/canned-replies
Get automated replies list
# Optional query: ?page=1&per_page=10 (per_page max 100). # Each reply: uuid, name, trigger, match_criteria, response_type, response (the text, or null for media), file (name, url) or null, created_at, updated_at. curl -X GET \ "https://whapier.com/api/canned-replies?page=1&per_page=10" \ -H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \ -H 'Accept: application/json'
POST
/api/canned-replies
Add automated reply
# Body: name and trigger (max 255), match_criteria (exact match | contains), response_type (text | image | audio), response.
# For text, response is the reply (max 4096). For image or audio send multipart/form-data with the file as response (png/jpg up to 5 MB; mp3/aac/ogg/amr/opus/m4a up to 16 MB); without a file the request is a 400.
# Response: { statusCode, id, message }. 403 when the plan's limit of canned replies is reached. Accepts an Idempotency-Key header.
curl -X POST \
https://whapier.com/api/canned-replies \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"name": "About Us",
"trigger": "what do you do?",
"match_criteria": "contains",
"response_type": "text",
"response": "We sell shoes"
}'
PUT
/api/canned-replies/{uuid}
Edit automated reply
# Same fields as Add automated reply. An update without a file keeps the media it already has. 404 for an unknown uuid.
curl -X PUT \
https://whapier.com/api/canned-replies/{{uuid}} \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"name": "About Us",
"trigger": "what do you do?",
"match_criteria": "contains",
"response_type": "text",
"response": "We sell shoes and clothes"
}'
DELETE
/api/canned-replies/{uuid}
Delete automated reply
# 404 for an unknown uuid. Response: { statusCode, id, message }.
curl -X DELETE \
https://whapier.com/api/canned-replies/{{uuid}} \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Accept: application/json'
WhatsApp lines
GET
/api/whatsapp-connections
List connections
# Returns each line's uuid (use as whatsapp_connection on /api/send). No internal database ids. # Each row: uuid, type, name, phone, display_phone_number and, for a WhatsApp Business line, verified_name and number_status. 403 when no line is connected. curl -X GET \ https://whapier.com/api/whatsapp-connections \ -H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \ -H 'Accept: application/json'
Messages
POST
/api/send
Send message
# Optional: whatsapp_connection (UUID from GET /api/whatsapp-connections). Omit to use the organization default line.
# phone (required) — any format; normalised to E.164 with the organization's country. A new number becomes a contact (first_name, last_name, email optional).
# message (required, max 4096); header and footer (optional, max 60); buttons (optional, up to 3 of { id, title }).
# Free-form messages need an open 24-hour window: 422 when the customer's last message is older than a day. 403: subscription, plan limit, trial daily allowance or an account under review.
# Response: 200 { statusCode, data: { success, data: { chat: { uuid, wam_id, status, type, created_at, contact } } } }. A refusal by WhatsApp is 200 with data.success false and data.data.error on /api; an HTTP 422 on /api/v2.
# Accepts an Idempotency-Key header.
curl -X POST \
https://whapier.com/api/send \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"phone": "+40721000001",
"message": "Hello Ana, how are you?",
"whatsapp_connection": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"header": "Example Shoes",
"footer": "Reply STOP to opt out",
"buttons": [
{
"id": "yes",
"title": "Fine"
},
{
"id": "no",
"title": "Not well"
}
]
}'
POST
/api/send/media
Send media
# Optional: whatsapp_connection (UUID from GET /api/whatsapp-connections). Omit to use the organization default line.
# media_type (image | video | audio | document | sticker); media_url (https, a public host; checked with one HEAD request against WhatsApp's size and type caps - 400 with the reason otherwise); caption (optional, not for audio); file_name (required for document).
# Same guards and response as Send message.
curl -X POST \
https://whapier.com/api/send/media \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"phone": "+40721000001",
"media_type": "image",
"media_url": "https://cdn.example.com/offers/october.jpg",
"caption": "Our October offers"
}'
POST
/api/send/template
Send template message
# Optional: "whatsapp_connection": "<uuid>" for a specific WABA line (UUID from GET /api/whatsapp-connections).
# template.name and template.language ("en" or { "code": "en" }) are required; components follow WhatsApp's own shape (header, body, button parameters).
# Works outside the 24-hour window. A contact who opted out (STOP) is not sent a MARKETING template: 422. Utility and authentication templates still go.
# Same guards and response as Send message.
curl -X POST \
https://whapier.com/api/send/template \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"phone": "+40721000001",
"template": {
"name": "order_update",
"language": {
"code": "en"
},
"components": [
{
"type": "header",
"parameters": [
{
"type": "text",
"text": "48213"
}
]
},
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "Ana"
},
{
"type": "text",
"text": "48213"
},
{
"type": "date_time",
"date_time": {
"fallback_value": "October 7, 2026"
}
}
]
},
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [
{
"type": "text",
"text": "48213"
}
]
}
]
}
}'
POST
/api/send/location
Send location
# latitude (-90 to 90) and longitude (-180 to 180) required; name and address optional.
# Same guards and response as Send message.
curl -X POST \
https://whapier.com/api/send/location \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"phone": "+40721000001",
"latitude": 46.77120000000000032969182939268648624420166015625,
"longitude": 23.623599999999999710098563809879124164581298828125,
"name": "Example Shoes",
"address": "Str. Memorandumului 12, Cluj-Napoca"
}'
POST
/api/send/contacts
Send contact cards
# contacts (1 to 10 cards) in WhatsApp's contacts[] shape: name.formatted_name required; phones[], emails[], addresses[], org optional.
# Same guards and response as Send message.
curl -X POST \
https://whapier.com/api/send/contacts \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"phone": "+40721000001",
"contacts": [
{
"name": {
"formatted_name": "Example Shoes Support",
"first_name": "Support"
},
"phones": [
{
"phone": "+40700000000",
"type": "WORK"
}
],
"emails": [
{
"email": "support@example.com",
"type": "WORK"
}
]
}
]
}'
POST
/api/send/reaction
Send reaction
# message_id (the wam_id of the message to react to) and emoji (required; "" removes the reaction).
# Same guards and response as Send message.
curl -X POST \
https://whapier.com/api/send/reaction \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"phone": "+40721000001",
"message_id": "wamid.HBgMNDA3MjEwMDAwMDEVAgARGBI5QkY2RkE3RDlCMjc2RjI2QkEA",
"emoji": "👍"
}'
POST
/api/send/location-request
Request location
# message (required, max 1024): the text shown above the "Send location" button.
# Same guards and response as Send message.
curl -X POST \
https://whapier.com/api/send/location-request \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"phone": "+40721000001",
"message": "Where should we deliver?"
}'
POST
/api/send/request-contact-info
Request contact info
# message (required, max 1024): asks a username-only WhatsApp user to share their phone number.
# Same guards and response as Send message.
curl -X POST \
https://whapier.com/api/send/request-contact-info \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"phone": "+40721000001",
"message": "Could you share your number so we can call you back?"
}'
POST
/api/send/carousel
Send carousel
# message (required, max 1024) and cards (2 to 10): header { type: image | video, link } required; body (max 160), url_button { text, url } and quick_replies (up to 3 of { id, title }) optional.
# Same guards and response as Send message.
curl -X POST \
https://whapier.com/api/send/carousel \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"phone": "+40721000001",
"message": "Our new arrivals",
"cards": [
{
"header": {
"type": "image",
"link": "https://cdn.example.com/shoes/1.jpg"
},
"body": "Runner, 89 EUR",
"url_button": {
"text": "See it",
"url": "https://example.com/shoes/1"
}
},
{
"header": {
"type": "image",
"link": "https://cdn.example.com/shoes/2.jpg"
},
"body": "Trail, 119 EUR",
"quick_replies": [
{
"id": "trail",
"title": "Tell me more"
}
]
}
]
}'
Flows
POST
/api/flows/{uuid}/start
Start a flow
# contact (required): the contact's uuid or phone (normalised with the organization's country).
# 200 { statusCode, success: true, message }; 404 flow or contact unknown; 422 flow not published; 409 the contact is already in a flow session.
# Whatever the flow sends is subject to the 24-hour window unless it uses template nodes.
curl -X POST \
https://whapier.com/api/flows/{{uuid}}/start \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"contact": "+40721000001"
}'
Campaigns
GET
/api/campaigns
List campaigns
# Optional: ?page=1&per_page=10&search= # Each campaign: uuid, name, status, scheduled_at, created_at, template (uuid, name, language, category, status), whatsapp_connection (uuid, name, type), audience (type: all | group | segment, uuid, name), counts (contacts, sent, delivered, read, failed). curl -X GET \ "https://whapier.com/api/campaigns?page=1&per_page=10" \ -H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \ -H 'Accept: application/json'
POST
/api/campaigns
Create campaign (WABA template with variables)
# Use when whatsapp_connection is a WhatsApp Cloud (WABA) line from GET /api/whatsapp-connections.
#
# PREREQUISITES
# - template: UUID of a row from GET /api/templates (organization-scoped), NOT the public template name string.
# - Match variable counts to your approved template: open the template metadata in the UI or Meta — BODY uses {{1}}, {{2}}, … in order.
# - You must supply one body parameter for each BODY placeholder, in the same order. A count that does not match the approved template is refused up front (422), not by WhatsApp afterwards.
#
# FIELD NOTES
# - contacts: "all" or a contact-group uuid; or segment: a saved segment's uuid (one of the two, not both). send_immediately: true, or scheduled_at (a date, read as UTC).
# - header: optional object; omitted means { "format": "TEXT", "parameters": [] }.
# - body: use body.parameters[] for variables. API format (recommended): { "type": "text", "value": "literal for {{1}}" } per slot.
# Alternative (dashboard-style): { "selection": "static", "value": "..." } or { "selection": "dynamic", "value": "first name" } (maps contact fields).
# - footer / buttons: optional; use [] or {} if unused. Templates with URL/quick-reply variables need buttons[] shaped like the web campaign form.
# - Media header (IMAGE|VIDEO|DOCUMENT): header.parameters[] with type (IMAGE|VIDEO|DOCUMENT), "selection": "link", "value": "https://..." (public URL; API cannot upload files).
#
# RESPONSE: 200 { "statusCode": 200, "uuid": "...", "message": "..." }. Poll GET /api/campaigns/{uuid} for the report and GET /api/campaigns/{uuid}/logs for per-recipient status, or subscribe to the campaign.finished webhook.
# 403 when the plan's campaign limit is reached. Accepts an Idempotency-Key header.
curl -X POST \
https://whapier.com/api/campaigns \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"name": "Billing reminder blast",
"whatsapp_connection": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"contacts": "all",
"send_immediately": true,
"template": "281a3383-985c-4084-bcb0-9ec523c952de",
"header": {
"format": "TEXT",
"parameters": []
},
"body": {
"parameters": [
{
"type": "text",
"value": "Ana"
},
{
"type": "text",
"value": "48213"
},
{
"type": "text",
"value": "7 October 2026"
}
]
},
"footer": {},
"buttons": []
}'
GET
/api/campaigns/{uuid}
Get one campaign with its report
# The campaign object of List campaigns plus report: counts (contacts, sent, delivered, read, failed, pending), failure_reasons (code, title, count - the ten most frequent), started_at, finished_at (null until the campaign is over), paused_reason, paused_detail and paused_at (null unless the status is paused).
# paused_reason is user (paused from the dashboard or the API), admin, failing, failure_rate or quality (paused by the platform; paused_detail says why). 404 for an unknown campaign.
curl -X GET \
https://whapier.com/api/campaigns/{{uuid}} \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Accept: application/json'
POST
/api/campaigns/{uuid}/pause
Pause a campaign
# No body. Only a scheduled or ongoing campaign can be paused: 409 otherwise, with the status the campaign is in. The logs stay as they are and resume picks them up.
# Response: { statusCode, status, message } - status is the campaign's new status. 404 for an unknown campaign.
curl -X POST \
https://whapier.com/api/campaigns/{{uuid}}/pause \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{}'
POST
/api/campaigns/{uuid}/resume
Resume a campaign
# No body. Only a paused campaign can be resumed: 409 otherwise. A campaign paused before its hour goes back to scheduled; one paused while sending goes back to ongoing.
# Response: { statusCode, status, message }. 404 for an unknown campaign.
curl -X POST \
https://whapier.com/api/campaigns/{{uuid}}/resume \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{}'
POST
/api/campaigns/{uuid}/cancel
Cancel a campaign
# No body. A scheduled, ongoing or paused campaign can be cancelled: 409 otherwise. What was not sent stays unsent; the report keeps showing who was reached.
# Response: { statusCode, status, message }. 404 for an unknown campaign.
curl -X POST \
https://whapier.com/api/campaigns/{{uuid}}/cancel \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{}'
GET
/api/campaigns/{uuid}/logs
Campaign logs
# Optional: ?page=1&per_page=10&search= (search contact name/phone).
# Each row: contact (uuid, phone, first_name, last_name), status, message (uuid, wam_id, status) or null, error (code, title, reason) or null, retry_count, created_at, updated_at. 404 for an unknown campaign.
curl -X GET \
"https://whapier.com/api/campaigns/{{uuid}}/logs?page=1&per_page=10" \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Accept: application/json'
Templates
GET
/api/templates
List templates
# Optional query: ?page=1&per_page=10 (per_page max 100). # Each template: uuid, name, language, category, status, components (as WhatsApp approved them), variables (header, body, buttons: how many placeholders each part takes), rejection (why it was rejected, or null), quality_score, updated_at. curl -X GET \ "https://whapier.com/api/templates?page=1&per_page=10" \ -H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \ -H 'Accept: application/json'
GET
/api/templates/{uuid}
Get one template with a ready-to-send example
# The template object of List templates plus example_payload: a complete body for POST /api/send/template with one parameter per placeholder (the examples the template was submitted with, else a value naming the slot), a link for a media header and a suffix for each URL button with a variable.
# Optional query: whatsapp_connection (a line uuid) when the same template exists on several lines. 404 for an unknown uuid.
curl -X GET \
https://whapier.com/api/templates/{{uuid}} \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Accept: application/json'
POST
/api/templates
Create template
# name (lowercase letters, digits and underscores), category (MARKETING | UTILITY) and language (a Meta language code such as en or pt_BR) are required; whatsapp_connection (a WhatsApp Business line uuid) is optional - the default line otherwise.
# header (optional): { format: TEXT, text (max 60, one {{1}} allowed), examples } or { format: IMAGE | VIDEO | DOCUMENT, media_url: a public https link to a sample file }.
# body.text (required, max 1024, placeholders {{1}}, {{2}}, ...) with body.examples, one per placeholder; footer (optional, max 60); buttons (optional, up to 10): { type: QUICK_REPLY | URL | PHONE_NUMBER | COPY_CODE, text, url, phone_number, example }.
# Response: 200 { statusCode, uuid, status, message } - status is PENDING until Meta reviews it (the template.status.update webhook tells you). 400 with Meta's reason when Meta refuses it; AUTHENTICATION templates cannot be submitted through the API yet. A sandbox key cannot submit (403). Accepts an Idempotency-Key header.
curl -X POST \
https://whapier.com/api/templates \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"name": "order_update",
"category": "UTILITY",
"language": "en",
"header": {
"format": "TEXT",
"text": "Order {{1}}",
"examples": [
"48213"
]
},
"body": {
"text": "Hi {{1}}, your order {{2}} ships on {{3}}.",
"examples": [
"Ana",
"48213",
"October 7"
]
},
"footer": "Example Shoes",
"buttons": [
{
"type": "URL",
"text": "Track",
"url": "https://example.com/track/{{1}}",
"example": "48213"
},
{
"type": "QUICK_REPLY",
"text": "Stop updates"
}
]
}'
POST
/api/templates/sync
Sync templates from Meta
# No body. Pulls the templates of every WhatsApp Business line from Meta, as the dashboard's refresh does, so a template changed in Meta Business Manager shows up here.
# Response: { statusCode, synced, message } - synced is how many templates were added or changed. A sandbox key cannot sync (403); 400 when Meta refused the request.
curl -X POST \
https://whapier.com/api/templates/sync \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{}'
Webhooks
GET
/api/webhooks
List webhooks
# Each row: uuid, url, events, secret_preview (the last six characters of the signing secret), created_at, updated_at. The full secret is shown once, in the answer to Add webhook. # 403 when the Webhooks module is not on the plan. curl -X GET \ https://whapier.com/api/webhooks \ -H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \ -H 'Accept: application/json'
GET
/api/webhooks/events
List events
# Every event a webhook can receive: data (name, description). The Webhooks page in the dashboard reads the same catalogue.
# A delivery is { event, data }; the shape of data per event is under Before you start > Webhooks, and POST /api/webhooks/{uuid}/test delivers a sample of any of them.
curl -X GET \
https://whapier.com/api/webhooks/events \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Accept: application/json'
POST
/api/webhooks
Add webhook
# url (required, https, unique per organization) and events (required, one or more names from GET /api/webhooks/events).
# Response: { statusCode, uuid, secret, message } - the only time the full secret is returned; store it to check X-Whapier-Signature. Accepts an Idempotency-Key header.
curl -X POST \
https://whapier.com/api/webhooks \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"url": "https://example.com/hooks/whapier",
"events": [
"message.received",
"message.status",
"campaign.finished"
]
}'
PUT
/api/webhooks/{uuid}
Edit webhook
# Same body as Add webhook. The events list replaces the stored one; the secret stays as it is. 404 for an unknown uuid.
curl -X PUT \
https://whapier.com/api/webhooks/{{uuid}} \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"url": "https://example.com/hooks/whapier",
"events": [
"message.received",
"message.status"
]
}'
DELETE
/api/webhooks/{uuid}
Delete webhook
# Removes the endpoint and its subscriptions; nothing is delivered to it any more. Response: { statusCode, uuid, message }. 404 for an unknown uuid.
curl -X DELETE \
https://whapier.com/api/webhooks/{{uuid}} \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Accept: application/json'
POST
/api/webhooks/{uuid}/test
Send a test event
# event (required): a name from GET /api/webhooks/events. The sample payload is posted to the url as { event, data, test: true }, signed with the secret in X-Whapier-Signature.
# Response: { statusCode, event, delivered, http_status, message } - delivered is false with the status your endpoint answered, or http_status null when it could not be reached. 404 for an unknown uuid.
curl -X POST \
https://whapier.com/api/webhooks/{{uuid}}/test \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"event": "message.status"
}'
Message status
GET
/api/messages
List messages
# Optional query: status (sent | delivered | read | failed | accepted | played), direction (inbound | outbound), since and until (dates, UTC), contact (a contact uuid), campaign (a campaign uuid: the messages it produced), wam_id (one message by WhatsApp id), page, per_page (default 25, max 100).
# Response: { statusCode, data: [ message ], meta: { page, per_page, total, last_page } }. 404 when contact or campaign is unknown.
# A message: uuid, wam_id, direction, status, type, content, created_at, delivered_at, read_at, failed_at, failure (code, title, reason, retryable) or null, pricing (category, model, billable, amount, currency, country), contact (uuid, phone, first_name, last_name).
# content by type - text: body, header, footer, buttons (type, text); template: name, language, body; image, video, audio, document, sticker: caption, media (type, url, filename); interactive and button: text, payload (what the customer pressed); location: latitude, longitude, name, address; contacts: contacts (name, phones); reaction: emoji, wam_id.
curl -X GET \
"https://whapier.com/api/messages?status=failed&per_page=25" \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Accept: application/json'
GET
/api/contacts/{uuid}/messages
Conversation with a contact
# The same message objects as List messages, for one contact, newest first. Optional query: per_page (default 25, max 100), before (the uuid of the oldest message you already have).
# Response: { statusCode, data: [ message ], meta: { per_page, has_more, next_before } } - pass next_before as before to get the older messages. 404 for an unknown contact or cursor.
curl -X GET \
"https://whapier.com/api/contacts/{{uuid}}/messages?per_page=25" \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Accept: application/json'
GET
/api/messages/{uuid}
Get one message
# The same message object plus history: a list of { status, timestamp, recorded_at }, every status change WhatsApp reported. 404 for an unknown uuid.
curl -X GET \
https://whapier.com/api/messages/{{uuid}} \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Accept: application/json'
Usage
GET
/api/usage
Plan and usage
# Response data: plan (name, status, valid_until), limits (contacts, messages, campaigns: used, limit; limit null = unlimited), trial_daily_messages (trial accounts only), messages_sent (today, last_7_days, last_30_days), estimated_cost (last_30_days: amount, currency), rate_limit (per_minute). curl -X GET \ https://whapier.com/api/usage \ -H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \ -H 'Accept: application/json'
Inbox
GET
/api/conversations/{contact_uuid}
Get a conversation
# Response data: contact (uuid, phone, first_name, last_name), ticketing (whether Settings > Tickets is on), ticket (status, assigned_to: name, email; opened_at, waiting_since, resolved_at) or null, messages (uuid, wam_id, direction, type, text, status, created_at - the 50 newest first), next_before.
# Optional query: before (the uuid of the oldest message you have) for the older messages; 400 when the cursor is not a message of this contact. 404 for an unknown contact.
curl -X GET \
https://whapier.com/api/conversations/{{uuid}} \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Accept: application/json'
POST
/api/conversations/{contact_uuid}/assign
Assign a conversation
# user_email (required, nullable): the email of an active member of the organization; null unassigns the ticket. A conversation without a ticket gets one.
# Response: { statusCode, data: ticket, message } with the ticket as Get a conversation shows it. 409 while ticketing is off; 422 when nobody active in the organization has that email; 404 for an unknown contact.
curl -X POST \
https://whapier.com/api/conversations/{{uuid}}/assign \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"user_email": "maria@example.com"
}'
POST
/api/conversations/{contact_uuid}/close
Close a conversation
# No body. Resolves the ticket (status closed, resolved_at set), as the inbox button does. Response: { statusCode, data: ticket, message }.
# 409 while ticketing is off; 404 for an unknown contact.
curl -X POST \
https://whapier.com/api/conversations/{{uuid}}/close \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{}'
POST
/api/conversations/{contact_uuid}/reopen
Reopen a conversation
# No body. Reopens a closed ticket (status open, resolved_at cleared); an open ticket is returned as it is. Response: { statusCode, data: ticket, message }.
# 409 while ticketing is off; 404 for an unknown contact.
curl -X POST \
https://whapier.com/api/conversations/{{uuid}}/reopen \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{}'
POST
/api/conversations/{contact_uuid}/notes
Add an internal note
# note (required, max 5000): shown to the agents in the thread, never sent to the contact. Works whether ticketing is on or off.
# Response: { statusCode, uuid, created_at, message }. 404 for an unknown contact. Accepts an Idempotency-Key header.
curl -X POST \
https://whapier.com/api/conversations/{{uuid}}/notes \
-H 'Authorization: Bearer <YOUR_AUTHORIZATION_TOKEN>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"note": "Customer called; wants the order delivered on Friday."
}'
Get an API key in two minutes
Free trial, no credit card — the developer panel is included on every plan.
Start free trial