# 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. 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"])
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.raybot.app/start/dev/webhooks-api.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
