Create a webhook
POSThttps://api.wapito.com/v1/webhooks
Send a channel token on every request: Authorization: Bearer wpt_YOUR_TOKEN
Subscribes an HTTPS endpoint to a set of events. Event names may be exact, prefix wildcards such as `messages.*`, or `*` for everything. The signing secret is returned exactly once in this response, so store it immediately; every delivery is signed with it in the `X-Wapito-Signature` header. URLs that resolve to private or loopback addresses are rejected.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
enabled | boolean | Optional | Start delivering immediately. Defaults to true. |
events | array of string | Required | Events to subscribe to: exact names, prefix wildcards such as `messages.*`, or `*`. |
headers | object | Optional | Extra headers sent with every delivery. |
url | string | Required | HTTPS endpoint that receives events. Private and loopback addresses are rejected.uri |
{
"events": [
"messages",
"messages.status",
"groups.participants"
],
"headers": {
"X-Acme-Tenant": "eu-1"
},
"url": "https://hooks.acme.example/wapito"
}Responses
201
The webhook, including its signing secret. This is the only time the full secret is returned.
| Field | Type | Required | Description |
|---|---|---|---|
created_at | string | null | Required | ISO 8601 timestamp.date-time |
enabled | boolean | Required | Disabled webhooks are skipped without queueing jobs. |
events | array of string | Required | Subscribed events: exact names, prefix wildcards such as `messages.*`, or `*`. |
headers | object | Required | Extra headers sent with every delivery. |
id | string | Required | Webhook id (`whk_` + ULID). |
secret_preview | string | null | Required | Masked signing secret, e.g. `whsec_…f31a`. |
url | string | Required | HTTPS endpoint Wapito posts events to.uri |
secret | string | Required | The `whsec_…` signing secret for `X-Wapito-Signature`. Store it now. |
{
"created_at": "2026-09-01T08:30:00.000Z",
"enabled": true,
"events": [
"messages",
"messages.status",
"groups.participants"
],
"headers": {
"X-Acme-Tenant": "eu-1"
},
"id": "whk_01JRQ8F4X9N2K7YB3C5V6W8H0T",
"secret": "whsec_8f2c1d94b6a70e35c4d1a2b3e4f5061728394a5b6c7d8e9f",
"secret_preview": "whsec_…f31a",
"url": "https://hooks.acme.example/wapito"
}400
The request body or query string is malformed, or the recipient cannot be parsed into a WhatsApp id.
Show 2 example bodies
invalid_recipient — `to` is not a dialable number or valid WhatsApp id
{
"error": {
"code": "invalid_recipient",
"details": {
"to": "+1555"
},
"message": "The recipient is not a valid WhatsApp address.",
"request_id": "req_01JRQ8F4X9N2K7YB3C5V6W8H0T"
}
}invalid_request — schema validation failed
{
"error": {
"code": "invalid_request",
"details": {
"issues": [
{
"message": "Array must contain at least 2 element(s)",
"path": "body.options"
}
]
},
"message": "The request payload failed validation.",
"request_id": "req_01JRQ8F4X9N2K7YB3C5V6W8H0T"
}
}401
The channel token is missing, malformed, revoked or belongs to a deleted channel.
Show 2 example bodies
token_revoked — the token was rotated in the dashboard
{
"error": {
"code": "token_revoked",
"message": "This channel token has been revoked.",
"request_id": "req_01JRQ8F4X9N2K7YB3C5V6W8H0T"
}
}unauthorized — no or unusable Bearer token
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid channel token.",
"request_id": "req_01JRQ8F4X9N2K7YB3C5V6W8H0T"
}
}402
A cap that a higher plan lifts was reached (Sandbox: one channel, two webhooks, no proxy). `details.checkout_url` is the dashboard page that starts the upgrade; `details.plan` names the plan that refused, `details.limit` the cap it applied (for counted features) and `details.price_ids` the Stripe prices, so a client can start Checkout itself. A cap no plan lifts (five webhooks on Premium) is `429 quota_exceeded` instead.
Show the example body
plan_required — upgrade needed, `checkout_url` included
{
"error": {
"code": "plan_required",
"details": {
"checkout_url": "https://wapito.com/app/?upgrade=1",
"feature": "webhooks",
"limit": 2,
"plan": "sandbox",
"price_ids": {
"monthly": "price_1S0aBcDeFgHiJkLmMONTHLY",
"yearly": "price_1S0aBcDeFgHiJkLmYEARLY"
}
},
"message": "This action requires a Premium channel.",
"request_id": "req_01JRQ8F4X9N2K7YB3C5V6W8H0T"
}
}403
The token is valid but the channel may not perform this action right now.
Show 2 example bodies
channel_locked — billing lapsed or the channel was locked by an operator
{
"error": {
"code": "channel_locked",
"details": {
"reason": "plan_required"
},
"message": "This channel is locked.",
"request_id": "req_01JRQ8F4X9N2K7YB3C5V6W8H0T"
}
}forbidden — the channel does not own the target object
{
"error": {
"code": "forbidden",
"message": "You are not allowed to perform this action.",
"request_id": "req_01JRQ8F4X9N2K7YB3C5V6W8H0T"
}
}422
The request is well formed but cannot be acted on.
Show the example body
webhook_url_invalid — not HTTPS, or resolves to a private address
{
"error": {
"code": "webhook_url_invalid",
"details": {
"url": "http://localhost:3000/hook"
},
"message": "The webhook URL is not reachable or not allowed.",
"request_id": "req_01JRQ8F4X9N2K7YB3C5V6W8H0T"
}
}429
A rate limit, a plan quota or one of the anti-ban guards stopped the request. Every one of these is safe to retry later; read `Retry-After` when present.
Show 5 example bodies
cold_send_limit — too many first messages to new recipients this hour
{
"error": {
"code": "cold_send_limit",
"details": {
"resets_at": "2026-09-15T09:00:00.000Z",
"window_cap": 20
},
"message": "The hourly limit for messages to new recipients is reached.",
"request_id": "req_01JRQ8F4X9N2K7YB3C5V6W8H0T"
}
}quota_exceeded — plan quota for the day or month
{
"error": {
"code": "quota_exceeded",
"details": {
"quota": "sent",
"resets_at": "2026-09-16T00:00:00.000Z",
"used": 150
},
"message": "The plan quota for this resource is exhausted.",
"request_id": "req_01JRQ8F4X9N2K7YB3C5V6W8H0T"
}
}rate_limited — per-minute API rate limit
{
"error": {
"code": "rate_limited",
"details": {
"retry_after": 12
},
"message": "Too many requests.",
"request_id": "req_01JRQ8F4X9N2K7YB3C5V6W8H0T"
}
}send_rate_limited — the send queue did not drain within 20 s
{
"error": {
"code": "send_rate_limited",
"details": {
"retry_after": 5
},
"message": "The send queue for this channel is saturated.",
"request_id": "req_01JRQ8F4X9N2K7YB3C5V6W8H0T"
}
}warmup_limit — the warm-up ladder cap for today
{
"error": {
"code": "warmup_limit",
"details": {
"cap": 200,
"day": 2
},
"message": "The warm-up limit for this channel is reached.",
"request_id": "req_01JRQ8F4X9N2K7YB3C5V6W8H0T"
}
}500
Something went wrong inside Wapito. Quote `request_id` when reporting it.
Show the example body
internal_error — unexpected failure
{
"error": {
"code": "internal_error",
"message": "Something went wrong on our side.",
"request_id": "req_01JRQ8F4X9N2K7YB3C5V6W8H0T"
}
}Errors
Code examples
import requests
url = "https://api.wapito.com/v1/webhooks"
payload = {
"url": "https://hooks.acme.example/wapito",
"events": ["messages", "messages.status", "groups.participants"],
"headers": { "X-Acme-Tenant": "eu-1" }
}
headers = {
"Authorization": "Bearer wpt_YOUR_TOKEN",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())Related
Other endpoints in this group
Glossary
Reference
Try it on your own number
Create a channel, link a WhatsApp number by QR or pairing code, and call the API in a couple of minutes. The Sandbox plan is free and needs no card.