API Reference
Overview
The BotBell API lets your programs and scripts send push notifications to your Apple devices and receive replies. No SDK required — just simple HTTP requests.
Base URL: https://api.botbell.appHow It Works
- Create a bot in the BotBell app and copy its push URL
- POST a JSON message to the push URL from any language or tool
- Receive an instant push notification on all your devices
- Optionally, receive your replies via Reply URL callback or polling API
Response Format
All API responses use the same JSON envelope. A code of 0 means success; any other code indicates an error.
// Success
{
"code": 0,
"message": "success",
"data": { ... }
}
// Error
{
"code": 40001,
"message": "Invalid bot token"
}Push a Message
Send a notification from your bot to the BotBell app. Your bot's push URL already contains the authentication token — just POST to it.
Endpoint
POST https://api.botbell.app/v1/push/<your_bot_token>No Authorization header needed. The token in the URL is your authentication.
Alternatively, you can use POST /v1/messages/push with the X-Bot-Token: <token> header.
Request Body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | The message text (max 4,096 characters) |
title | string | No | Title displayed above the message (max 256 characters) |
url | string | No | A tappable link attached to the notification |
image_url | string | No | URL of an image to show in the notification |
summary | string | No | Custom summary for long messages. If omitted, auto-truncated from message (max 512 chars) |
format | string | No | Message format: 'text' (default) or 'markdown' for Markdown rendering |
actions_description | string | No | Description text shown above action buttons (max 256 chars) |
actions | array | No | Quick reply buttons (max 5). See Actions below. |
Action Fields
| Field | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Action identifier returned when user taps (max 64 chars) |
label | string | Yes | Button text displayed to user (max 64 chars) |
type | string | No | "button" (default) sends label as reply; "input" opens a text field for custom input |
placeholder | string | No | Placeholder text for input field (max 128 chars, only for type "input") |
{
"message": "Deploy v2.3.0 to production?",
"title": "Deploy Request",
"actions_description": "How would you like to proceed?",
"actions": [
{"key": "approve", "label": "Approve"},
{"key": "reject", "label": "Reject"},
{"key": "custom", "label": "Other...", "type": "input", "placeholder": "Enter your reason"}
]
}Example Request
curl -X POST https://api.botbell.app/v1/push/bt_your_token \
-H "Content-Type: application/json" \
-d '{
"message": "Server CPU is at 95%!",
"title": "Alert",
"url": "https://grafana.example.com/dashboard"
}'Response
{
"code": 0,
"message": "success",
"data": {
"message_id": "msg_abc123",
"delivered": true,
"timestamp": 1709827200
}
}| Field | Description |
|---|---|
message_id | Unique ID for this message |
delivered | true if the push notification was sent to APNs successfully; false if no device is registered |
timestamp | Unix timestamp (seconds) when the message was created |
Try It Now
Paste your bot token and send a real push notification to your device.
Receiving Replies
When you reply to a bot in the app, BotBell can forward your reply to your server automatically. This is optional — you only need this if your bot program handles two-way conversations.
Setting Up a Reply URL
In the BotBell app, go to your bot's settings and enter a Reply URL (e.g. https://your-server.com/botbell/reply). BotBell will POST your replies to this URL as they happen.
Callback Payload
When you reply, BotBell sends a POST request to your Reply URL with the following format:
POST https://your-server.com/botbell/reply
Headers:
Content-Type: application/json
X-Webhook-Signature: sha256=abc123...
X-Webhook-Timestamp: 1709827300
Body:
{
"event": "user_reply",
"bot_id": "bot_abc123",
"message_id": "msg_xyz789",
"content": "Got it, thanks!",
"timestamp": 1709827300,
"action": "approve"
}Signature Verification
Each callback includes an HMAC-SHA256 signature so you can verify it came from BotBell. The signing key is the Webhook Secret shown in your bot settings.
signature = HMAC-SHA256(
key: <your_webhook_secret>,
message: "<timestamp>.<raw_request_body>"
)To verify the signature:
- Extract the X-Webhook-Timestamp and X-Webhook-Signature headers
- Check that the timestamp is within 5 minutes of the current time (to prevent replay attacks)
- Compute HMAC-SHA256 over the string "<timestamp>.<raw_body>" using your Webhook Secret as the key
- Compare the computed value with the signature after the "sha256=" prefix
Your Server's Response
Return any HTTP 2xx status code to acknowledge receipt. Non-2xx responses or timeouts (5 seconds) are treated as failures — the reply will be stored in the polling queue for 24 hours so you can retrieve it later.
Polling for Replies
If you don't have a public server to receive callbacks, you can poll for replies instead. This is also useful as a fallback when your Reply URL is temporarily down.
Endpoint
GET https://api.botbell.app/v1/messages/poll
Headers:
X-Bot-Token: <your_bot_token>Query Parameters
| Field | Type | Default | Description |
|---|---|---|---|
since | integer | - | Only return messages after this Unix timestamp |
limit | integer | 20 | Number of messages to return (max 100) |
Response
{
"code": 0,
"message": "success",
"data": {
"messages": [
{
"message_id": "msg_xyz789",
"content": "Got it, thanks!",
"timestamp": 1709827300,
"action": "approve"
}
],
"has_more": false
}
}Note: Polled messages are marked as read and will not be returned again. Unpolled messages expire after 24 hours.
Error Codes
| Code | HTTP Status | Description |
|---|---|---|
| 40001 | 401 | Invalid or missing bot token |
| 40010 | 400 | Request validation failed (e.g. missing message field, body too large) |
| 40029 | 429 | Rate limit exceeded — too many requests per minute |
| 40030 | 403 | Monthly message quota exhausted |
| 40031 | 403 | Bot limit reached for your plan |
| 50000 | 500 | Internal server error |
Rate Limits & Quotas
| Limit | Value |
|---|---|
| Push rate (per bot) | 30 |
| Message body (characters) | 4,096 |
| Title (characters) | 256 |
| Free monthly quota | 300 messages / account |
| Poll message retention | 24h |
| Reply URL timeout | 5s |
Code Examples
Sending Messages
curl -X POST https://api.botbell.app/v1/push/bt_your_token \
-H "Content-Type: application/json" \
-d '{"message":"Server CPU at 95%","title":"Alert"}'import requests
resp = requests.post(
"https://api.botbell.app/v1/push/bt_your_token",
json={
"message": "Build #42 succeeded",
"title": "CI/CD",
"url": "https://ci.example.com/builds/42",
},
)
print(resp.json()) # {"code": 0, "data": {"message_id": "...", ...}}const resp = await fetch("https://api.botbell.app/v1/push/bt_your_token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: "New order received!",
title: "E-Commerce",
}),
});
const data = await resp.json();
console.log(data);package main
import (
"bytes"
"encoding/json"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]string{
"message": "Disk usage above 90%",
"title": "Alert",
})
http.Post(
"https://api.botbell.app/v1/push/bt_your_token",
"application/json",
bytes.NewReader(body),
)
}<?php
$ch = curl_init('https://api.botbell.app/v1/push/bt_your_token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'message' => 'Payment received: $49.99',
'title' => 'Stripe',
]),
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);Verifying Reply Signatures
import hmac, hashlib
def verify_signature(payload: bytes, timestamp: str, signature: str, secret: str) -> bool:
message = f"{timestamp}.{payload.decode()}"
expected = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)const crypto = require("crypto");
function verifySignature(payload, timestamp, signature, secret) {
const message = `${timestamp}.${payload}`;
const expected = crypto.createHmac("sha256", secret).update(message).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(`sha256=${expected}`),
Buffer.from(signature),
);
}