Zene API & webhooks
Ask Claude, ChatGPT or Cursor about your AI visibility over MCP, pull scores, questions, competitors and alerts into your own dashboards and Looker Studio, trigger scans, and push alerts to Slack, Microsoft Teams, Zapier or any webhook.
Overview
The REST API is part of the Agency plan. It is JSON over HTTPS, read-mostly, and scoped to one workspace per key - a key can only ever see its own workspace's brands. The MCP server for AI assistants is included from Pro (read-only) and gets write tools on Agency. Alert delivery to Slack, Teams and webhooks is included from the Growth plan and needs no code.
| Endpoint | What it returns |
|---|---|
GET /me | The workspace the key belongs to, its plan and your current rate-limit window. Use it as the connection test (Zapier's "test" request). |
GET /brands | Active brands in the workspace with their current visibility score (0-100, the dashboard number) and 7-day change. |
GET /brands/{id} | Overall visibility score (last 30 days) with its daily sparkline, plus the score, change and status per AI engine and the share of answers that mention the brand (mention_rate, 0-1) with its 95% confidence interval. score is null for an engine that hasn't answered successfully yet. |
GET /brands/{id}/questions | Tracked questions with the latest successful answer per engine: whether the brand was mentioned, its position in the answer, the tone, and the sources the engine cited. |
GET /brands/{id}/competitors | Tracked competitors with their visibility per engine over the last 30 days and the average over the engines your brand is scanned on (the same number as the in-app head-to-head). |
GET /brands/{id}/alerts | Smart alerts, newest first: an engine starts or stops naming the brand, tone turns negative or recovers, a score milestone. Poll with since to pick up new ones. |
GET /brands/{id}/trend | Daily visibility score over a window as flat rows - one per day and engine, plus engine: "overall" for the all-engine line. Only days with scan data appear. Made for time-series tools (it is what the Looker Studio connector charts). |
GET /brands/{id}/share-of-voice | Share of voice in AI answers: of the latest answer per tracked question and engine in the window, how many name the brand and each tracked competitor, and each one's share (0-1) of all those mentions. |
GET /brands/{id}/sources | The web domains AI engines cited in the brand's answers, most cited first: citations, answers citing the domain with and without the brand mentioned (the "without" ones are outreach targets), the engines, and whether it is the brand's own site. |
GET /brands/{id}/actions | The brand's action plan - in progress and suggested first, then done - with difficulty, estimated impact and verification status. |
POST /brands/{id}/scans | Queue a fresh scan of the brand's tracked questions. Same rules as "Scan now" in the app: one scan per brand at a time (the running one is returned with already_running: true) and your plan's manual-scan quota per brand per 24 hours (429 with Retry-After). Listen for scan.completed instead of polling. |
POST /hooks | Subscribe a URL to an event (REST hooks - what Zapier calls "subscribe"). Deliveries are signed exactly like webhook channels. Up to 50 hooks per workspace. |
DELETE /hooks/{id} | Unsubscribe. A hook whose URL answers 410 Gone is removed automatically, and revoking an API key removes the hooks it created. |
GET /hooks | The workspace's hooks. |
GET /hooks/sample | A sample delivery body for an event, returned as a bare JSON array (Zapier's "perform list" format) so fields can be mapped before a real event fires. |
MCP server (AI assistants)
Zene speaks the Model Context Protocol, so an AI assistant can answer "how visible are we on Perplexity this month, and who's beating us?" from your live data. Server URL: https://tryzene.com/api/mcp (Streamable HTTP, stateless JSON, protocol 2025-06-18). Authenticate with an API key in the Authorization: Bearer zene_live_… header, exactly like the REST API. Keep the key out of the URL: headers don't end up in proxy logs, browser history or screenshots.
- Pro and Growth: read-only keys and the read tools.
- Agency: read-only or full-access keys; full-access keys also get the write tools.
Write tools never change anything on the first call: without "confirm": true they return a preview of the change, which the assistant should show you before confirming. Every confirmed write is logged in Settings → API. Requests count against the key's 120 requests a minute.
| Tool | Type | What it does |
|---|---|---|
list_brands | Read | List the workspace's active brands with their current AI-visibility score, 7-day change, tracked-question count and last scan time. Start here: every other tool takes a brand_id from this list. Scores are 0-100 (null = not measured yet). |
get_brand_visibility | Read | A brand's overall visibility score (last 30 days, with daily sparkline) and, per AI engine (ChatGPT, Claude, Gemini, Perplexity, ...), its score, change, status and mention rate with a 95% confidence interval. Scores are 0-100 (null = not measured yet). |
list_questions | Read | The brand's tracked questions (prompts) with the latest successful answer per engine: whether the brand was mentioned, its position, the tone, and the sources the engine cited. |
list_competitors | Read | Tracked competitors with their mention rate per engine over the last 30 days (score = % of tracked questions whose latest answer names them) and the average over the engines the brand is scanned on. |
get_share_of_voice | Read | Share of voice in AI answers: of the latest answer per tracked question and engine in the window, how many name the brand and each tracked competitor, and each one's share (0-1) of all those mentions, with per-engine counts. |
list_top_sources | Read | The web domains AI engines cited most when answering the brand's questions in the window: citations, answers citing the domain with / without the brand mentioned (the 'without' ones are outreach targets), engines, and whether it is the brand's own site. |
list_alerts | Read | Smart alerts for the brand, newest first: an engine started or stopped naming the brand, tone shifted, a competitor overtook it, a score milestone. |
get_visibility_trend | Read | Daily visibility score over the window as flat rows { date, engine, score } - engine "overall" is the all-engine line, the others are per engine. Only days with scan data appear. Scores are 0-100 (null = not measured yet). |
list_actions | Read | The brand's recommended actions to improve AI visibility (in progress and suggested first, then done), with difficulty, estimated impact and verification status. Use the ids with complete_action. |
add_question | Write (Agency) | Start tracking a question (a prompt people ask AI assistants) for a brand. Counts against the plan's tracked-question limit. Returns a preview unless confirm is true. The question gets answers on the next scheduled scan or after trigger_scan. |
remove_question | Write (Agency) | Stop tracking a question (its answer history is kept). Returns a preview unless confirm is true. |
add_competitor | Write (Agency) | Start tracking a competitor for a brand (counts against the plan's competitors-per-brand limit). Returns a preview unless confirm is true. |
trigger_scan | Write (Agency) | Queue a fresh scan of the brand's tracked questions on every engine of the plan. Same rules as 'Scan now' in the app: one scan per brand at a time and the plan's manual-scan quota. Results arrive within minutes. Returns a preview unless confirm is true. |
complete_action | Write (Agency) | Mark an action from list_actions as done. Zene snapshots today's visibility and verifies the impact on the following scans. Returns a preview unless confirm is true. |
Claude Desktop
Add Zene to claude_desktop_config.json (Settings → Developer → Edit config) through the mcp-remote bridge, then restart Claude:
{
"mcpServers": {
"zene": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://tryzene.com/api/mcp",
"--header",
"Authorization:${ZENE_AUTH}"
],
"env": {
"ZENE_AUTH": "Bearer zene_live_…"
}
}
}
}Claude Code
claude mcp add --transport http zene https://tryzene.com/api/mcp \
--header "Authorization: Bearer $ZENE_API_KEY"Cursor
In ~/.cursor/mcp.json (or the project's .cursor/mcp.json):
{
"mcpServers": {
"zene": {
"url": "https://tryzene.com/api/mcp",
"headers": {
"Authorization": "Bearer zene_live_…"
}
}
}
}Claude.ai and ChatGPT connectors (deprecated key-in-URL form)
Web connectors can't send a custom header yet, so for them only, the server also accepts the key in the URL: https://tryzene.com/api/mcp/zene_live_…. This form is deprecated: a URL ends up in logs and screenshots, so it is read-only (write tools are always disabled), its responses carry a Deprecation header, and it will be retired once OAuth sign-in is available - we'll announce a date first. Use a dedicated read-only key for it and revoke the key if the URL was shared. Every other client should use the header.
- Claude.ai (plans with custom connectors): Settings → Connectors → Add custom connector, name it Zene, paste the URL above and leave the OAuth fields empty.
- ChatGPT (plans with developer mode): Settings → Apps & Connectors → Advanced → turn on Developer mode, then Create a connector with the URL above and No authentication.
Menu names in these apps change often - look for "custom connector" / "MCP server". OAuth sign-in (instead of a key in the URL) is not available yet; when it is, these connectors will move to it.
Authentication
Create a key in Settings → API (workspace owners and admins). Keys look like zene_live_… and are shown once - we store only a SHA-256 hash. Send the key as a Bearer token on every request:
Authorization: Bearer zene_live_Xy7Qm2Lp…Keys act for the whole workspace, not for the person who created them, and keep working if that person leaves - revoke them in Settings → API. Keep keys server-side: never ship one to a browser or commit it to a repository. A key stops working while the workspace is suspended or off the plan it needs.
Keys have one of two scopes. Full access keys (Agency) work everywhere: the REST API, Zapier and every MCP tool. Read-only keys (Pro and Growth, or Agency on request) work only with the MCP server's read tools - the right key to hand to an AI assistant. A read-only key on the REST API answers 403 insufficient_scope.
Rate limits
Each key may make 120 requests per minute. Every authenticated response carries the current window:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1790414460 # unix seconds when the window resetsOver the limit you get 429 with a Retry-After header (seconds). Requests that end in an error still count. Triggering scans is additionally bound by your plan's manual-scan quota per brand.
Errors
Errors use HTTP status codes and one JSON shape:
{
"error": {
"code": "not_found",
"message": "Brand not found."
}
}| Status | code | When |
|---|---|---|
| 400 | invalid_parameter / invalid_body | A query parameter or JSON body is malformed. |
| 401 | invalid_api_key | Missing, unknown or revoked key. |
| 403 | insufficient_scope | A read-only (MCP) key was used on the REST API. |
| 403 | plan_required | The workspace isn't on the Agency plan (any more). |
| 403 | account_suspended | The workspace is suspended. |
| 404 | not_found | No such brand / hook in this workspace (or no such endpoint). |
| 409 | limit_reached | 50 hooks already exist. |
| 422 | no_tracked_questions | The brand has nothing to scan. |
| 429 | rate_limited | More than 120 requests in the current minute for this key. |
| 429 | scan_quota_exceeded / scan_cooldown | The plan's manual-scan quota or cooldown for this brand. |
| 500 | internal_error | Our fault - retry with backoff. |
Pagination & dates
Successful responses wrap the result in data. Lists take limit (1-100, default 50) and offset and return a pagination object with total and has_more. Timestamps are ISO 8601 in UTC. Scores are integers from 0 to 100; null means "not measured yet", not zero.
Endpoints
GET /me
The workspace the key belongs to, its plan and your current rate-limit window. Use it as the connection test (Zapier's "test" request).
curl https://tryzene.com/api/v1/me \
-H "Authorization: Bearer $ZENE_API_KEY"Response · 200 OK
{
"data": {
"account": {
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"name": "Acme Agency"
},
"plan": "agency",
"key": {
"id": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
"name": "Zapier",
"prefix": "Xy7Qm2Lp",
"created_at": "2026-09-20T10:04:11.52+00:00"
},
"rate_limit": {
"limit": 120,
"remaining": 119,
"reset": 1790414460
}
}
}GET /brands
Active brands in the workspace with their current visibility score (0-100, the dashboard number) and 7-day change.
limit- 1-100, default 50offset- default 0
curl "https://tryzene.com/api/v1/brands?limit=20" \
-H "Authorization: Bearer $ZENE_API_KEY"Response · 200 OK
{
"data": [
{
"id": "7b0c1f2e-4d5a-4c3b-9e8f-1a2b3c4d5e6f",
"name": "Notion",
"website_url": "https://www.notion.com",
"industry": "Productivity software",
"country": "US",
"created_at": "2026-06-02T08:15:00+00:00",
"score": 61,
"delta": 4,
"last_scan_at": "2026-09-26T09:30:00+00:00",
"tracked_questions": 20
}
],
"pagination": {
"limit": 20,
"offset": 0,
"total": 1,
"has_more": false
}
}GET /brands/{id}
Overall visibility score (last 30 days) with its daily sparkline, plus the score, change and status per AI engine and the share of answers that mention the brand (mention_rate, 0-1) with its 95% confidence interval. score is null for an engine that hasn't answered successfully yet.
curl https://tryzene.com/api/v1/brands/7b0c1f2e-4d5a-4c3b-9e8f-1a2b3c4d5e6f \
-H "Authorization: Bearer $ZENE_API_KEY"Response · 200 OK
{
"data": {
"id": "7b0c1f2e-4d5a-4c3b-9e8f-1a2b3c4d5e6f",
"name": "Notion",
"website_url": "https://www.notion.com",
"industry": "Productivity software",
"country": "US",
"created_at": "2026-06-02T08:15:00+00:00",
"tracked_questions": 20,
"last_scan_at": "2026-09-26T09:30:00+00:00",
"score": {
"overall": 61,
"delta": 4,
"sparkline": [
55,
57,
58,
60,
61
],
"window_days": 30
},
"engines": [
{
"engine": "chatgpt",
"score": 72,
"delta": 6,
"status": "ok",
"last_scanned_at": "2026-09-26T09:21:40+00:00",
"sparkline": [
66,
70,
72
],
"mention_rate": 0.75,
"mention_rate_low": 0.53,
"mention_rate_high": 0.89,
"observations": 20
},
{
"engine": "perplexity",
"score": 38,
"delta": -3,
"status": "watch",
"last_scanned_at": "2026-09-26T09:22:02+00:00",
"sparkline": [
41,
40,
38
],
"mention_rate": 0.4,
"mention_rate_low": 0.22,
"mention_rate_high": 0.61,
"observations": 20
}
]
}
}GET /brands/{id}/questions
Tracked questions with the latest successful answer per engine: whether the brand was mentioned, its position in the answer, the tone, and the sources the engine cited.
limit- 1-100, default 50offset- default 0
curl "https://tryzene.com/api/v1/brands/7b0c1f2e-4d5a-4c3b-9e8f-1a2b3c4d5e6f/questions?limit=50" \
-H "Authorization: Bearer $ZENE_API_KEY"Response · 200 OK
{
"data": [
{
"id": "5f1e2d3c-4b5a-4968-8776-655443322110",
"text": "What is the best note-taking app for teams?",
"language": "en",
"topic": "Category",
"country": null,
"created_at": "2026-06-02T08:16:12+00:00",
"results": [
{
"engine": "chatgpt",
"mentioned": true,
"position": 2,
"sentiment": "positive",
"scanned_at": "2026-09-26T09:21:40+00:00",
"citations": [
{
"url": "https://www.notion.com/product",
"title": "Notion - your connected workspace"
}
]
}
]
}
],
"pagination": {
"limit": 50,
"offset": 0,
"total": 20,
"has_more": false
}
}GET /brands/{id}/competitors
Tracked competitors with their visibility per engine over the last 30 days and the average over the engines your brand is scanned on (the same number as the in-app head-to-head).
limit- 1-100, default 50offset- default 0
curl https://tryzene.com/api/v1/brands/7b0c1f2e-4d5a-4c3b-9e8f-1a2b3c4d5e6f/competitors \
-H "Authorization: Bearer $ZENE_API_KEY"Response · 200 OK
{
"data": [
{
"id": "a3b4c5d6-e7f8-4a9b-8c0d-1e2f3a4b5c6d",
"name": "Obsidian",
"website_url": "https://obsidian.md",
"detected_automatically": false,
"added_at": "2026-06-02T08:20:00+00:00",
"score": 44,
"engines": {
"chatgpt": 55,
"claude": 40,
"gemini": 45,
"perplexity": 36
}
}
],
"pagination": {
"limit": 50,
"offset": 0,
"total": 1,
"has_more": false
}
}GET /brands/{id}/alerts
Smart alerts, newest first: an engine starts or stops naming the brand, tone turns negative or recovers, a score milestone. Poll with since to pick up new ones.
since- ISO 8601 timestamp (default: 30 days ago)limit- 1-100, default 50
curl "https://tryzene.com/api/v1/brands/7b0c1f2e-4d5a-4c3b-9e8f-1a2b3c4d5e6f/alerts?since=2026-09-01T00:00:00Z" \
-H "Authorization: Bearer $ZENE_API_KEY"Response · 200 OK
{
"data": [
{
"id": "0f6a4c55-5a1e-4a54-9a2d-2f3e4d5c6b7a",
"kind": "new_visibility",
"tone": "positive",
"title": "Notion now appears on Perplexity",
"detail": "Perplexity started naming Notion - now in 4 of 12 compared answers.",
"engine": "perplexity",
"occurred_at": "2026-09-26T09:30:00+00:00"
}
],
"pagination": {
"limit": 50,
"since": "2026-09-01T00:00:00+00:00"
}
}GET /brands/{id}/trend
Daily visibility score over a window as flat rows - one per day and engine, plus engine: "overall" for the all-engine line. Only days with scan data appear. Made for time-series tools (it is what the Looker Studio connector charts).
days- 1-180, default 30
curl "https://tryzene.com/api/v1/brands/7b0c1f2e-4d5a-4c3b-9e8f-1a2b3c4d5e6f/trend?days=90" \
-H "Authorization: Bearer $ZENE_API_KEY"Response · 200 OK
{
"data": [
{
"date": "2026-09-25",
"engine": "overall",
"score": 58
},
{
"date": "2026-09-25",
"engine": "chatgpt",
"score": 70
},
{
"date": "2026-09-26",
"engine": "overall",
"score": 61
},
{
"date": "2026-09-26",
"engine": "chatgpt",
"score": 72
}
],
"pagination": {
"days": 90
}
}GET /brands/{id}/sources
The web domains AI engines cited in the brand's answers, most cited first: citations, answers citing the domain with and without the brand mentioned (the "without" ones are outreach targets), the engines, and whether it is the brand's own site.
days- 1-180, default 30limit- 1-100, default 50
curl "https://tryzene.com/api/v1/brands/7b0c1f2e-4d5a-4c3b-9e8f-1a2b3c4d5e6f/sources?days=30&limit=20" \
-H "Authorization: Bearer $ZENE_API_KEY"Response · 200 OK
{
"data": [
{
"domain": "g2.com",
"citations": 14,
"answers_with_brand": 3,
"answers_without_brand": 9,
"engines": [
"chatgpt",
"perplexity"
],
"owned": false
},
{
"domain": "notion.com",
"citations": 11,
"answers_with_brand": 10,
"answers_without_brand": 1,
"engines": [
"chatgpt",
"claude",
"gemini"
],
"owned": true
}
],
"pagination": {
"limit": 20,
"days": 30,
"total": 37,
"has_more": true
}
}GET /brands/{id}/actions
The brand's action plan - in progress and suggested first, then done - with difficulty, estimated impact and verification status.
status- suggested · in_progress · completed · dismissed (optional)limit- 1-100, default 50
curl "https://tryzene.com/api/v1/brands/7b0c1f2e-4d5a-4c3b-9e8f-1a2b3c4d5e6f/actions?status=suggested" \
-H "Authorization: Bearer $ZENE_API_KEY"Response · 200 OK
{
"data": [
{
"id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f7a",
"title": "Get listed on G2's 'best note-taking apps' page",
"status": "suggested",
"difficulty": "easy",
"impact_estimate": 8,
"channel": "listings",
"verification_status": null,
"created_at": "2026-09-20T08:00:00+00:00",
"completed_at": null
}
],
"pagination": {
"limit": 50,
"status": "suggested"
}
}POST /brands/{id}/scans
Queue a fresh scan of the brand's tracked questions. Same rules as "Scan now" in the app: one scan per brand at a time (the running one is returned with already_running: true) and your plan's manual-scan quota per brand per 24 hours (429 with Retry-After). Listen for scan.completed instead of polling.
curl -X POST https://tryzene.com/api/v1/brands/7b0c1f2e-4d5a-4c3b-9e8f-1a2b3c4d5e6f/scans \
-H "Authorization: Bearer $ZENE_API_KEY"Response · 202 Accepted (or 200 when a scan is already running)
{
"data": {
"scan": {
"id": "e1d2c3b4-a5f6-4e7d-8c9b-0a1b2c3d4e5f",
"brand_id": "7b0c1f2e-4d5a-4c3b-9e8f-1a2b3c4d5e6f",
"status": "pending",
"created_at": "2026-09-26T10:02:00+00:00",
"started_at": null,
"completed_at": null
},
"already_running": false
}
}REST hooks
Instead of polling, subscribe a URL to an event and Zene POSTs to it when the event happens. This is the interface Zapier's REST-hook triggers use; any other automation tool works the same way.
POST /hooks
Subscribe a URL to an event (REST hooks - what Zapier calls "subscribe"). Deliveries are signed exactly like webhook channels. Up to 50 hooks per workspace.
curl -X POST https://tryzene.com/api/v1/hooks \
-H "Authorization: Bearer $ZENE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"target_url":"https://hooks.zapier.com/hooks/standard/123/abc/","event":"alert.created"}'Response · 201 Created
{
"data": {
"id": "c0ffee00-1234-4abc-9def-0123456789ab",
"event": "alert.created",
"target_url": "https://hooks.zapier.com/hooks/standard/123/abc/",
"enabled": true,
"created_at": "2026-09-26T10:05:00+00:00"
}
}DELETE /hooks/{id}
Unsubscribe. A hook whose URL answers 410 Gone is removed automatically, and revoking an API key removes the hooks it created.
curl -X DELETE https://tryzene.com/api/v1/hooks/c0ffee00-1234-4abc-9def-0123456789ab \
-H "Authorization: Bearer $ZENE_API_KEY"Response · 200 OK
{
"data": {
"id": "c0ffee00-1234-4abc-9def-0123456789ab",
"deleted": true
}
}GET /hooks
The workspace's hooks.
curl https://tryzene.com/api/v1/hooks \
-H "Authorization: Bearer $ZENE_API_KEY"Response · 200 OK
{
"data": [
{
"id": "c0ffee00-1234-4abc-9def-0123456789ab",
"event": "alert.created",
"target_url": "https://hooks.zapier.com/hooks/standard/123/abc/",
"enabled": true,
"created_at": "2026-09-26T10:05:00+00:00"
}
]
}GET /hooks/sample?event=alert.created
A sample delivery body for an event, returned as a bare JSON array (Zapier's "perform list" format) so fields can be mapped before a real event fires.
curl "https://tryzene.com/api/v1/hooks/sample?event=scan.completed" \
-H "Authorization: Bearer $ZENE_API_KEY"Response · 200 OK
[
{
"id": "6e5d4c3b-2a1f-4e0d-9c8b-7a6f5e4d3c2b",
"event": "scan.completed",
"created_at": "2026-09-26T09:30:00.000Z",
"account_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"data": {
"scan": {
"id": "3c9d2b1a-8e7f-4a6b-b5c4-d3e2f1a0b9c8",
"started_at": "2026-09-26T09:12:04.000Z",
"completed_at": "2026-09-26T09:30:00.000Z",
"partial": false,
"questions_total": 20,
"questions_scanned": 20,
"answers": 100,
"mentions": 57,
"brand": {
"id": "7b0c1f2e-4d5a-4c3b-9e8f-1a2b3c4d5e6f",
"name": "Notion"
}
},
"score": {
"overall": 61,
"delta": 4
}
},
"message": {
"title": "Scan completed: Notion",
"body": "AI visibility is 61/100 (+4).",
"url": "https://tryzene.com/dashboard"
}
}
]Webhooks & signing
Hooks and webhook channels (Settings → Integrations) receive the same deliveries. Events:
alert.created- An AI engine starts or stops naming a brand, tone turns negative or recovers, or a score milestone.scan.completed- A brand's visibility scan finished, with the updated score.report.sent- A scheduled report was generated and sent.
Each delivery is a JSON POST with these headers:
Content-Type: application/json
User-Agent: Zene-Webhooks/1.0 (+https://tryzene.com/developers)
Zene-Event: alert.created
Zene-Delivery: 5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a
Zene-Signature: t=1790414400,v1=5b1f0c…and a body like this (data depends on the event):
{
"id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
"event": "alert.created",
"created_at": "2026-09-26T09:30:00.000Z",
"account_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"data": {
"alert": {
"id": "0f6a4c55-5a1e-4a54-9a2d-2f3e4d5c6b7a",
"kind": "new_visibility",
"tone": "positive",
"title": "Notion now appears on Perplexity",
"detail": "Perplexity started naming Notion - now in 4 of 12 compared answers.",
"engine": "perplexity",
"occurred_at": "2026-09-26T09:30:00.000Z",
"brand": {
"id": "7b0c1f2e-4d5a-4c3b-9e8f-1a2b3c4d5e6f",
"name": "Notion"
}
}
},
"message": {
"title": "Notion now appears on Perplexity",
"body": "Perplexity started naming Notion - now in 4 of 12 compared answers.",
"url": "https://tryzene.com/engines/perplexity"
}
}Verify every delivery. v1 is the hex HMAC-SHA256 of t + "." + raw body, keyed with the channel's signing secret (Settings → Integrations → Signing secret). Reject timestamps older than a few minutes to stop replays. In Node:
import crypto from "node:crypto";
import express from "express";
// Zene-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, t + "." + rawBody)>
function verifyZeneSignature(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(
(header ?? "").split(",").map((kv) => kv.trim().split("=")),
);
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
// Verify against the RAW body - re-serialized JSON won't match the signature.
app.post("/zene-webhook", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
if (!verifyZeneSignature(raw, req.get("Zene-Signature"), process.env.ZENE_WEBHOOK_SECRET)) {
return res.status(400).send("bad signature");
}
const event = JSON.parse(raw);
// event.id is the delivery id - identical across retries, so de-duplicate on it.
console.log(event.event, event.message.title);
res.sendStatus(200);
});Answer with any 2xx within 5 seconds. Redirects are not followed. Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes and 2 hours (5 attempts); 4xx answers other than 408 and 429 are not retried. A channel that fails 10 times in a row is paused - you'll see why in Settings, and turning it back on resumes deliveries. id stays the same across retries, so de-duplicate on it. Destinations must be public HTTPS URLs.
Slack & Microsoft Teams
No code needed (Growth and Agency). In Settings → Integrations, add a channel, paste the URL, pick the events, and press Send test.
- Slack: create an incoming webhook for the channel (Slack → Apps → Incoming Webhooks) and paste its
https://hooks.slack.com/services/…URL. Alerts arrive as formatted messages with an "Open in Zene" button. - Microsoft Teams: in the channel, open Workflows and use the template "Post to a channel when a webhook request is received" (or an incoming-webhook connector), then paste the URL it gives you. Alerts arrive as Adaptive Cards.
Zapier
Two ways to connect, depending on your plan:
- Webhooks by Zapier (Growth or Agency). Create a Zap with the Webhooks by Zapier → Catch Hook trigger, copy the hook URL, and add it in Settings → Integrations as a Webhook channel. Press Send test, then map fields from the delivery body above.
- Your own Zapier integration (Agency, API key). In the Zapier Platform, create a private integration with API Key authentication (header
Authorization: Bearer {{bundle.authData.api_key}}) andGET /meas the connection test. Add a REST Hook trigger per event:- Subscribe:
POST /hookswith{"target_url": "{{bundle.targetUrl}}", "event": "alert.created"} - Unsubscribe:
DELETE /hooks/{{bundle.subscribeData.data.id}}(the subscribe response is{ data: { id, … } }) - Perform list (sample data):
GET /hooks/sample?event=alert.created
- Subscribe:
Looker Studio
The Zene community connector (Agency, full-access key) brings three tables into Looker Studio for client dashboards: visibility over time (date × engine score, from /trend), question results (question × engine: mentioned, position, tone, cited sources, from /questions) and cited sources (from /sources).
- In Looker Studio, Create → Data source, search for Zene (or open the connector link we send you).
- Paste a full-access API key when asked, then pick the brand id (from
GET /brands) and the dataset. - Add the data source to a report. Fields refresh from the API on Looker's cache schedule.
Prefer to run it yourself? The connector's Apps Script source lives in the Zene repository under integrations/looker-studio/ with deploy steps.
Questions or a missing endpoint? Email hello@tryzene.com. Found a security issue? See Security.