Episode alerts for your own apps
The same instant feed that powers the NotifyBot Telegram bot — every new serial episode, preview, trailer and movie across 12 OTT platforms — delivered to your server as JSON.
/v1/alerts with a cursor.Quick start
- Buy an API plan from an admin, then open the bot and send
/api. Your API key arrives in a private message — it is shown only once. - Call the API from your server. The first successful call locks the key to that server's IP (see IP lock).
- Poll for new episodes, or register a webhook with
/api webhook add https://your-server/hook.
curl -H "Authorization: Bearer nb_YOUR_KEY" \
"https://notification.ottdlbot.online/v1/alerts?since=1790500000&limit=50"
Authentication
Send your key with every request, in either header:
Authorization: Bearer nb_YOUR_KEY
X-Api-Key: nb_YOUR_KEY
Keys start with nb_. Lost or leaked a key? Tap 🔑 New API key in /api — the old key stops working immediately.
Server IP lock
Each personal key works from one server IP. The first server that calls the API is registered automatically; requests from any other IP are refused with 403 ip_not_allowed.
- Moving servers? Send
/api ip 203.0.113.10in the bot. The IP can be changed once every 7 days. - Webhooks must point to that same server — the webhook host has to resolve to your registered IP.
- Calling from behind a proxy or NAT? Register the public IP the API sees (it is shown in the
403error message).
Rate limits
A burst of 30 requests, refilling at 1 request per second per key. Over the limit you get 429 with {"error":"rate_limited","retry_after":1}. Polling /v1/alerts every 5–10 seconds is well within the limit; webhooks need no polling at all.
Endpoints
New episodes in the order they were detected. The main endpoint for polling.
| Query | Type | Description |
|---|---|---|
since | number | Unix time (seconds). Returns alerts detected after it. Default: last hour. |
platform | string | Only one platform, e.g. ZEE5. See platforms. |
limit | int | 1–200, default 50. |
Response: {"alerts": [Episode…], "next_since": 1790500433.48}. Pass next_since back as since to page forward; it is null when you are caught up.
{
"alerts": [
{
"platform": "ZEE5",
"show_id": "0-6-4z5723311",
"serial": "Manetana",
"season": "1",
"epnum": 560,
"epname": "Manetana - September 27, 2026",
"ctype": "Serials",
"language": "Kannada",
"release_date": "27-September-2026",
"release_ts": 1790447400.0,
"created_at": 1790500433.48,
"url": "https://notification.ottdlbot.online/w/…",
"image": "https://akamaividz2.zee5.com/image/upload/…jpg",
"key": "0-1-6z51082258",
"spoiler": false
}
],
"next_since": null
}
Search the catalogue of tracked shows.
| Query | Type | Description |
|---|---|---|
q | string | Title search (case-insensitive). |
platform | string | Filter by platform. |
language | string | Filter by language, e.g. Marathi. |
active | bool | 1 = currently airing only, 0 = not airing. |
limit / cursor | int | Page size (1–200, default 50) and offset. Use next_cursor from the response. |
Response: {"shows":[{"show_id","platform","serial","language","last_ep","episode_count","active","poster","last_episode_at"}…], "total": 1834, "next_cursor": 50}
Episode history of one show, newest first.
| Query | Type | Description |
|---|---|---|
before | number | Only episodes detected before this Unix time. Use next_before to page back. |
limit | int | 1–200, default 50. |
curl -H "X-Api-Key: nb_YOUR_KEY" \
"https://notification.ottdlbot.online/v1/shows/SonyLIV/1700000195/episodes?limit=5"
No key needed. Returns {"ok": true, "service": "notifybot-api"} — use it for uptime checks.
Episode object
| Field | Type | Description |
|---|---|---|
platform | string | OTT platform, e.g. JioHotstar. |
show_id | string | Platform's show id. |
serial | string | Show / movie title. |
season, epnum | string, int | Season and episode number (empty for movies). Batch cards use a range like "11-14". |
epname | string | Episode title. |
ctype | string | Serials, Previews, Trailers or Movies. |
language, languages | string, string[] | Primary language and all audio languages when known. |
release_ts, release_date | number, string | Platform's release time (Unix seconds) and date label. |
created_at | number | When we detected it (Unix seconds). This is the since cursor. |
url | string | Watch link. Opens the episode on the OTT platform. |
image | string | Poster / thumbnail URL. |
key | string | Platform's content id — unique per episode. Use it to de-duplicate. |
spoiler | bool | true for previews / promos. |
Errors
| Status | error | Meaning |
|---|---|---|
| 401 | unauthorized | Missing or invalid API key. |
| 403 | ip_not_allowed | Request came from an IP other than your registered server. |
| 403 | subscription_expired | Your API plan has ended — renew via an admin, then check /api. |
| 403 | forbidden | Key paused or revoked. |
| 429 | rate_limited | Slow down; retry after retry_after seconds. |
| 500 | internal | Our side — retry with backoff. |
Webhooks
Instead of polling, let us POST each new episode to your server the moment it is detected.
- Add up to 2 URLs in the bot:
/api webhook add https://your-server/hook(list with/api webhook, remove with/api webhook del 1). - The URL's host must resolve to your registered server IP.
- Reply with any
2xxwithin 8 seconds. Failed deliveries are retried after 5 s, 15 s, 45 s, 2 min and 5 min. - A delivery can arrive more than once — de-duplicate on
delivery_id(or the episodekey).
POST /hook HTTP/1.1
Content-Type: application/json
User-Agent: NotifyBot-Push/1.0
X-NB-Timestamp: 1790500434
X-NB-Signature: sha256=5f0c…e91a
{"type": "episode.released", "delivery_id": "alert:…:9f3a", "episode": { …Episode object… }}
Verify signatures
Get your signing secret from 🔏 Webhook secret in /api. Compute HMAC-SHA256(secret, timestamp + "." + raw_body) and compare it with X-NB-Signature. Reject requests whose timestamp is more than 5 minutes old.
import hmac, hashlib, time
def verified(secret: str, ts: str, signature: str, raw_body: bytes) -> bool:
if abs(time.time() - int(ts)) > 300:
return False
mac = hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest("sha256=" + mac, signature)
const crypto = require("crypto");
function verified(secret, ts, signature, rawBody) {
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const mac = crypto.createHmac("sha256", secret).update(ts + ".").update(rawBody).digest("hex");
const a = Buffer.from("sha256=" + mac), b = Buffer.from(signature);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
function verified($secret, $ts, $signature, $rawBody) {
if (abs(time() - intval($ts)) > 300) return false;
$mac = hash_hmac('sha256', $ts . '.' . $rawBody, $secret);
return hash_equals('sha256=' . $mac, $signature);
}
Code examples
Poll for new episodes
import time, requests
API = "https://notification.ottdlbot.online"
HEADERS = {"Authorization": "Bearer nb_YOUR_KEY"}
since = time.time() - 3600
seen = set()
while True:
r = requests.get(f"{API}/v1/alerts", headers=HEADERS,
params={"since": since, "limit": 100}, timeout=15)
if r.status_code == 429:
time.sleep(r.json().get("retry_after", 1)); continue
r.raise_for_status()
data = r.json()
for ep in data["alerts"]:
if ep["key"] in seen:
continue
seen.add(ep["key"])
print(f'{ep["platform"]} · {ep["serial"]} E{ep["epnum"]} · {ep["language"]} → {ep["url"]}')
since = max(since, ep["created_at"])
if data["next_since"] is None:
time.sleep(10)
const API = "https://notification.ottdlbot.online";
const headers = { Authorization: "Bearer nb_YOUR_KEY" };
let since = Date.now() / 1000 - 3600;
async function poll() {
const res = await fetch(`${API}/v1/alerts?since=${since}&limit=100`, { headers });
if (res.status === 429) return setTimeout(poll, 1000);
const data = await res.json();
for (const ep of data.alerts) {
console.log(`${ep.platform} · ${ep.serial} E${ep.epnum} → ${ep.url}`);
since = Math.max(since, ep.created_at);
}
setTimeout(poll, data.next_since === null ? 10000 : 0);
}
poll();
curl -s -H "X-Api-Key: nb_YOUR_KEY" \
"https://notification.ottdlbot.online/v1/alerts?since=$(( $(date +%s) - 3600 ))&platform=SonyLIV" | jq '.alerts[] | {serial, epnum, url}'
Receive webhooks (Python / Flask)
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = "ws_YOUR_SIGNING_SECRET"
@app.post("/hook")
def hook():
if not verified(SECRET, request.headers.get("X-NB-Timestamp", "0"),
request.headers.get("X-NB-Signature", ""), request.get_data()):
abort(401)
ep = request.get_json()["episode"]
print("New:", ep["serial"], ep["epnum"], ep["url"])
return "", 204
Platforms & values
Platforms (use exactly as written): ZEE5 JioHotstar SonyLIV SunNXT DangalPlay ManoramaMAX TarangPlus PrimeVideo Aha Netflix ETVWin Stage
Content types: Serials Previews Trailers Movies
Languages: full English names — Hindi Bengali Tamil Telugu Kannada Malayalam Marathi Gujarati Odia Punjabi Bhojpuri Haryanvi Rajasthani English and international languages.
Fair use & monitoring
Personal use only. An API plan is for one person's own apps on one server. You may not resell it, share the key, run it on multiple servers, or use it to operate a public bot or channel that redistributes alerts or sells subscriptions.
To protect the service, we record how every key is used: request times, IP addresses and user agents, webhook destinations, and opens of the watch links included in API data (links go through notification.ottdlbot.online/w/… and redirect to the OTT platform). These records are used only for security, abuse prevention and support, and are kept for 90 days.
Keys used from other servers, shared, or used to redistribute alerts are revoked without a refund.
Support
Questions, a new plan or a key reset: message the admin through the NotifyBot Telegram bot. Manage your key, server IP and webhooks anytime with /api.