Whapier

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