---
title: "An endpoint's delivery log"
method: GET
path: "/v1/webhooks/{id}/logs"
tags: ["Webhooks"]
---

# An endpoint's delivery log

`GET /v1/webhooks/{id}/logs`

## Request

- Base URL: `https://api.kawaa.com`
- URL: `https://api.kawaa.com/v1/webhooks/{id}/logs`
- 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:read`)

The deliveries made to this endpoint, newest first: product events, test sends and retries, each with what was sent and what the endpoint answered. Entries are kept for 30 days. An entry for a `verification.*` or `job.*` event holds that job's data, so it is removed with the job's results when the plan keeps results for less time than that, and can no longer be read or retried.

**A scoped key sees only the entries whose payload it may read.** Each entry holds its payload verbatim, so an entry is returned only when the key holds the scope that owns what its event carries (`verification:read` for `verification.*` and `job.*`, `account:read` for `credits.*`, `deliverability:read` for `blacklist.detected` and `dmarc.failure`; on an account that can have white-label sub-accounts, `credits.*` also needs `white_label:read`, because those events can name a sub-account). The events left out for that reason are named in `withheld_events`, and an unrestricted key or a dashboard session sees every entry. An id that belongs to another account is a 404.

## Path parameters

- `id` string, required

## Query parameters

- `limit` integer
- `offset` integer
- `cursor` string

## Response `200`

One page of the log.

- object
  - `success` unknown, required
  - `data` object, required
    - `logs` object[], required
      - `log_id` string, required: What the retry operation takes as `{logId}`.
      - `event` string, required: The event the delivery carried, as it was stored. Entries written before the event names were settled can carry a legacy alias (`batch.completed`, `batch.failed`, `credits.depleted`) rather than its canonical name.
      - `status` 'success' | 'failed', required: `success` when the endpoint answered 2xx.
      - `status_code` integer, nullable, required: The endpoint's HTTP status. Null when no HTTP response was received (timeout, DNS or connection error, or a URL refused as unsafe); `error_message` says which.
      - `response_time_ms` integer, nullable, required: How long the delivery took, across its attempts. Null on an entry that did not record it.
      - `request_body` string, required: The JSON body that was sent, verbatim, as a string: parse it to read the payload. The signature was computed over exactly these bytes. A test send's body carries `"test": true`.
      - `response_body` string, nullable, required: The start of what the endpoint answered, truncated. Null when it answered nothing or no response was received.
      - `error_message` string, nullable, required: Why a failed delivery failed, e.g. `HTTP 500`, a timeout, or `Webhook URL rejected: ...`. Null on a success.
      - `created_at` string, date-time, required
      - `delivery_type` 'event' | 'test' | 'retry', required: `event` for a real delivery, `test` for a send from the test operation, `retry` for a re-send from the retry operation. Entries written before this field existed read as `event`.
    - `count` integer, required: How many entries are on this page.
    - `total` integer, required: An older name for `count`, kept for existing clients: the number of entries on THIS page, not the number of deliveries the endpoint has had, which is not counted.
    - `limit` integer, required: The page size that was applied.
    - `offset` integer, required: The offset that was applied, echoed even when `cursor` was sent and the offset was not used.
    - `cursor` string: Present when there may be more entries. Send it back as `cursor` to read the next page; its absence is the end. A page can be shorter than `limit`, or empty, and still carry one, so page until it is absent rather than stopping at a short page.
    - `withheld_events` string[]: Present only for a scoped key that may not read some events' payloads: the canonical names of the events whose entries this key is not shown. Entries for these events may exist and are not on the page. Absent when the key is refused nothing.
    - `unclassified_withheld` unknown
    - `unclassified_withheld_unknown` unknown

## 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)
