Webhooks
Webhooks allow your application to receive real-time notifications when events occur in Tuberalytics. Instead of polling the API, register a webhook endpoint and we'll send HTTP POST requests to your URL.
Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/webhooks |
List your webhook endpoints |
| POST | /api/v1/webhooks |
Create a webhook endpoint |
| PATCH | /api/v1/webhooks/:id |
Update a webhook endpoint |
| DELETE | /api/v1/webhooks/:id |
Delete a webhook endpoint |
Event Types
| Event | Description |
|---|---|
channel.analysis.completed |
AI channel analysis finished successfully |
channel.analysis.failed |
AI channel analysis failed |
niche.synthesis.completed |
Niche synthesis completed |
niche.research.completed |
Niche research run completed |
keywords.generated |
Keyword generation completed |
competitors.discovered |
Competitor discovery completed |
outlier.detected.niche |
New outlier video detected in a tracked niche |
outlier.detected.competitor |
New outlier video from a competitor channel |
outliers.weekly.niche |
Weekly digest: top 100 outliers from your niches |
outliers.weekly.competitor |
Weekly digest: top 100 outliers from tracked competitors |
video.new.competitor |
New video published by a competitor |
Webhook Payload Format
All webhook deliveries are HTTP POST requests with a JSON body:
{
"event": "channel.analysis.completed",
"data": {
"channel_id": 170,
"channel_title": "Nick Saraev",
"analysis_id": 456
},
"timestamp": "2026-03-30T15:30:00Z"
}
Headers
| Header | Description |
|---|---|
Content-Type |
application/json |
X-Tuberalytics-Signature |
HMAC-SHA256 signature for verification |
X-Tuberalytics-Event |
Event type (e.g. channel.analysis.completed) |
X-Tuberalytics-Delivery |
Unique delivery ID |
Signature Verification
Every webhook delivery is signed with your endpoint's signing secret using HMAC-SHA256. Always verify signatures to ensure requests are from Tuberalytics.
The signature is in the X-Tuberalytics-Signature header, formatted as sha256=<hex_digest>.
Verification Examples
import hmac
import hashlib
def verify_signature(payload_body, signature_header, signing_secret):
expected = "sha256=" + hmac.new(
signing_secret.encode(),
payload_body.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)
require "openssl"
def verify_signature(payload_body, signature_header, signing_secret)
expected = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", signing_secret, payload_body)
Rack::Utils.secure_compare(expected, signature_header)
end
# In your webhook controller:
payload = request.body.read
signature = request.headers["X-Tuberalytics-Signature"]
unless verify_signature(payload, signature, ENV["TUBERALYTICS_WEBHOOK_SECRET"])
head :unauthorized
return
end
const crypto = require("crypto");
function verifySignature(payloadBody, signatureHeader, signingSecret) {
const expected = "sha256=" + crypto
.createHmac("sha256", signingSecret)
.update(payloadBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader)
);
}
function verifySignature($payloadBody, $signatureHeader, $signingSecret) {
$expected = "sha256=" . hash_hmac("sha256", $payloadBody, $signingSecret);
return hash_equals($expected, $signatureHeader);
}
Retry Policy
Failed deliveries are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
After 3 failed attempts, the delivery is marked as permanently failed.
Auto-disable: After 10 consecutive delivery failures across any deliveries, the webhook endpoint is automatically disabled. Re-enable it from the API or web UI after fixing the issue.
List Webhook Endpoints
GET /api/v1/webhooks
List all your registered webhook endpoints.
Example Request
curl -X GET "https://tuberalytics.com/api/v1/webhooks" \
-H "Authorization: Bearer sk_live_your_api_key"
response = requests.get(
"https://tuberalytics.com/api/v1/webhooks",
headers={"Authorization": "Bearer sk_live_your_api_key"}
)
data = response.json()
uri = URI("https://tuberalytics.com/api/v1/webhooks")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer sk_live_your_api_key"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)
const response = await fetch("https://tuberalytics.com/api/v1/webhooks", {
headers: { "Authorization": "Bearer sk_live_your_api_key" }
});
const data = await response.json();
$ch = curl_init("https://tuberalytics.com/api/v1/webhooks");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer sk_live_your_api_key"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$data = json_decode($response, true);
Example Response
{
"data": [
{
"id": 1,
"url": "https://myapp.com/webhooks/tuberalytics",
"events": ["channel.analysis.completed", "competitors.discovered"],
"enabled": true,
"failure_count": 0,
"last_triggered_at": "2026-03-30T10:00:00Z",
"created_at": "2026-03-15T12:00:00Z"
}
]
}
Create Webhook Endpoint
POST /api/v1/webhooks
Register a new webhook endpoint. The response includes the signing_secret — save it immediately, as it is never returned again.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
url |
string | Yes | HTTPS endpoint URL |
events |
array | No | Event types to subscribe to (defaults to all events) |
Example Request
curl -X POST "https://tuberalytics.com/api/v1/webhooks" \
-H "Authorization: Bearer sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"url": "https://myapp.com/webhooks/tuberalytics", "events": ["channel.analysis.completed"]}'
response = requests.post(
"https://tuberalytics.com/api/v1/webhooks",
json={
"url": "https://myapp.com/webhooks/tuberalytics",
"events": ["channel.analysis.completed"]
},
headers={"Authorization": "Bearer sk_live_your_api_key"}
)
data = response.json()
# IMPORTANT: Save data["data"]["signing_secret"] — it's only shown once
uri = URI("https://tuberalytics.com/api/v1/webhooks")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer sk_live_your_api_key"
req["Content-Type"] = "application/json"
req.body = {
url: "https://myapp.com/webhooks/tuberalytics",
events: ["channel.analysis.completed"]
}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)
# IMPORTANT: Save data["data"]["signing_secret"] — it's only shown once
const response = await fetch("https://tuberalytics.com/api/v1/webhooks", {
method: "POST",
headers: {
"Authorization": "Bearer sk_live_your_api_key",
"Content-Type": "application/json"
},
body: JSON.stringify({
url: "https://myapp.com/webhooks/tuberalytics",
events: ["channel.analysis.completed"]
})
});
const data = await response.json();
// IMPORTANT: Save data.data.signing_secret — it's only shown once
$ch = curl_init("https://tuberalytics.com/api/v1/webhooks");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer sk_live_your_api_key",
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"url" => "https://myapp.com/webhooks/tuberalytics",
"events" => ["channel.analysis.completed"]
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$data = json_decode($response, true);
// IMPORTANT: Save $data["data"]["signing_secret"] — it's only shown once
Example Response (HTTP 201)
{
"data": {
"id": 2,
"url": "https://myapp.com/webhooks/tuberalytics",
"events": ["channel.analysis.completed"],
"enabled": true,
"failure_count": 0,
"last_triggered_at": null,
"created_at": "2026-03-30T15:00:00Z",
"signing_secret": "whsec_abc123def456ghi789jkl012"
}
}
Important: The
signing_secretis only returned when the endpoint is first created. Store it securely.
Update Webhook Endpoint
PATCH /api/v1/webhooks/:id
Update a webhook endpoint's URL, events, or enabled status.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
url |
string | No | New HTTPS endpoint URL |
events |
array | No | New event subscriptions |
enabled |
boolean | No | Enable or disable the endpoint |
Example Request
curl -X PATCH "https://tuberalytics.com/api/v1/webhooks/2" \
-H "Authorization: Bearer sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"events": ["channel.analysis.completed", "competitors.discovered"], "enabled": true}'
response = requests.patch(
"https://tuberalytics.com/api/v1/webhooks/2",
json={"events": ["channel.analysis.completed", "competitors.discovered"], "enabled": True},
headers={"Authorization": "Bearer sk_live_your_api_key"}
)
data = response.json()
uri = URI("https://tuberalytics.com/api/v1/webhooks/2")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer sk_live_your_api_key"
req["Content-Type"] = "application/json"
req.body = { events: ["channel.analysis.completed", "competitors.discovered"], enabled: true }.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)
const response = await fetch("https://tuberalytics.com/api/v1/webhooks/2", {
method: "PATCH",
headers: {
"Authorization": "Bearer sk_live_your_api_key",
"Content-Type": "application/json"
},
body: JSON.stringify({ events: ["channel.analysis.completed", "competitors.discovered"], enabled: true })
});
const data = await response.json();
$ch = curl_init("https://tuberalytics.com/api/v1/webhooks/2");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PATCH");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer sk_live_your_api_key",
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"events" => ["channel.analysis.completed", "competitors.discovered"],
"enabled" => true
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$data = json_decode($response, true);
Example Response
{
"data": {
"id": 2,
"url": "https://myapp.com/webhooks/tuberalytics",
"events": ["channel.analysis.completed", "competitors.discovered"],
"enabled": true,
"failure_count": 0,
"last_triggered_at": null,
"created_at": "2026-03-30T15:00:00Z"
}
}
Delete Webhook Endpoint
DELETE /api/v1/webhooks/:id
Permanently delete a webhook endpoint. Any pending deliveries will be cancelled.
Example Request
curl -X DELETE "https://tuberalytics.com/api/v1/webhooks/2" \
-H "Authorization: Bearer sk_live_your_api_key"
response = requests.delete(
"https://tuberalytics.com/api/v1/webhooks/2",
headers={"Authorization": "Bearer sk_live_your_api_key"}
)
data = response.json()
uri = URI("https://tuberalytics.com/api/v1/webhooks/2")
req = Net::HTTP::Delete.new(uri)
req["Authorization"] = "Bearer sk_live_your_api_key"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)
const response = await fetch("https://tuberalytics.com/api/v1/webhooks/2", {
method: "DELETE",
headers: { "Authorization": "Bearer sk_live_your_api_key" }
});
const data = await response.json();
$ch = curl_init("https://tuberalytics.com/api/v1/webhooks/2");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer sk_live_your_api_key"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$data = json_decode($response, true);
Example Response
{
"data": {
"message": "Webhook endpoint deleted."
}
}
Weekly Digest Events
Two digest events deliver a curated list of outlier videos every Monday at 10am UTC.
outliers.weekly.niche
Top 100 outlier videos from the past 7 days across all niches your channels belong to. Niches are deduplicated — if two of your channels share a niche, you receive one digest covering all unique niches.
Example Payload
{
"event": "outliers.weekly.niche",
"data": {
"period_start": "2026-03-26",
"period_end": "2026-04-02",
"total_outliers": 47,
"videos": [
{
"video_id": 123,
"youtube_video_id": "abc123",
"title": "Video Title",
"channel_title": "Channel Name",
"channel_id": 456,
"niche_name": "home fitness",
"niche_id": 15,
"view_count": 500000,
"average_views_ratio": 12.5,
"published_at": "2026-03-28T10:00:00Z"
}
]
},
"timestamp": "2026-04-02T10:00:00Z"
}
outliers.weekly.competitor
Top 100 outlier videos from the past 7 days published by your tracked competitors, ordered by average_views_ratio descending.
Example Payload
{
"event": "outliers.weekly.competitor",
"data": {
"period_start": "2026-03-26",
"period_end": "2026-04-02",
"total_outliers": 23,
"videos": [
{
"video_id": 789,
"youtube_video_id": "xyz789",
"title": "Competitor Video Title",
"channel_title": "Competitor Channel",
"channel_id": 101,
"view_count": 250000,
"average_views_ratio": 8.3,
"published_at": "2026-03-29T14:00:00Z"
}
]
},
"timestamp": "2026-04-02T10:00:00Z"
}
Videos are ordered by average_views_ratio descending and capped at 100 per digest. Both digests can be configured from the API Keys page in the web UI or via the webhooks API.