---
title: "Send a test delivery"
method: POST
path: "/v1/webhooks/{id}/test"
tags: ["Webhooks"]
---

# Send a test delivery

`POST /v1/webhooks/{id}/test`

## Request

- Base URL: `https://api.kawaa.com`
- URL: `https://api.kawaa.com/v1/webhooks/{id}/test`
- Auth: one of:
  - API key in header `X-Api-Key`
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://api.kawaa.com/oauth/authorize`, token URL `https://api.kawaa.com/oauth/token`, scopes: `webhooks:write`)

Delivers a signed sample payload to the endpoint so you can confirm signature verification before real events arrive. It carries the same signature headers as a real delivery, `X-Kawaa-Signature-V2` with the time of this send and the legacy `X-Kawaa-Signature`.

## Path parameters

- `id` string, required

## Request body

- object: Optional. `event` picks which event shape the test delivery carries; omit the body entirely and the endpoint picks one. Deliberately NOT closed, because this route is not: it reads only `event` from the body, and anything else is ignored rather than refused. That is unlike create and update, which answer 400 for the first key they do not recognise, so a misspelled `evnet` here still selects the default event rather than failing. The VALUE of `event` is now validated (an unknown name is a 400), so the remaining gap is a misspelled KEY. Check the response's `event` field to confirm which sample was sent. Send only `event`.
  - `event` string: Which event's sample payload to send. Omitted, the FIRST event Kawaa defines (`verification.completed`) is used, not necessarily one this webhook is subscribed to, so a test with no body can exercise an event the endpoint never receives in practice. Explicit `null` and other non-string values are refused with 400; only omission selects the default. **An unrecognised name is refused with `400 Invalid events: <name>`** (the same message create and update give), so a mistyped `dmarc.failur` fails loudly instead of delivering a `verification.completed` sample and reporting success. Legacy aliases (`batch.completed`, `credits.depleted`) resolve to their canonical names. The response's `event` field names whichever event was actually sent. Valid names come from `GET /v1/webhooks/events`.

## Response `200`

The delivery was ATTEMPTED. **200 does not mean it worked**: branch on `data.success`.

- object
  - `success` unknown, required
  - `data` object, required
    - `success` boolean, required: Whether the endpoint accepted the delivery (a 2xx). False for a non-2xx, a timeout, a connection failure, and a URL refused as unsafe, all of which still answer HTTP 200 at this route, because the REQUEST succeeded even though the DELIVERY did not.
    - `status_code` integer, required: What the endpoint answered. `0` when nothing was reached.
    - `response_time_ms` integer, required
    - `response_body` string, nullable, required: The first part of the endpoint's body, for diagnosing a rejection. Null when there was none.
    - `message` string, required: What happened, in prose: the only place a DNS refusal or an SSRF block is explained.
    - `event` string, required: The canonical event whose sample payload was delivered, and the value of the `X-Kawaa-Event` header on that delivery. Equals what you sent, with a legacy alias resolved to its canonical name, or `verification.completed` when the request named no event. Read it to confirm which integration path the test actually exercised.

## Other responses

- `400`: The request is malformed, or names a field this endpoint does not accept. Retrying it unchanged will fail identically.
- `401`: No credential, or one that is not accepted. `code` distinguishes `API_KEY_SUSPENDED` (subscription ended; restartable) from `API_KEY_REVOKED`, `SESSION_EXPIRED` and `OAUTH_TOKEN_EXPIRED` (a connected app's one-hour access token lapsed: refresh it and retry, or sign in again if no refresh token was issued or the refresh is refused).
- `403`: Refused, and **the code says which of three problems it is**, and they need different remedies: - `INSUFFICIENT_SCOPE`: the credential is valid but not scoped for this route. With an API key, issue one that carries the scope named in the message. With an OAuth access token, connect the app again asking for that scope as well as the ones it has, unless the message says a connected app can never be given it; then use an API key with every scope the message names, or have the account owner do it in the dashboard. - `PLAN_UPGRADE_REQUIRED`: the account's plan does not include the feature. **No key of any scope reaches it**, so issuing another is wasted work; the remedy is a plan change. Returned by the verification routes for `options.enrich`, and by the finder, the monitors, the audit log, auto-clean and the white-label routes. - `PLAN_LIMIT_EXCEEDED`: the plan includes the feature and the account is at its ceiling (webhook endpoints, team seats, integrations, auto-clean lists). Delete one or upgrade. - `FORBIDDEN`: refused for a reason none of the above covers. `ErrorCode` is an open set, so branch on the code with a default rather than exhaustively.
- `404`: No such resource on this account.
- `429`: Too many requests. When the server knows how long to wait, `Retry-After` says so and `error.retry_after_seconds` carries the same number; the `X-RateLimit-*` headers say where you stand.
- `default`: Any status this operation does not list. Kawaa's own errors carry the error envelope. The gateway in front of the API can answer any operation with its own bare body (`GatewayError`): a `503` after about 29 seconds, when the request may still have run and been charged, a `413` for a body over about 6 MB, or a `400` for a path with malformed percent-encoding.

---

[API](https://skmtc.dev/kawaa/apis/kawaa-api.md) · [All operations](https://skmtc.dev/kawaa/apis/kawaa-api/llms.txt) · [OpenAPI document](https://skmtc.dev/kawaa/apis/kawaa-api/revisions/d27c9c33f6ba?raw)
