[relaybell docs › Developers › Server SDKs](https://relaybell.com/docs/server-sdks) · [All docs as Markdown](https://relaybell.com/docs.md) · [Index for AI assistants](https://relaybell.com/llms.txt)

# Server SDKs

> The Node.js, Python and PHP server SDKs, including listing notifications and verifying webhooks.

Thin wrappers over the REST API for your **server** — install from your package manager. They use your secret API key, so never put them (or the key) in code that runs in the browser; on web pages, use the `relaybell-sdk.js` script tag from the [Quickstart](https://relaybell.com/docs/quickstart).

| Language | Install |
| --- | --- |
| Node.js | `npm install relaybell` |
| Python | `pip install relaybell` |
| PHP | `composer require relaybell/relaybell-php` |

> **Note:** Prefer no package manager? Zipped builds are also available: [Node](https://relaybell.com/relaybell-node-sdk.zip) · [PHP](https://relaybell.com/relaybell-php-sdk.zip) · [Python](https://relaybell.com/relaybell-python-sdk.zip).

```js tab="Node.js"
// Node
const { RelayBell } = require("relaybell");
const rb = new RelayBell({ apiKey: "rb_live_xxxxx" });
await rb.send({ title: "Hello", body: "World", url: "https://example.com" });
await rb.track("purchase", { externalId: "user_123", properties: { value: 42 } });
```

```php tab="PHP"
<?php // PHP
$rb = new Relaybell\Client("rb_live_xxxxx");
$rb->send(["title" => "Hello", "body" => "World", "url" => "https://example.com"]);
```

```python tab="Python"
# Python
from relaybell import RelayBell
rb = RelayBell("rb_live_xxxxx")
rb.send(title="Hello", body="World", url="https://example.com")
```

Each SDK can also make the [`userHash`](https://relaybell.com/docs/identity) for identity verification, with no network call: Node `userHash(secret, externalId)`, PHP `Relaybell\Client::userHash($secret, $externalId)`, Python `user_hash(secret, external_id)` (Python SDK 1.1.0 or newer).

**Version 1.2.0** of all three adds reading your sends — Node and PHP `listNotifications(params)` and `getNotification(id)`, Python `list_notifications(**params)` and `get_notification(id)`, for [`GET /api/v1/notifications`](https://relaybell.com/docs/analytics-reports#notifications-list) (`from`, `to`, `status`, `q`, `limit`, `cursor`, `tz`) and `/:id` — and [webhook verification](https://relaybell.com/docs/webhooks#webhook-verify) in one call (last column). The Node SDK also ships TypeScript types, and throws if it is created in a browser, where your API key would be exposed.

| SDK | Options | Errors | Verify a webhook (1.2.0) |
| --- | --- | --- | --- |
| Node | `new RelayBell({ apiKey, baseUrl?, timeout? })`: `timeout` in ms (default 30000), `baseUrl` defaults to `https://relaybell.com`. | A non-2xx answer, network failure or timeout throws `RelayBellError` with `.status`, `.body` and `.code`. | `verifyWebhook({ payload, headers, secret, tolerance = 300 })`: `payload` is the raw body (string or Buffer), `headers` an object or `Headers`. Returns the parsed event; throws `RelayBellWebhookError` with `.code`. |
| Python | `RelayBell(api_key, base_url="https://relaybell.com", timeout=30.0)` (seconds). | Raises `relaybell.RelayBellError` with `.status` and `.body`. | `verify_webhook(payload: bytes, headers, secret, tolerance=300)`: returns the event as a dict; raises `relaybell.WebhookVerificationError` with `.code`. |
| PHP | `new Relaybell\Client($apiKey, $baseUrl = "https://relaybell.com", ["timeout" => 30, "connectTimeout" => …])` (seconds). | Throws `Relaybell\RelaybellException` (`getStatusCode()`). | `Relaybell\Webhook::verify($payload, $headers, $secret, $tolerance = 300)`: `$headers` may be `getallheaders()` or `$_SERVER`. Returns the event as an array; throws `Relaybell\WebhookVerificationException` (`getReason()`). |

Each verify function checks `X-Relaybell-Webhook-Signature` (the timestamp within `tolerance` seconds, the signature in constant time) and fails with one of these codes: `missing_secret` (an empty secret: it never verifies without one), `missing_signature`, `malformed_signature`, `timestamp_out_of_tolerance`, `signature_mismatch`, `invalid_payload`. The older `X-Relaybell-Signature` header is only accepted when you pass `allowLegacy: true` (Python `allow_legacy=True`, PHP a fifth argument `true`) and the request has no `v1` signature.

**Field names are the REST API's camelCase**: `externalId`, `externalIds`, `transactional`, `segmentId`, `sendAt`. Node and PHP pass fields through as they are, so a snake_case targeting field such as `external_id` is refused with `400 unknown_field`. The Python SDK 1.2.0 also takes snake_case keyword arguments and turns them into camelCase before sending — `external_id`, `external_ids`, `segment_id`, `exclude_segment_id`, `exclude_filters`, `topic_id`, `send_at`, `per_timezone`, `local_time`, `require_interaction`, `smart_time`, `ab_test` (with `auto_winner`, `test_percent`, `after_hours`) — and camelCase keeps working.
