---
title: "Webhook endpoints API"
description: "Register webhook endpoints, choose their events, rotate signing secrets, send test deliveries and read delivery results from your own servers."
canonical_url: "https://noxtica.com/docs/api/reference/webhooks/"
markdown_url: "https://noxtica.com/docs/api/reference/webhooks/index.md"
last_updated: 2026-11-07
published: 2026-10-27
section: "server-apis"
section_title: "Server APIs"
openapi_release: "v0.16.1"
openapi_spec_version: "1.5.1-2026-11-03"
site: "Noxtica"
language: "en"
source: markdown
---

# Webhook endpoints API

Register webhook endpoints, choose their events, rotate signing secrets, send test deliveries and read delivery results from your own servers.

Call these operations from your own servers with an `sk_` server API key that carries the `read:webhooks` scope (reads) or the `write:webhooks` scope (changes). Every call is scoped to your
organisation, and the endpoints you manage here are the ones the Backoffice shows (Integrations & API, Webhooks): one list.

**Scopes.** `read:webhooks` covers every read: `GET /v1/webhooks`, `GET /v1/webhooks/{webhookId}`, `GET /v1/webhooks/{webhookId}/deliveries` and `GET /v1/webhooks/event-types`. `write:webhooks` covers every change: creating, updating and deleting an endpoint, rotating its signing secret and sending a test delivery. `write:webhooks` does not include reading, so a key that manages endpoints needs both. A call without the scope answers `403` with `API key does not have read:webhooks scope` (or `write:webhooks`): the message names the missing scope. A key keeps its scopes until a tenant administrator changes them in the Backoffice (API Configuration, Edit scopes on the key: the key id and the secret stay the same and nothing has to be redeployed; an added scope reaches the API within about a minute, a removed scope stops working at once); the other way is to create a new key with the scopes ticked.

The events themselves, their payloads and the delivery rules (headers, signature, retries, deduplication) are described with the API each event belongs to: identity verification (KYC, KYB, KYM),
device intelligence (risk scoring, accounts) and the Pulse tenant API. A test delivery with `POST /v1/webhooks/{webhookId}/test` sends a sample of any of them.

| Environment | Base URL |
| --- | --- |
| Production | `https://collect.noxtica.com` |

Credential: `ApiKeyAuth`. See [Authentication](https://noxtica.com/docs/api/authentication/).

## List the events you can subscribe to {#listWebhookEventTypes}

`GET /v1/webhooks/event-types`

Server-to-server. Returns every event type an endpoint can subscribe to, grouped by product (`kyc`, `kyb`, `kym`, `risk`, `account`, `pulse`), in display order, with the
name of the `data` schema each carries. Use it to build a subscription from the live catalog instead of copying event names. `identity` is `true` for the events that are delivered
only to an endpoint whose signing secret is sealed at rest (every KYC, KYB and KYM session and review event): rotate the secret of an older endpoint once before subscribing it to them.

The payload of every event is documented in the `webhooks` section of the identity verification API (KYC, KYB, KYM), the device intelligence API (risk and account) and the
Pulse tenant API; `dataType` names the schema.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `read:webhooks`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `product` | query | string | No | Return only the events of one product. Allowed values: `kyc`, `kyb`, `kym`, `risk`, `account`, `pulse`. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The event catalog. |  | `application/json` WebhookEventTypesResponse |
| `400` | `invalid_query_parameters` (an unknown query parameter) or `invalid_product`. | `invalid_product`, `invalid_query_parameters` | `application/json` WebhookApiError |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `404` | The byte-identical "V1 endpoint not found" response returned when the route's module is not enabled for your organisation, and also for a path that does not exist — indistinguishable by design (anti-enumeration), so do not treat it as proof the path is wrong. | `V1 endpoint not found` | `application/json` |
| `429` | `rate_limit_exceeded`: too many requests. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying. The test delivery has a tighter limit than the other operations. | `rate_limit_exceeded` | `application/json` WebhookApiError |
| `503` | Noxtica could not read the key's record. This is a temporary fault on Noxtica's side and says nothing about your key. Retry after the `Retry-After` delay; do not treat it as a revoked key. | `api_key_auth_unavailable` | `application/json` |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |
| `503` | `Retry-After` | integer | Seconds to wait before retrying. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `products` | array of string | Yes |  |
| `eventTypes` | array of WebhookEventTypeEntry | Yes |  |
| `eventTypes[]` | object (WebhookEventTypeEntry) | No |  |
| `eventTypes[].type` | string | Yes | A webhook event type, exactly as it appears in the envelope `type`, the `X-Noxtica-Event` header and an endpoint's `events`. Allowed values: `kyc.session.created`, `kyc.session.opened`, `kyc.session.verifying`, `kyc.session.review_pending`, `kyc.session.completed`, `kyc.session.unverified`, `kyc.session.error`, `kyc.session.expired`, `kyc.session.cancelled`, `kyc.review.decided`, `kyc.verification.completed`, `kyc.erasure.completed`, `kyc.document.expiry_warning`, `kyc.reverification.due`, `kyb.session.created`, `kyb.session.opened`, `kyb.session.submitted`, `kyb.session.under_review`, `kyb.session.completed`, `kyb.session.expired`, `kyb.session.abandoned`, `kyb.entity.created`, `kyb.entity.under_review`, `kyb.entity.closed`, `kym.session.created`, `kym.session.opened`, `kym.session.submitted`, `kym.session.under_review`, `kym.session.completed`, `kym.session.expired`, `kym.session.abandoned`, `kym.case.status_changed`, `kym.alert.created`, `submission.scored`, `risk.flagged`, `risk.high`, `risk.cluster_flagged`, `account.agent_linked`, `pulse.detection.v1`, `pulse.enrolment.linked.v1`, and 1 more. |
| `eventTypes[].product` | string | Yes | Allowed values: `kyc`, `kyb`, `kym`, `risk`, `account`, `pulse`. |
| `eventTypes[].group` | string | Yes | What the event is about inside its product: `session`, `verification`, `review`, `erasure`, `monitoring`, `entity`, `case`, `alert`, `scoring`, `agent`, `detection`, `link` or `response`. |
| `eventTypes[].label` | string | Yes | A short English name. |
| `eventTypes[].description` | string | Yes | One sentence saying when the event fires. |
| `eventTypes[].schemaVersion` | string | Yes | The version of the `data` member. Allowed values: `1`. |
| `eventTypes[].dataType` | string | Yes | The name of the schema of the `data` member. |
| `eventTypes[].identity` | boolean | Yes | `true` when the event is delivered only to an endpoint with a sealed signing secret. |
| `requestId` | string | No |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "products": [
    "kyc",
    "kyb",
    "kym",
    "risk",
    "account",
    "pulse"
  ],
  "eventTypes": [
    {
      "type": "kyc.session.created",
      "product": "kyc",
      "group": "session",
      "label": "Session created",
      "description": "A hosted KYC session (invitation) was minted.",
      "schemaVersion": "1",
      "dataType": "KycSessionLifecycleData",
      "identity": true
    },
    {
      "type": "kyc.session.completed",
      "product": "kyc",
      "group": "session",
      "label": "Session finished",
      "description": "A hosted KYC session reached a terminal outcome: completed, unverified or error.",
      "schemaVersion": "1",
      "dataType": "KycSessionCompletedData",
      "identity": true
    }
  ],
  "requestId": "req-test"
}
```

### Example request

**curl**

```bash
curl "https://collect.noxtica.com/v1/webhooks/event-types" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/webhooks/event-types', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/webhooks/event-types",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## List your webhook endpoints {#listWebhookEndpoints}

`GET /v1/webhooks`

Server-to-server. Returns every webhook endpoint of your organisation, the ones registered in the Backoffice and the ones created through this API (one store). The signing secret is
never returned. `limit` is the most endpoints an organisation can register (25).

**Authentication:** Bearer token (`ApiKeyAuth`), scope `read:webhooks`.

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The endpoints. |  | `application/json` WebhookEndpointList |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `404` | The byte-identical "V1 endpoint not found" response returned when the route's module is not enabled for your organisation, and also for a path that does not exist — indistinguishable by design (anti-enumeration), so do not treat it as proof the path is wrong. | `V1 endpoint not found` | `application/json` |
| `429` | `rate_limit_exceeded`: too many requests. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying. The test delivery has a tighter limit than the other operations. | `rate_limit_exceeded` | `application/json` WebhookApiError |
| `503` | `webhooks_unavailable`: a temporary fault, or `webhook_secret_kek_required`: encryption of webhook secrets is not available for your organisation, so nothing was written. Retry with backoff. | `webhooks_unavailable` | `application/json` WebhookApiError |
| `503` | Noxtica could not read the key's record. This is a temporary fault on Noxtica's side and says nothing about your key. Retry after the `Retry-After` delay; do not treat it as a revoked key. | `api_key_auth_unavailable` | `application/json` |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |
| `503` | `Retry-After` | integer | Seconds to wait before retrying. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `webhooks` | array of WebhookEndpoint | Yes |  |
| `webhooks[]` | object (WebhookEndpoint) | No | One webhook endpoint. The signing secret is never part of it. |
| `webhooks[].id` | string | Yes |  |
| `webhooks[].url` | string (uri) | Yes | The HTTPS endpoint Noxtica POSTs to. |
| `webhooks[].events` | array of string | Yes | The event types the endpoint receives. Exact match, no wildcard. |
| `webhooks[].description` | string, nullable | Yes | At most 200 characters. |
| `webhooks[].enabled` | boolean | Yes | A disabled endpoint is kept but receives nothing. |
| `webhooks[].secretSealed` | boolean | Yes | `true` when the signing secret is stored encrypted, which the session and review events (`identity` events) require. `false` for an endpoint created before encrypted storage; rotate its secret once. |
| `webhooks[].createdAt` | integer (int64) | Yes | Unix ms. |
| `webhooks[].updatedAt` | integer (int64) | Yes | Unix ms. |
| `webhooks[].lastDeliveryAt` | integer (int64), nullable | Yes | Unix ms of the last delivery attempt |
| `webhooks[].lastStatus` | string, nullable | Yes | The result of the last attempt: `ok`, `http_<status>`, `blocked` (the URL was refused), `error`, `secret_plaintext_ineligible`; `null` before the first attempt. |
| `limit` | integer | Yes | The most endpoints an organisation can register. |
| `requestId` | string | No |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "webhooks": [
    {
      "id": "7f3a2c10-5b8e-4d2a-9c61-0e4a7b9d1f35",
      "url": "https://hooks.example.com/noxtica",
      "events": [
        "kyc.session.created",
        "kyc.session.completed",
        "kyc.review.decided"
      ],
      "description": "Onboarding backend",
      "enabled": true,
      "secretSealed": true,
      "createdAt": 1790760000000,
      "updatedAt": 1790760000000,
      "lastDeliveryAt": 1790762445000,
      "lastStatus": "ok"
    }
  ],
  "limit": 25,
  "requestId": "req-test"
}
```

### Example request

**curl**

```bash
curl "https://collect.noxtica.com/v1/webhooks" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/webhooks', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/webhooks",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## Register a webhook endpoint {#createWebhookEndpoint}

`POST /v1/webhooks`

Server-to-server. Registers an HTTPS endpoint and the events it receives. **The signing secret is returned once, in this response, and never again**: store it now. Use `rotate-secret`
to replace it later.

`url` must be `https`, without embedded credentials, and must not point at a private, loopback or internal address; the same URL cannot be registered twice. `events` is the exact list
of event types the endpoint receives (see `GET /v1/webhooks/event-types`): there is no wildcard, no default subscription and an empty list is refused, so list every event you want, and
an event type added later is not delivered to an endpoint that did not select it. At most 25 endpoints per organisation. The new endpoint is enabled. The secret is stored encrypted; when
encryption is not available for your organisation the call is refused (`503 webhook_secret_kek_required`) and nothing is written.

Every create, update, delete and rotation is recorded in the organisation's audit log with your API key as the actor. The endpoints you create here appear in the Backoffice and the other
way round.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `write:webhooks`.

### Request body

Content type `application/json`, required.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string (uri) | Yes | HTTPS, no embedded credentials, not a private, loopback or internal address; not already registered. Pattern `^https://`. |
| `events` | array of string | Yes | The event types to receive. Duplicates are ignored; the order you send is kept. At least 1 item. |
| `description` | string, nullable | No | A label for you. At most 200 characters. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `201` | Endpoint registered. `secret` is shown only here. |  | `application/json` WebhookEndpointWithSecret |
| `400` | `invalid_json`, `unknown_field` (a key other than `url`, `events`, `description`), `invalid_url`, `invalid_events` (not an array, an unknown event type, or empty; `message` says which), `invalid_description` (more than 200 characters) or `webhook_limit_reached` (25 endpoints already). | `invalid_description`, `invalid_events`, `invalid_json`, `invalid_url`, `unknown_field`, `webhook_limit_reached` | `application/json` WebhookApiError |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `404` | The byte-identical "V1 endpoint not found" response returned when the route's module is not enabled for your organisation, and also for a path that does not exist — indistinguishable by design (anti-enumeration), so do not treat it as proof the path is wrong. | `V1 endpoint not found` | `application/json` |
| `409` | `url_already_registered`: this URL is already one of your endpoints. | `url_already_registered` | `application/json` WebhookApiError |
| `413` | The request body exceeded this route's byte cap. | `body_too_large` | `application/json` ErrorEnvelope |
| `415` | `unsupported_media_type`: the body is not `Content-Type: application/json`. | `unsupported_media_type` | `application/json` WebhookApiError |
| `429` | `rate_limit_exceeded`: too many requests. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying. The test delivery has a tighter limit than the other operations. | `rate_limit_exceeded` | `application/json` WebhookApiError |
| `500` | `webhook_secret_encryption_failed`: sealing the secret failed; nothing was written. Retry. | `webhook_secret_encryption_failed` | `application/json` WebhookApiError |
| `503` | `webhooks_unavailable`: a temporary fault, or `webhook_secret_kek_required`: encryption of webhook secrets is not available for your organisation, so nothing was written. Retry with backoff. | `webhook_secret_kek_required`, `webhooks_unavailable` | `application/json` WebhookApiError |
| `503` | Noxtica could not read the key's record. This is a temporary fault on Noxtica's side and says nothing about your key. Retry after the `Retry-After` delay; do not treat it as a revoked key. | `api_key_auth_unavailable` | `application/json` |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |
| `503` | `Retry-After` | integer | Seconds to wait before retrying. |

**Response fields** (`201`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `webhook` | object (WebhookEndpoint) | Yes | One webhook endpoint. The signing secret is never part of it. |
| `webhook.id` | string | Yes |  |
| `webhook.url` | string (uri) | Yes | The HTTPS endpoint Noxtica POSTs to. |
| `webhook.events` | array of string | Yes | The event types the endpoint receives. Exact match, no wildcard. |
| `webhook.description` | string, nullable | Yes | At most 200 characters. |
| `webhook.enabled` | boolean | Yes | A disabled endpoint is kept but receives nothing. |
| `webhook.secretSealed` | boolean | Yes | `true` when the signing secret is stored encrypted, which the session and review events (`identity` events) require. `false` for an endpoint created before encrypted storage; rotate its secret once. |
| `webhook.createdAt` | integer (int64) | Yes | Unix ms. |
| `webhook.updatedAt` | integer (int64) | Yes | Unix ms. |
| `webhook.lastDeliveryAt` | integer (int64), nullable | Yes | Unix ms of the last delivery attempt |
| `webhook.lastStatus` | string, nullable | Yes | The result of the last attempt: `ok`, `http_<status>`, `blocked` (the URL was refused), `error`, `secret_plaintext_ineligible`; `null` before the first attempt. |
| `secret` | string | Yes | The signing secret. Shown once; store it now. The HMAC key is its UTF-8 bytes. |
| `requestId` | string | No |  |

**Response example** (`201`)

```json
{
  "ok": true,
  "webhook": {
    "id": "7f3a2c10-5b8e-4d2a-9c61-0e4a7b9d1f35",
    "url": "https://hooks.example.com/noxtica",
    "events": [
      "kyc.session.created",
      "kyc.session.opened",
      "kyc.session.completed",
      "kyc.review.decided"
    ],
    "description": "Onboarding backend",
    "enabled": true,
    "secretSealed": true,
    "createdAt": 1790760000000,
    "updatedAt": 1790760000000,
    "lastDeliveryAt": null,
    "lastStatus": null
  },
  "secret": "whsec_example_do_not_use_0123456789abcdef",
  "requestId": "req-test"
}
```

### Example request

**curl**

```bash
curl -X POST "https://collect.noxtica.com/v1/webhooks" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://hooks.example.com/noxtica",
  "events": [
    "kyc.session.created",
    "kyc.session.opened",
    "kyc.session.completed",
    "kyc.review.decided"
  ],
  "description": "Onboarding backend"
}'
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/webhooks', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    url: 'https://hooks.example.com/noxtica',
    events: [
      'kyc.session.created',
      'kyc.session.opened',
      'kyc.session.completed',
      'kyc.review.decided',
    ],
    description: 'Onboarding backend',
  }),
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/webhooks",
    data=json.dumps({
        "url": "https://hooks.example.com/noxtica",
        "events": [
            "kyc.session.created",
            "kyc.session.opened",
            "kyc.session.completed",
            "kyc.review.decided",
        ],
        "description": "Onboarding backend",
    }).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
        "Content-Type": "application/json",
    },
    method="POST",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## Read a webhook endpoint {#getWebhookEndpoint}

`GET /v1/webhooks/{webhookId}`

Server-to-server. Returns one endpoint of your organisation. The signing secret is never returned. An id of another organisation is indistinguishable from an unknown id (`404 webhook_not_found`).

**Authentication:** Bearer token (`ApiKeyAuth`), scope `read:webhooks`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `webhookId` | path | string | Yes | The id of one of your webhook endpoints (`id` of the endpoint object). Pattern `^[A-Za-z0-9._:-]{1,128}$`. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The endpoint. |  | `application/json` WebhookEndpointEnvelope |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `404` | `webhook_not_found`: no endpoint with this id exists in your organisation (an id of another organisation gets the same answer). | `webhook_not_found` | `application/json` WebhookApiError |
| `429` | `rate_limit_exceeded`: too many requests. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying. The test delivery has a tighter limit than the other operations. | `rate_limit_exceeded` | `application/json` WebhookApiError |
| `503` | `webhooks_unavailable`: a temporary fault, or `webhook_secret_kek_required`: encryption of webhook secrets is not available for your organisation, so nothing was written. Retry with backoff. | `webhooks_unavailable` | `application/json` WebhookApiError |
| `503` | Noxtica could not read the key's record. This is a temporary fault on Noxtica's side and says nothing about your key. Retry after the `Retry-After` delay; do not treat it as a revoked key. | `api_key_auth_unavailable` | `application/json` |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |
| `503` | `Retry-After` | integer | Seconds to wait before retrying. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `webhook` | object (WebhookEndpoint) | Yes | One webhook endpoint. The signing secret is never part of it. |
| `webhook.id` | string | Yes |  |
| `webhook.url` | string (uri) | Yes | The HTTPS endpoint Noxtica POSTs to. |
| `webhook.events` | array of string | Yes | The event types the endpoint receives. Exact match, no wildcard. |
| `webhook.description` | string, nullable | Yes | At most 200 characters. |
| `webhook.enabled` | boolean | Yes | A disabled endpoint is kept but receives nothing. |
| `webhook.secretSealed` | boolean | Yes | `true` when the signing secret is stored encrypted, which the session and review events (`identity` events) require. `false` for an endpoint created before encrypted storage; rotate its secret once. |
| `webhook.createdAt` | integer (int64) | Yes | Unix ms. |
| `webhook.updatedAt` | integer (int64) | Yes | Unix ms. |
| `webhook.lastDeliveryAt` | integer (int64), nullable | Yes | Unix ms of the last delivery attempt |
| `webhook.lastStatus` | string, nullable | Yes | The result of the last attempt: `ok`, `http_<status>`, `blocked` (the URL was refused), `error`, `secret_plaintext_ineligible`; `null` before the first attempt. |
| `requestId` | string | No |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "webhook": {
    "id": "7f3a2c10-5b8e-4d2a-9c61-0e4a7b9d1f35",
    "url": "https://hooks.example.com/noxtica",
    "events": [
      "kyc.session.created",
      "kyc.session.completed"
    ],
    "description": null,
    "enabled": true,
    "secretSealed": true,
    "createdAt": 1790760000000,
    "updatedAt": 1790760000000,
    "lastDeliveryAt": 1790762445000,
    "lastStatus": "ok"
  },
  "requestId": "req-test"
}
```

### Example request

**curl**

```bash
curl "https://collect.noxtica.com/v1/webhooks/example-webhookId" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/webhooks/example-webhookId', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/webhooks/example-webhookId",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## Change a webhook endpoint {#updateWebhookEndpoint}

`PUT /v1/webhooks/{webhookId}`

Server-to-server. Partial update: send only the members you change (`url`, `events`, `description`, `enabled`); at least one is required. `events` replaces the whole list. Set `enabled`
to `false` to pause deliveries without losing the endpoint; events that occur while it is disabled are not queued for it. The signing secret is not changed (see `rotate-secret`).

**Authentication:** Bearer token (`ApiKeyAuth`), scope `write:webhooks`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `webhookId` | path | string | Yes | The id of one of your webhook endpoints (`id` of the endpoint object). Pattern `^[A-Za-z0-9._:-]{1,128}$`. |

### Request body

Content type `application/json`, required.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string (uri) | No | Pattern `^https://`. |
| `events` | array of string | No | Replaces the whole list. At least 1 item. |
| `description` | string, nullable | No | At most 200 characters. |
| `enabled` | boolean | No |  |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The updated endpoint. |  | `application/json` WebhookEndpointEnvelope |
| `400` | `invalid_json`, `unknown_field`, `empty_update` (no member sent), `invalid_url`, `invalid_events`, `invalid_description` or `invalid_enabled` (not a boolean). | `empty_update`, `invalid_description`, `invalid_enabled`, `invalid_events`, `invalid_json`, `invalid_url`, `unknown_field` | `application/json` WebhookApiError |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `404` | `webhook_not_found`: no endpoint with this id exists in your organisation (an id of another organisation gets the same answer). | `webhook_not_found` | `application/json` WebhookApiError |
| `409` | `url_already_registered`: another of your endpoints already uses this URL. | `url_already_registered` | `application/json` WebhookApiError |
| `413` | The request body exceeded this route's byte cap. | `body_too_large` | `application/json` ErrorEnvelope |
| `415` | `unsupported_media_type`: the body is not `Content-Type: application/json`. | `unsupported_media_type` | `application/json` WebhookApiError |
| `429` | `rate_limit_exceeded`: too many requests. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying. The test delivery has a tighter limit than the other operations. | `rate_limit_exceeded` | `application/json` WebhookApiError |
| `503` | `webhooks_unavailable`: a temporary fault, or `webhook_secret_kek_required`: encryption of webhook secrets is not available for your organisation, so nothing was written. Retry with backoff. | `webhooks_unavailable` | `application/json` WebhookApiError |
| `503` | Noxtica could not read the key's record. This is a temporary fault on Noxtica's side and says nothing about your key. Retry after the `Retry-After` delay; do not treat it as a revoked key. | `api_key_auth_unavailable` | `application/json` |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |
| `503` | `Retry-After` | integer | Seconds to wait before retrying. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `webhook` | object (WebhookEndpoint) | Yes | One webhook endpoint. The signing secret is never part of it. |
| `webhook.id` | string | Yes |  |
| `webhook.url` | string (uri) | Yes | The HTTPS endpoint Noxtica POSTs to. |
| `webhook.events` | array of string | Yes | The event types the endpoint receives. Exact match, no wildcard. |
| `webhook.description` | string, nullable | Yes | At most 200 characters. |
| `webhook.enabled` | boolean | Yes | A disabled endpoint is kept but receives nothing. |
| `webhook.secretSealed` | boolean | Yes | `true` when the signing secret is stored encrypted, which the session and review events (`identity` events) require. `false` for an endpoint created before encrypted storage; rotate its secret once. |
| `webhook.createdAt` | integer (int64) | Yes | Unix ms. |
| `webhook.updatedAt` | integer (int64) | Yes | Unix ms. |
| `webhook.lastDeliveryAt` | integer (int64), nullable | Yes | Unix ms of the last delivery attempt |
| `webhook.lastStatus` | string, nullable | Yes | The result of the last attempt: `ok`, `http_<status>`, `blocked` (the URL was refused), `error`, `secret_plaintext_ineligible`; `null` before the first attempt. |
| `requestId` | string | No |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "webhook": {
    "id": "7f3a2c10-5b8e-4d2a-9c61-0e4a7b9d1f35",
    "url": "https://hooks.example.com/noxtica",
    "events": [
      "kyc.session.created",
      "kyc.session.completed",
      "kyb.session.completed"
    ],
    "description": "Onboarding backend",
    "enabled": true,
    "secretSealed": true,
    "createdAt": 1790760000000,
    "updatedAt": 1790763000000,
    "lastDeliveryAt": 1790762445000,
    "lastStatus": "ok"
  },
  "requestId": "req-test"
}
```

### Example request

**curl**

```bash
curl -X PUT "https://collect.noxtica.com/v1/webhooks/example-webhookId" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "events": [
    "kyc.session.created",
    "kyc.session.completed",
    "kyb.session.completed"
  ],
  "enabled": true
}'
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/webhooks/example-webhookId', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    events: [
      'kyc.session.created',
      'kyc.session.completed',
      'kyb.session.completed',
    ],
    enabled: true,
  }),
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/webhooks/example-webhookId",
    data=json.dumps({
        "events": [
            "kyc.session.created",
            "kyc.session.completed",
            "kyb.session.completed",
        ],
        "enabled": True,
    }).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
        "Content-Type": "application/json",
    },
    method="PUT",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## Delete a webhook endpoint {#deleteWebhookEndpoint}

`DELETE /v1/webhooks/{webhookId}`

Server-to-server. Deletes the endpoint. Deliveries still waiting for it are dropped.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `write:webhooks`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `webhookId` | path | string | Yes | The id of one of your webhook endpoints (`id` of the endpoint object). Pattern `^[A-Za-z0-9._:-]{1,128}$`. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The endpoint was deleted. |  | `application/json` WebhookEndpointDeleted |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `404` | `webhook_not_found`: no endpoint with this id exists in your organisation (an id of another organisation gets the same answer). | `webhook_not_found` | `application/json` WebhookApiError |
| `429` | `rate_limit_exceeded`: too many requests. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying. The test delivery has a tighter limit than the other operations. | `rate_limit_exceeded` | `application/json` WebhookApiError |
| `503` | `webhooks_unavailable`: a temporary fault, or `webhook_secret_kek_required`: encryption of webhook secrets is not available for your organisation, so nothing was written. Retry with backoff. | `webhooks_unavailable` | `application/json` WebhookApiError |
| `503` | Noxtica could not read the key's record. This is a temporary fault on Noxtica's side and says nothing about your key. Retry after the `Retry-After` delay; do not treat it as a revoked key. | `api_key_auth_unavailable` | `application/json` |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |
| `503` | `Retry-After` | integer | Seconds to wait before retrying. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `id` | string | Yes |  |
| `removed` | boolean | Yes | Allowed values: `true`. |
| `requestId` | string | No |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "id": "7f3a2c10-5b8e-4d2a-9c61-0e4a7b9d1f35",
  "removed": true,
  "requestId": "req-test"
}
```

### Example request

**curl**

```bash
curl -X DELETE "https://collect.noxtica.com/v1/webhooks/example-webhookId" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/webhooks/example-webhookId', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/webhooks/example-webhookId",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="DELETE",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## Rotate the signing secret of an endpoint {#rotateWebhookSecret}

`POST /v1/webhooks/{webhookId}/rotate-secret`

Server-to-server. Mints a new signing secret, returns it **once**, and stops the previous secret from verifying **at once**: deliveries sent after this call are signed with the new secret, so
update your receiver first or accept both for a moment. The secret is stored encrypted, which is what the session and review events require: rotate an endpoint that was created before
encrypted storage once before you subscribe it to them (`secretSealed` is `false` until you do). The call has no body.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `write:webhooks`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `webhookId` | path | string | Yes | The id of one of your webhook endpoints (`id` of the endpoint object). Pattern `^[A-Za-z0-9._:-]{1,128}$`. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The endpoint and its new secret. `secret` is shown only here. |  | `application/json` WebhookEndpointWithSecret |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `404` | `webhook_not_found`: no endpoint with this id exists in your organisation (an id of another organisation gets the same answer). | `webhook_not_found` | `application/json` WebhookApiError |
| `429` | `rate_limit_exceeded`: too many requests. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying. The test delivery has a tighter limit than the other operations. | `rate_limit_exceeded` | `application/json` WebhookApiError |
| `500` | `webhook_secret_encryption_failed`: sealing the new secret failed; the existing secret is unchanged. Retry. | `webhook_secret_encryption_failed` | `application/json` WebhookApiError |
| `503` | `webhooks_unavailable`: a temporary fault, or `webhook_secret_kek_required`: encryption of webhook secrets is not available for your organisation, so nothing was written. Retry with backoff. | `webhooks_unavailable` | `application/json` WebhookApiError |
| `503` | Noxtica could not read the key's record. This is a temporary fault on Noxtica's side and says nothing about your key. Retry after the `Retry-After` delay; do not treat it as a revoked key. | `api_key_auth_unavailable` | `application/json` |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |
| `503` | `Retry-After` | integer | Seconds to wait before retrying. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `webhook` | object (WebhookEndpoint) | Yes | One webhook endpoint. The signing secret is never part of it. |
| `webhook.id` | string | Yes |  |
| `webhook.url` | string (uri) | Yes | The HTTPS endpoint Noxtica POSTs to. |
| `webhook.events` | array of string | Yes | The event types the endpoint receives. Exact match, no wildcard. |
| `webhook.description` | string, nullable | Yes | At most 200 characters. |
| `webhook.enabled` | boolean | Yes | A disabled endpoint is kept but receives nothing. |
| `webhook.secretSealed` | boolean | Yes | `true` when the signing secret is stored encrypted, which the session and review events (`identity` events) require. `false` for an endpoint created before encrypted storage; rotate its secret once. |
| `webhook.createdAt` | integer (int64) | Yes | Unix ms. |
| `webhook.updatedAt` | integer (int64) | Yes | Unix ms. |
| `webhook.lastDeliveryAt` | integer (int64), nullable | Yes | Unix ms of the last delivery attempt |
| `webhook.lastStatus` | string, nullable | Yes | The result of the last attempt: `ok`, `http_<status>`, `blocked` (the URL was refused), `error`, `secret_plaintext_ineligible`; `null` before the first attempt. |
| `secret` | string | Yes | The signing secret. Shown once; store it now. The HMAC key is its UTF-8 bytes. |
| `requestId` | string | No |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "webhook": {
    "id": "7f3a2c10-5b8e-4d2a-9c61-0e4a7b9d1f35",
    "url": "https://hooks.example.com/noxtica",
    "events": [
      "kyc.session.created",
      "kyc.session.completed"
    ],
    "description": null,
    "enabled": true,
    "secretSealed": true,
    "createdAt": 1790760000000,
    "updatedAt": 1790764000000,
    "lastDeliveryAt": 1790762445000,
    "lastStatus": "ok"
  },
  "secret": "whsec_example_do_not_use_fedcba9876543210",
  "requestId": "req-test"
}
```

### Example request

**curl**

```bash
curl -X POST "https://collect.noxtica.com/v1/webhooks/example-webhookId/rotate-secret" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/webhooks/example-webhookId/rotate-secret', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/webhooks/example-webhookId/rotate-secret",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="POST",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## Send a test delivery to an endpoint {#sendWebhookTestEvent}

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

Server-to-server. Sends **one** signed sample delivery of an event type to the endpoint, with the same four headers, envelope and signature as a real delivery, and reports what your
receiver answered. Use it to check reachability and signature verification before you rely on an endpoint, and to see the exact payload of any event.

`eventType` picks the event (any type of `GET /v1/webhooks/event-types`, whether or not the endpoint subscribes to it); without it the first event of the endpoint is used. The sample carries
obvious placeholder data (ids such as `test-session`), never real data, and its delivery id starts with `test_`: ignore deliveries with that prefix in your business logic. A test delivery is
a single attempt: it is not retried, it is not recorded in the delivery list, and it updates the endpoint's `lastStatus`. The test is audited before it leaves. A tighter rate limit than the
other operations applies.

The call answers `200` whenever the attempt was made, even when your receiver refused it: read `delivered`, `status` and `result`. The session and review events are signed only with a sealed
secret; for an endpoint that does not have one the answer carries `error: secret_plaintext_ineligible` and nothing is sent: rotate the secret, then test again.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `write:webhooks`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `webhookId` | path | string | Yes | The id of one of your webhook endpoints (`id` of the endpoint object). Pattern `^[A-Za-z0-9._:-]{1,128}$`. |

### Request body

Content type `application/json`, optional.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `eventType` | string | No | The event to send a sample of. Default is the first event of the endpoint (`submission.scored` when it has none). Allowed values: `kyc.session.created`, `kyc.session.opened`, `kyc.session.verifying`, `kyc.session.review_pending`, `kyc.session.completed`, `kyc.session.unverified`, `kyc.session.error`, `kyc.session.expired`, `kyc.session.cancelled`, `kyc.review.decided`, `kyc.verification.completed`, `kyc.erasure.completed`, `kyc.document.expiry_warning`, `kyc.reverification.due`, `kyb.session.created`, `kyb.session.opened`, `kyb.session.submitted`, `kyb.session.under_review`, `kyb.session.completed`, `kyb.session.expired`, `kyb.session.abandoned`, `kyb.entity.created`, `kyb.entity.under_review`, `kyb.entity.closed`, `kym.session.created`, `kym.session.opened`, `kym.session.submitted`, `kym.session.under_review`, `kym.session.completed`, `kym.session.expired`, `kym.session.abandoned`, `kym.case.status_changed`, `kym.alert.created`, `submission.scored`, `risk.flagged`, `risk.high`, `risk.cluster_flagged`, `account.agent_linked`, `pulse.detection.v1`, `pulse.enrolment.linked.v1`, and 1 more. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The attempt was made (or refused for the reason in `error`). |  | `application/json` WebhookTestResult |
| `400` | `invalid_json`, `unknown_field` or `invalid_event_type` (not a type of the event catalog). | `invalid_event_type`, `invalid_json`, `unknown_field` | `application/json` WebhookApiError |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `404` | `webhook_not_found`: no endpoint with this id exists in your organisation (an id of another organisation gets the same answer). | `webhook_not_found` | `application/json` WebhookApiError |
| `413` | The request body exceeded this route's byte cap. | `body_too_large` | `application/json` ErrorEnvelope |
| `415` | `unsupported_media_type`: a body was sent that is not `Content-Type: application/json`. | `unsupported_media_type` | `application/json` WebhookApiError |
| `429` | `rate_limit_exceeded`: too many requests. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying. The test delivery has a tighter limit than the other operations. | `rate_limit_exceeded` | `application/json` WebhookApiError |
| `503` | `webhooks_unavailable`: a temporary fault, or `webhook_secret_kek_required`: encryption of webhook secrets is not available for your organisation, so nothing was written. Retry with backoff. | `webhooks_unavailable` | `application/json` WebhookApiError |
| `503` | Noxtica could not read the key's record. This is a temporary fault on Noxtica's side and says nothing about your key. Retry after the `Retry-After` delay; do not treat it as a revoked key. | `api_key_auth_unavailable` | `application/json` |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |
| `503` | `Retry-After` | integer | Seconds to wait before retrying. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | The attempt was made. Allowed values: `true`. |
| `delivered` | boolean | Yes | `true` when your receiver answered with a `2xx`. |
| `eventType` | string | Yes |  |
| `deliveryId` | string | Yes | The delivery id sent in `X-Noxtica-Delivery`; starts with `test_`. |
| `status` | integer, nullable | Yes | The HTTP status your receiver answered |
| `result` | string | Yes | The outcome in the `lastStatus` vocabulary: `ok`, `http_<status>`, `blocked`, `error`, `secret_plaintext_ineligible`. |
| `ms` | integer | Yes | How long the attempt took |
| `error` | string | No | Present when nothing was sent because the endpoint's secret is not sealed and the event needs one. Allowed values: `secret_plaintext_ineligible`. |
| `message` | string | No |  |
| `requestId` | string | No |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "delivered": true,
  "eventType": "kyc.session.created",
  "deliveryId": "test_5b8e4d2a-9c61-4a7b-8f35-0e4a7b9d1f35",
  "status": 200,
  "result": "ok",
  "ms": 183,
  "requestId": "req-test"
}
```

### Example request

**curl**

```bash
curl -X POST "https://collect.noxtica.com/v1/webhooks/example-webhookId/test" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "eventType": "kyc.session.created"
}'
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/webhooks/example-webhookId/test', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    eventType: 'kyc.session.created',
  }),
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/webhooks/example-webhookId/test",
    data=json.dumps({
        "eventType": "kyc.session.created",
    }).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
        "Content-Type": "application/json",
    },
    method="POST",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## List recent deliveries of an endpoint {#listWebhookDeliveries}

`GET /v1/webhooks/{webhookId}/deliveries`

Server-to-server. Returns the recent deliveries of KYC, KYB and KYM events to one endpoint, newest first: what happened to each (still retrying, delivered, refused for good, or dead after the
retry budget). The list is a notification surface, never authority, and it never holds a URL, a body or a secret: `lastError` is a closed code. Deliveries of the other products (risk scoring,
account and Pulse events) are not listed; their outcome is the endpoint's `lastStatus`. Test deliveries are not listed.

Page with `cursor`: pass the `nextCursor` of the previous page unchanged; `nextCursor` is `null` on the last page.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `read:webhooks`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `webhookId` | path | string | Yes | The id of one of your webhook endpoints (`id` of the endpoint object). Pattern `^[A-Za-z0-9._:-]{1,128}$`. |
| `limit` | query | integer | No | Page size, 1 to 50 (default 25). A value outside the range is refused with `400 limit_out_of_bounds`. Minimum 1, maximum 50. Default `25`. |
| `cursor` | query | string | No | The `nextCursor` of the previous page, unchanged. Anything else is refused with `400 invalid_cursor`. At most 320 characters. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | One page of deliveries. |  | `application/json` WebhookDeliveryList |
| `400` | `invalid_query_parameters`, `limit_out_of_bounds` or `invalid_cursor`. | `invalid_cursor`, `invalid_query_parameters`, `limit_out_of_bounds` | `application/json` WebhookApiError |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `404` | `webhook_not_found`: no endpoint with this id exists in your organisation (an id of another organisation gets the same answer). | `webhook_not_found` | `application/json` WebhookApiError |
| `429` | `rate_limit_exceeded`: too many requests. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying. The test delivery has a tighter limit than the other operations. | `rate_limit_exceeded` | `application/json` WebhookApiError |
| `503` | `webhooks_unavailable`: a temporary fault, or `webhook_secret_kek_required`: encryption of webhook secrets is not available for your organisation, so nothing was written. Retry with backoff. | `webhooks_unavailable` | `application/json` WebhookApiError |
| `503` | Noxtica could not read the key's record. This is a temporary fault on Noxtica's side and says nothing about your key. Retry after the `Retry-After` delay; do not treat it as a revoked key. | `api_key_auth_unavailable` | `application/json` |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |
| `503` | `Retry-After` | integer | Seconds to wait before retrying. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `deliveries` | array of WebhookDelivery | Yes |  |
| `deliveries[]` | object (WebhookDelivery) | No | The outcome of one delivery of a KYC, KYB or KYM event to one endpoint. |
| `deliveries[].id` | string | Yes | The delivery id; equals the `X-Noxtica-Delivery` header and the envelope `id`. |
| `deliveries[].eventType` | string | Yes | A webhook event type, exactly as it appears in the envelope `type`, the `X-Noxtica-Event` header and an endpoint's `events`. Allowed values: `kyc.session.created`, `kyc.session.opened`, `kyc.session.verifying`, `kyc.session.review_pending`, `kyc.session.completed`, `kyc.session.unverified`, `kyc.session.error`, `kyc.session.expired`, `kyc.session.cancelled`, `kyc.review.decided`, `kyc.verification.completed`, `kyc.erasure.completed`, `kyc.document.expiry_warning`, `kyc.reverification.due`, `kyb.session.created`, `kyb.session.opened`, `kyb.session.submitted`, `kyb.session.under_review`, `kyb.session.completed`, `kyb.session.expired`, `kyb.session.abandoned`, `kyb.entity.created`, `kyb.entity.under_review`, `kyb.entity.closed`, `kym.session.created`, `kym.session.opened`, `kym.session.submitted`, `kym.session.under_review`, `kym.session.completed`, `kym.session.expired`, `kym.session.abandoned`, `kym.case.status_changed`, `kym.alert.created`, `submission.scored`, `risk.flagged`, `risk.high`, `risk.cluster_flagged`, `account.agent_linked`, `pulse.detection.v1`, `pulse.enrolment.linked.v1`, and 1 more. |
| `deliveries[].state` | string | Yes | `retrying`: the last attempt failed and another is scheduled. `delivered`: your receiver answered with a `2xx`. `terminal`: refused for good (a `4xx`, a refused URL, or a secret that cannot sign this event). `dead`: the retry budget is spent. Allowed values: `retrying`, `delivered`, `terminal`, `dead`. |
| `deliveries[].attempts` | integer | Yes | Minimum 0. |
| `deliveries[].httpStatus` | integer, nullable | Yes | The HTTP status of the last attempt |
| `deliveries[].lastError` | string, nullable | Yes | A closed code, `null` for a delivered row: `http_<status>`, `timeout`, `network_error`, `blocked_url`, `secret_plaintext_ineligible`, `retries_exhausted`. |
| `deliveries[].createdAt` | integer (int64) | Yes | Unix ms the delivery was first recorded. |
| `deliveries[].deliveredAt` | integer (int64), nullable | Yes | Unix ms of the successful attempt |
| `nextCursor` | string, nullable | Yes | Pass it as `cursor` for the next page; `null` on the last page. |
| `requestId` | string | No |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "deliveries": [
    {
      "id": "6d3c3a8e-6b4b-4a8f-8f4e-0a0e6b3d9d21:kyc.session.completed:7f3a2c10-5b8e-4d2a-9c61-0e4a7b9d1f35",
      "eventType": "kyc.session.completed",
      "state": "delivered",
      "attempts": 1,
      "httpStatus": 200,
      "lastError": null,
      "createdAt": 1790762445000,
      "deliveredAt": 1790762446000
    },
    {
      "id": "6d3c3a8e-6b4b-4a8f-8f4e-0a0e6b3d9d21:kyc.session.review_pending:7f3a2c10-5b8e-4d2a-9c61-0e4a7b9d1f35",
      "eventType": "kyc.session.review_pending",
      "state": "retrying",
      "attempts": 2,
      "httpStatus": 503,
      "lastError": "http_503",
      "createdAt": 1790762400000,
      "deliveredAt": null
    }
  ],
  "nextCursor": null,
  "requestId": "req-test"
}
```

### Example request

**curl**

```bash
curl "https://collect.noxtica.com/v1/webhooks/example-webhookId/deliveries" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/webhooks/example-webhookId/deliveries', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/webhooks/example-webhookId/deliveries",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

_This page is generated from the Noxtica OpenAPI specification release `v0.16.1`: `v1.yaml` version `1.5.1-2026-11-03` (SHA-256 `41508500e857`). Do not edit it by hand._
