# Webhooks overview

> Receive real-time SMS events from 23 Telecom — impression, click, conversion and delivery webhooks with configurable payload fields, per-workspace routing and HMAC signing.
> Source: https://docs.23telecom.co.uk/webhooks/overview/

Instructions for LLMs: This is one page of the 23 Telecom messaging API docs
(SMS today; more channels planned). Base URL: https://restlink23telecom.com/api/v1,
auth via the X-API-Key header. Match errors on the error_code field, never on
description text. Full docs: https://docs.23telecom.co.uk/llms-full.txt · Schemas: https://docs.23telecom.co.uk/openapi.yaml

Webhooks push events to your server the moment they happen — no polling.
Each event type goes to its own URL, configured per workspace.

## Event types

| Type | Fires when |
| --- | --- |
| `impression` | SMS accepted by the gateway (API single-send path) |
| `click` | Recipient clicks a short URL (optional per-recipient dedup) |
| `conversion` | Postback received via `/track` or createAction |
| `delivery` | Carrier returns a delivery report (DLR) |

## Configure

In the [portal](https://restlink23telecom.com) under **Webhooks**, or via API:

`PUT /api/v1/user/webhooks` (permission: `webhooks.write`)

```
PUT https://restlink23telecom.com/api/v1/user/webhooks
Header: X-API-Key: <your key>
```
*(The web page shows this example in cURL, Node.js, Python, PHP, Ruby, Java, Go and .NET.)*

  `PUT` only touches the fields you send. To disable a channel, send `null`
  (or `""`) as its URL. Reading settings back requires `webhooks.read`;
  the signing secret is never echoed — only `signing_secret_set: true|false`.

### Validation errors

`PUT /user/webhooks` returns `400` with stable typed codes:

| Code | When |
| --- | --- |
| `INVALID_URL` | URL is not a string or fails SSRF validation (`""`/`null` are valid — they disable the channel) |
| `INVALID_SECRET` | `signing_secret` empty, longer than 64 chars, or wrong type |
| `INVALID_FIELDS` | `*_fields` is null, empty, or contains an unknown field name |
| `INVALID_TOGGLE` | `unique_clicks_only` is not a boolean |
| `CONFLICTING_FIELDS` | `signing_secret` and `clear_signing_secret: true` sent together |
| `INVALID_BODY` | JSON parse error |

## Choose your payload fields

You pick which fields each webhook carries (defaults shown in the example
above). The full field list per event is in
[delivery webhook](/webhooks/delivery#payload-fields). `workspace_id` is
**opt-in** for every event type — add it to the relevant `*_fields` list if
you route multiple workspaces to one receiver.

## Click dedup (`unique_clicks_only`)

When `true`, the click webhook fires only on the **first human click** per
(broadcast, recipient) pair; repeat clicks are skipped. Bot clicks never fire
webhooks. Applies to broadcast links only — API auto-shortened links always
deliver every human click.

## Test & inspect

`POST /api/v1/user/webhooks/test` (permission: `webhooks.write`)

```
POST https://restlink23telecom.com/api/v1/user/webhooks/test
Header: X-API-Key: <your key>
```
*(The web page shows this example in cURL, Node.js, Python, PHP, Ruby, Java, Go and .NET.)*

`GET /api/v1/user/webhooks/logs` (permission: `webhooks.read`)

```
GET https://restlink23telecom.com/api/v1/user/webhooks/logs
Header: X-API-Key: <your key>
```
*(The web page shows this example in cURL, Node.js, Python, PHP, Ruby, Java, Go and .NET.)*

Logs include the payload sent, response code, response time and error details
for every delivery attempt.

## Next

- [Delivery webhook](/webhooks/delivery) — payload, retries, receiver examples
- [Securing webhooks](/webhooks/security) — HMAC signatures, secret rotation
- [Tracker postbacks](/webhooks/postbacks) — conversion ingestion and URL placeholders