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

Fields of the request body
FieldTypeRequiredDescription
enabledbooleanOptionalStart delivering immediately. Defaults to true.
eventsarray of stringRequiredEvents to subscribe to: exact names, prefix wildcards such as `messages.*`, or `*`.
headersobjectOptionalExtra headers sent with every delivery.
urlstringRequiredHTTPS 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.

Fields of the 201 response
FieldTypeRequiredDescription
created_atstring | nullRequiredISO 8601 timestamp.date-time
enabledbooleanRequiredDisabled webhooks are skipped without queueing jobs.
eventsarray of stringRequiredSubscribed events: exact names, prefix wildcards such as `messages.*`, or `*`.
headersobjectRequiredExtra headers sent with every delivery.
idstringRequiredWebhook id (`whk_` + ULID).
secret_previewstring | nullRequiredMasked signing secret, e.g. `whsec_…f31a`.
urlstringRequiredHTTPS endpoint Wapito posts events to.uri
secretstringRequiredThe `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

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.