> For the complete documentation index, see [llms.txt](https://docs.raybot.app/start/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.raybot.app/start/dev/webhooks-api.md).

# Webhooks API

A webhook is a destination that receives HTTPS POST requests whenever a followed wallet produces a notification — the same events the Telegram bots deliver to chats.

> **Attaching wallets to a webhook is a separate step.** This API only manages the webhook destination itself (URL, label, auth header, pause/resume). To add or remove the addresses that feed into a webhook, reuse the existing `/publicapi/wallets/add`, `/publicapi/wallets/delete`, and `/publicapi/wallets/show` endpoints, passing the webhook's `id` (returned by this API) as their `user_id` parameter and `bot: 0`.

***

#### 🌐 Base URL

All requests are made to:

```agda
https://webapi.raybot.app
```

***

### 🔐 Authentication

Same as the rest of `/publicapi` — obtain an API key with `/api` in the RayBot Telegram Bot and pass it on every request.

> **Required Query Parameters:**
>
> * `api_user` (string, required): Your API user ID
> * `token` (string, required): API authentication token

```atom
GET /publicapi/webhooks?api_user=your_user_id&token=your_api_key
```

#### Rate Limits

**Public API endpoints (**`/publicapi`**)**: — 5 requests per 10 seconds

#### Plan gating

Webhooks are a paid feature. Your plan determines how many **active** webhooks you can have at once (paused and expired webhooks do not count toward the cap). On a free plan the cap is `0` — `POST /publicapi/webhooks` returns `403 WEBHOOK_NOT_AVAILABLE`.

***

### 📋 Endpoints

#### **List Webhooks**

**Endpoint:** `GET /publicapi/webhooks`

Return every webhook owned by the caller (excluding soft-deleted ones), along with the plan's webhook limit and the number currently in use.

**Parameters:**

> **Query Parameters:**
>
> * `api_user` (string, required): Your API user ID
> * `token` (string, required): API authentication token

**Response:**

```json
{
  "webhooks": [
    {
      "id": "17408000000012345",
      "url": "https://example.com/hook",
      "label": "prod-alerts",
      "auth_header_set": true,
      "active": true,
      "status": "send",
      "consecutive_failures": 0,
      "address_count": 12,
      "created_at": "2026-04-17T10:15:00.000Z"
    }
  ],
  "limit": 5,
  "used": 1
}
```

> * `id` — use this as `user_id` when calling `/publicapi/wallets/*` to attach addresses.
> * `auth_header_set` — whether an `Authorization` header is configured. The value itself is never returned.
> * `status` — one of `send`, `pause`, `expired`. Only `send` counts toward `used`.

> **Status Codes:**
>
> * `200`: Success
> * `400`: Invalid authentication parameters
> * `401`: Invalid authentication
> * `429`: Rate limit exceeded

***

#### **Create Webhook**

**Endpoint:** `POST /publicapi/webhooks`

Register a new webhook destination. The server sends a verification `POST` to the URL during creation and refuses to persist the webhook if that call does not return a 2xx response.

**Parameters:**

> **Query Parameters:**
>
> * `api_user` (string, required): Your API user ID
> * `token` (string, required): API authentication token

**Request Body:**

```json
{
  "url": "https://example.com/hook",
  "label": "prod-alerts",
  "auth_header": "Bearer my-secret"
}
```

> **Validation Rules:**
>
> * `url` required, ≤500 characters, must be `https://`. Hostnames `localhost`, `.local`, `.internal`, and private / loopback / link-local / ULA IPs (both IPv4 and IPv6) are rejected.
> * `label` optional, ≤50 characters.
> * `auth_header` optional, ≤200 characters. Sent verbatim as the `Authorization` header on deliveries.

**Verification Payload**

While creating the webhook, the server POSTs this to your URL (5-second timeout, no redirects):

```json
{
  "event_type": "test",
  "timestamp": "2026-04-17T10:15:00.000Z",
  "message": "RayBot webhook verification"
}
```

Headers: `Content-Type: application/json`, `User-Agent: RayBot-Webhook/1.0`, `X-RayBot-Event: test`, plus `Authorization: <auth_header>` if you set one. Reply with any 2xx status to accept.

**Response:**

```json
{
  "id": "17408000000012345",
  "url": "https://example.com/hook",
  "label": "prod-alerts",
  "auth_header_set": true,
  "active": true,
  "status": "send",
  "consecutive_failures": 0,
  "address_count": 0,
  "created_at": "2026-04-17T10:15:00.000Z"
}
```

> **Status Codes:**
>
> * `201`: Webhook created
> * `400`: Validation error (`INVALID_URL`, `INVALID_LABEL`, `INVALID_AUTH_HEADER`)
> * `401`: Invalid authentication
> * `403`: Plan limits — `WEBHOOK_NOT_AVAILABLE` (plan has 0 webhooks) or `WEBHOOK_LIMIT_REACHED` (already at cap)
> * `422`: `TEST_FAILED` — the verification POST did not succeed; the response body includes `upstream_status` and `error`
> * `429`: Rate limit exceeded

***

#### **Get Webhook**

**Endpoint:** `GET /publicapi/webhooks/{id}`

Return a single webhook by id.

**Parameters:**

> **Query Parameters:**
>
> * `api_user` (string, required): Your API user ID
> * `token` (string, required): API authentication token
>
> **Path Parameters:**
>
> * `id` (string, required): Webhook id returned from create/list.

**Response:** Same object shape as an item in the list response.

> **Status Codes:**
>
> * `200`: Success
> * `401`: Invalid authentication
> * `404`: `WEBHOOK_NOT_FOUND`
> * `429`: Rate limit exceeded

***

#### **Update Webhook**

**Endpoint:** `PATCH /publicapi/webhooks/{id}`

Update the mutable fields on a webhook. The URL itself is immutable — to change it, delete and recreate the webhook.

**Parameters:**

> **Query Parameters:**
>
> * `api_user` (string, required): Your API user ID
> * `token` (string, required): API authentication token
>
> **Path Parameters:**
>
> * `id` (string, required): Webhook id

**Request Body** (any subset):

```json
{
  "label": "new-label",
  "auth_header": "Bearer rotated",
  "active": false
}
```

> **Validation / Semantics:**
>
> * `label` — same rules as create. `null` or `""` clears it.
> * `auth_header` — same rules as create. `null` or `""` removes the stored header.
> * `active` — toggle delivery:
>   * `false` → pauses the webhook (`status: "pause"`) and sets every active address on the webhook to `pause`.
>   * `true` → resumes delivery (`status: "send"`), resets `consecutive_failures` to `0`, and restores paused addresses to `active`.
> * Passing a `url` field returns `400 INVALID_URL`.

**Response:** Full webhook object after the update (same shape as create).

> **Status Codes:**
>
> * `200`: Updated
> * `400`: `INVALID_URL` (URL change attempted), `INVALID_LABEL`, `INVALID_AUTH_HEADER`
> * `401`: Invalid authentication
> * `404`: `WEBHOOK_NOT_FOUND`
> * `429`: Rate limit exceeded

***

#### **Delete Webhook**

**Endpoint:** `DELETE /publicapi/webhooks/{id}`

Delete the webhook. All addresses attached to it are also deleted. Allowed regardless of plan (free users can clean up after downgrade).

**Parameters:**

> **Query Parameters:**
>
> * `api_user` (string, required): Your API user ID
> * `token` (string, required): API authentication token
>
> **Path Parameters:**
>
> * `id` (string, required): Webhook id

**Response:** Empty body.

> **Status Codes:**
>
> * `204`: Deleted
> * `401`: Invalid authentication
> * `404`: `WEBHOOK_NOT_FOUND`
> * `429`: Rate limit exceeded

***

#### **Send Test Delivery**

**Endpoint:** `POST /publicapi/webhooks/{id}/test`

Send the same verification payload as on creation, using the webhook's current URL and auth header. Useful for confirming a rotated `auth_header` or diagnosing delivery failures. Does not touch `consecutive_failures` and does not auto-disable the webhook.

**Parameters:**

> **Query Parameters:**
>
> * `api_user` (string, required): Your API user ID
> * `token` (string, required): API authentication token
>
> **Path Parameters:**
>
> * `id` (string, required): Webhook id

**Response (success):**

```json
{
  "status_code": 200,
  "latency_ms": 142
}
```

> **Status Codes:**
>
> * `200`: Upstream returned 2xx
> * `401`: Invalid authentication
> * `404`: `WEBHOOK_NOT_FOUND`
> * `422`: `TEST_FAILED` — body includes `upstream_status`, `error`, `latency_ms`
> * `429`: Rate limit exceeded

***

### 🔗 Managing Addresses

Use the existing `/publicapi/wallets/*` endpoints, setting:

* `user_id` = the webhook's `id` (from list / create / get)
* `bot` = `0`

```atom
POST /publicapi/wallets/add?api_user=...&token=...
{
  "user_id": "17408000000012345",
  "bot": 0,
  "wallets": [
    { "wallet_address": "0xabc...", "wallet_name": "main" }
  ]
}
```

Addresses attached to a webhook respect the wallet limits that applied when the webhook was created. Use `/publicapi/wallets/settings` the same way to adjust per-address tx filters.

***

### 🚨 Error Codes

Errors returned by this API use a structured envelope:

```json
{
  "error": {
    "code": "WEBHOOK_LIMIT_REACHED",
    "message": "Webhook limit reached",
    "details": { "limit": 5, "used": 5 }
  }
}
```

| Code                    | HTTP      | When                                                                                     |
| ----------------------- | --------- | ---------------------------------------------------------------------------------------- |
| `UNAUTHORIZED`          | 400 / 401 | Missing or invalid `api_user`/`token`                                                    |
| `INVALID_URL`           | 400       | URL is malformed, not https, too long, or points at a private/internal host              |
| `INVALID_LABEL`         | 400       | `label` exceeds 50 characters                                                            |
| `INVALID_AUTH_HEADER`   | 400       | `auth_header` exceeds 200 characters                                                     |
| `WEBHOOK_NOT_FOUND`     | 404       | No webhook with that id owned by the caller                                              |
| `WEBHOOK_NOT_AVAILABLE` | 403       | Plan limit is 0                                                                          |
| `WEBHOOK_LIMIT_REACHED` | 403       | Plan limit reached — body includes `limit`, `used`                                       |
| `TEST_FAILED`           | 422       | Verification POST returned non-2xx or errored — body includes `upstream_status`, `error` |
| `RATE_LIMIT_EXCEEDED`   | 429       | `/publicapi` rate limit hit                                                              |

***

### 📦 Delivery Payload

Real notifications (not test pings) arrive as HTTPS POSTs with these headers:

```
Content-Type: application/json
User-Agent: RayBot-Webhook/1.0
X-RayBot-Event: <event_type>
X-RayBot-Delivery-Id: <uuid>
Authorization: <your auth_header>    # if configured
```

The body carries the same event schema the Telegram bots render — swaps, transfers, token mints, etc. See [Example Payloads](/start/dev/webhooks-api/example-payloads.md) for real (sanitized) deliveries of the most common event types. After 10 consecutive failed deliveries the webhook auto-disables (`active: false`, `status: "pause"`); use `PATCH … { "active": true }` to re-enable once you've fixed the receiver.

***

### 🔧 SDKs and Examples

#### **JavaScript/Node.js Example**

```javascript
const axios = require('axios');

const apiClient = axios.create({
  baseURL: 'https://webapi.raybot.app',
  timeout: 30000,
});

async function createWebhook(apiUser, token, { url, label, authHeader }) {
  const res = await apiClient.post(
    `/publicapi/webhooks?api_user=${apiUser}&token=${token}`,
    { url, label, auth_header: authHeader }
  );
  return res.data;
}

async function attachAddresses(apiUser, token, webhookId, wallets) {
  const res = await apiClient.post(
    `/publicapi/wallets/add?api_user=${apiUser}&token=${token}`,
    { user_id: webhookId, bot: 0, wallets }
  );
  return res.data;
}

(async () => {
  const webhook = await createWebhook('api_user_id', 'your_api_token', {
    url: 'https://example.com/hook',
    label: 'prod-alerts',
    authHeader: 'Bearer my-secret',
  });

  await attachAddresses('api_user_id', 'your_api_token', webhook.id, [
    { wallet_address: '0xabc...', wallet_name: 'main' },
  ]);

  console.log('Webhook ready:', webhook.id);
})();
```

#### **Python Example**

```python
import requests

class RayBotWebhooks:
    def __init__(self, base_url, api_user, token):
        self.base_url = base_url
        self.params = {"api_user": api_user, "token": token}

    def create(self, url, label=None, auth_header=None):
        body = {"url": url, "label": label, "auth_header": auth_header}
        r = requests.post(f"{self.base_url}/publicapi/webhooks", params=self.params, json=body)
        r.raise_for_status()
        return r.json()

    def attach_addresses(self, webhook_id, wallets):
        body = {"user_id": webhook_id, "bot": 0, "wallets": wallets}
        r = requests.post(f"{self.base_url}/publicapi/wallets/add", params=self.params, json=body)
        r.raise_for_status()
        return r.json()

api = RayBotWebhooks("https://webapi.raybot.app", "api_user_id", "your_token")

webhook = api.create(
    url="https://example.com/hook",
    label="prod-alerts",
    auth_header="Bearer my-secret",
)
api.attach_addresses(webhook["id"], [{"wallet_address": "0xabc...", "wallet_name": "main"}])
print("Webhook ready:", webhook["id"])
```
