Agent skill
Streak Webhooks Skill
Receive and authenticate Streak webhooks. Use when setting up a Streak CRM webhook endpoint, verifying the X-Streak-Webhook-Token header, debugging 401s, or handling events like BOX_CREATE, BOX_EDIT, BOX_CHANGE_STAGE, COMMENT_CREATE and TASK_CREATE.
Install this skill
npx skills add hookdeck/webhook-skills --skill streak-webhooks
Streak (the CRM inside Gmail) POSTs the updated object(s), as a JSON array, to your targetUrl when a pipeline or team event happens. Webhooks are not signed. If you set a token when you created the webhook, Streak sends it as plain text in the X-Streak-Webhook-Token header and your endpoint authenticates the request by comparing that whole value with the token you stored.
Set a token when you create the webhook through the Streak API. Webhooks without one send nothing to verify. The token is optional in the API, so a webhook created without it can't be authenticated at all.
A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS and rotate the credential if it leaks: the update endpoint POST /webhooks/{webhookKey} lists token in its schema (its prose says only event and targetUrl are applied, so confirm with a delivery); otherwise delete and recreate the webhook with a new token.
When to Use This Skill
- How do I receive Streak webhooks?
- How do I verify the
X-Streak-Webhook-Tokenheader? - Does Streak sign webhook payloads? (No: use the shared token)
- How do I handle
BOX_CREATE,BOX_CHANGE_STAGEorCOMMENT_CREATEevents? - Why does my Streak webhook have nothing to verify?
- Why did my Streak webhook disappear? (Your endpoint returned 410 Gone)
Verification (core)
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
const ha = crypto.createHash('sha256').update(String(a)).digest();
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb);
}
function verifyStreakRequest(headers, token = process.env.STREAK_WEBHOOK_TOKEN) {
if (!token) return false; // never accept when unconfigured
const received = headers['x-streak-webhook-token']; // whole value, plain text
return Boolean(received) && safeEqual(received, token);
}
Python: hmac.compare_. Always encode both sides to bytes; passing str raises on non-ASCII.
- Header:
X-Streak-Webhook-Token: <token>(name is case-insensitive). The value is thetokenyou passed toPOST /api/v2/webhooks, compared whole. - Missing header or mismatch: return
401. Body that isn't a JSON object/array:400. Handled:200. - Read the body raw (
express.raw({ type: '*/*' })) and authenticate before parsing it. - Return
200on success.410 Gonemakes Streak delete the webhook; any other non-200 response is retried with a backoff delay.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Types
The event decides which object is sent (Box, Stage, Comment, Meeting note/Call log, Task, Contact, Organization). The body may not name the event, so create one webhook per event and give each its own URL or query parameter (for example /webhooks/streak?event=BOX_).
| Event | Category | Object | Sent When |
|---|---|---|---|
BOX_CREATE | Pipeline | Box | A box is created |
BOX_DELETE | Pipeline | Box | A box is deleted |
BOX_EDIT | Pipeline | Box | A custom field, notes, assignee or name is edited |
BOX_CHANGE_STAGE | Pipeline | Box | A box changes stage |
BOX_CHANGE_PIPELINE | Pipeline | Box | A box changes pipeline |
BOX_NEW_EMAIL_ADDRESS | Pipeline | Box | A new email address is detected on a box |
STAGE_CREATE | Pipeline | Stage | A stage is created |
COMMENT_CREATE | Pipeline | Comment | A comment is created |
MEETING_CREATE / MEETING_UPDATE | Pipeline | Meeting note/Call log | A meeting note or call log is created / changed |
TASK_CREATE | Pipeline | Task | A task is created |
TASK_COMPLETE | Pipeline | Task | A task is completed |
TASK_DUE | Pipeline | Task | A task is due |
TASK_UPDATED | Pipeline | Task | Anything about a task changes |
CONTACT_CREATE / CONTACT_UPDATE / CONTACT_DELETE | Team | Contact | A contact is created / updated / deleted |
ORG_CREATE / ORG_UPDATE / ORG_DELETE | Team | Organization | An organization is created / updated / deleted |
Pipeline events need a pipelineKey when the webhook is created; team events need a teamKey.
Environment Variables
STREAK_WEBHOOK_TOKEN=your-webhook-token # the `token` you passed when creating the webhook
Local Development
Streak can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 streak --path /webhooks/streak
Use the printed URL (plus ?event=...) as targetUrl when creating the webhook.
Resources
- references/overview.md - What Streak webhooks are, events and payload shape
- references/setup.md - Create a webhook with a token through the API
- references/verification.md - Token check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests