# Guide to Streak Webhooks: Features and Best Practices

Streak webhooks notify your systems when something changes in Streak, the CRM that runs inside Gmail. When a box moves stage, a task falls due or a contact is updated, Streak can send the changed object to your service, so you can sync deals to a data warehouse, start an onboarding workflow when a deal is won, or alert a team in another tool.

This guide covers how to create Streak webhooks through the API, how to authenticate them with a token, which events exist, and the practices that make a Streak receiver dependable.

## What are Streak webhooks?

A Streak webhook subscribes one target URL to one event. Streak "will POST an object to a target URL when certain events happen", and the "updated object(s) will be in an array". The event decides the kind of object: a box, stage, comment, meeting note or call log, task, contact or organisation.

Webhooks are either **pipeline-based** or **team-based**. Box, stage, comment, meeting and task events belong to a pipeline, and contact and organisation events belong to a team. You choose by passing a `pipelineKey` or a `teamKey` when you create the webhook, and you can't switch a webhook between the two later.

Streak doesn't sign requests. When you create a webhook you can give it an optional `token`, which Streak includes "as a header (X-Streak-Webhook-Token) in every request sent to the target url". That token is the only way to tell a real Streak request from a forged one, and a webhook created without it sends nothing to check.

## Streak webhook features

| Feature | Details |
| --- | --- |
| Configuration | Streak API only: `POST https://api.streak.com/api/v2/webhooks` |
| Scope | One event per webhook, on a pipeline (`pipelineKey`) or a team (`teamKey`) |
| Method | `POST` |
| Body | The updated object or objects, in an array |
| Authentication | Optional `token`, sent as `X-Streak-Webhook-Token` |
| Signature and timestamp | None |
| `410 Gone` response | The webhook deletes itself |
| Other non-`200` responses | Retried with a backoff delay |
| Retry count, timeout, source IPs | Not documented |

## Events

| Event | Category | Object | Sent when |
| --- | --- | --- | --- |
| `BOX_CREATE` | Pipeline | Box | A new box is created |
| `BOX_DELETE` | Pipeline | Box | A box is deleted |
| `STAGE_CREATE` | Pipeline | Stage | A new stage is created |
| `BOX_NEW_EMAIL_ADDRESS` | Pipeline | Box | A new email address is detected on a box |
| `BOX_EDIT` | Pipeline | Box | A custom field, notes, assigned to or name is edited on a box |
| `BOX_CHANGE_STAGE` | Pipeline | Box | A box changes stage |
| `BOX_CHANGE_PIPELINE` | Pipeline | Box | A box changes pipeline |
| `COMMENT_CREATE` | Pipeline | Comment | A comment is created |
| `MEETING_CREATE` | Pipeline | Meeting note or call log | A meeting note or call log is created in a box |
| `MEETING_UPDATE` | Pipeline | Meeting note or call log | A meeting note or call log changes |
| `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, such as text, due date, status or assignee |
| `CONTACT_CREATE` | Team | Contact | A contact is created |
| `CONTACT_UPDATE` | Team | Contact | A contact is updated |
| `CONTACT_DELETE` | Team | Contact | A contact is deleted |
| `ORG_CREATE` | Team | Organisation | An organisation is created |
| `ORG_UPDATE` | Team | Organisation | An organisation is updated |
| `ORG_DELETE` | Team | Organisation | An organisation is deleted |

Streak's docs don't show a delivered payload or say whether the body names the event. Because each webhook has exactly one event, the safe approach is to put the event in the target URL, for example `https://example.com/webhooks/streak?event=BOX_CHANGE_STAGE`, and route on that. Always treat the body as an array, even when one object changed.

> **Inspect Streak webhooks as they arrive.** Create a webhook with a [Hookdeck Inspect](https://inspect.hookdeck.com) URL as the target to see the exact headers and body for each event, with no account or setup required.

## Setting up Streak webhooks

Streak webhooks are created through the API. Generate a long random token first, for example with `openssl rand -hex 32`.

1. In Gmail, open the Streak sidebar, go to **Integrations**, and under **Streak API** click **Create New Key**.
2. Find the key of the pipeline (for pipeline events) or team (for team events) you want to watch.
3. Create the webhook. Streak's API uses HTTP Basic auth with your API key as the username and no password:

```bash
curl -X POST "https://api.streak.com/api/v2/webhooks?pipelineKey=YOUR_PIPELINE_KEY" \
  -u "YOUR_API_KEY:" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "BOX_CHANGE_STAGE",
    "targetUrl": "https://example.com/webhooks/streak?event=BOX_CHANGE_STAGE",
    "token": "YOUR_WEBHOOK_TOKEN"
  }'

```

1. Repeat for each event you need. Use `teamKey` instead of `pipelineKey` for contact and organisation events.

The `token` is optional in the API, so check every existing webhook. To add or change a token on one, update it with `POST /api/v2/webhooks/{webhookKey}`, which takes the same `token` field.

To route through Hookdeck, create a source with the **Streak** type and use its URL, with your `?event=` query string, as the `targetUrl`. Enter the same token in the source's **Webhook Token** field. The source compares it with the `X-Streak-Webhook-Token` header and accepts `POST`.

## Securing Streak webhooks

Compare the whole `X-Streak-Webhook-Token` header with the token you set, in constant time. Reject requests with no header: a webhook without a token can't be told apart from a forgery.

```javascript
const crypto = require('crypto');
const express = require('express');

const TOKEN = process.env.STREAK_WEBHOOK_TOKEN;

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

app.post('/webhooks/streak', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.get('x-streak-webhook-token');
  if (!TOKEN || typeof received !== 'string' || !safeEqual(received, TOKEN)) {
    return res.sendStatus(401);
  }

  let objects;
  try {
    objects = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  const event = req.query.event; // set when you created the webhook
  const items = Array.isArray(objects) ? objects : [objects];
  // Queue { event, items }, then respond. Never return 410 unless you
  // mean to delete the webhook.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

TOKEN = os.environ.get("STREAK_WEBHOOK_TOKEN", "")

def verify_streak_request(headers) -> bool:
    if not TOKEN:
        return False  # fail closed when unconfigured
    received = headers.get("x-streak-webhook-token")
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), TOKEN.encode("utf-8"))

```

A static token authenticates the sender, not the body. It can't detect a modified payload, and with no signed timestamp a captured request can be replayed and still pass. Serve the endpoint over HTTPS only, use a long random token, and change it on the webhook and in your receiver if it might have leaked.

> **Make Streak webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the token on every Streak request, stores each event durably, and lets you replay any delivery after a fix.

## Streak webhook limitations and pain points

### The token is optional

**The Problem:** Requests arrive with nothing to authenticate them.

**Why It Happens:** `token` is an optional field when you create a webhook, and a webhook created without it sends no `X-Streak-Webhook-Token` header.

**Workarounds:**

* Always pass a token when creating a webhook, and add one to existing webhooks with the update endpoint.
* Fail closed when the header is missing.

**How Hookdeck Can Help:** Hookdeck's Streak source requires the token. Hookdeck answers Streak with a `200` either way, but a request with a missing or wrong token is recorded as failed verification and never reaches your destination.

### A 410 deletes the webhook

**The Problem:** A webhook disappears, and events stop without an error.

**Why It Happens:** Streak treats `410 Gone` as a request to delete the webhook. A framework default, a proxy or a retired route that answers `410` removes it.

**Workarounds:**

* Never return `410` from a Streak endpoint unless you mean it.
* List your webhooks through the API on a schedule and recreate any that are missing.

**How Hookdeck Can Help:** Hookdeck answers Streak itself, so a `410` from your service never reaches Streak and can't delete the webhook. The failed delivery stays in Hookdeck for you to retry.

### Undocumented retry limits

**The Problem:** You don't know how long Streak keeps retrying after an outage on your side.

**Why It Happens:** Streak says it retries "with a backoff delay" but not how many times or how long it waits for a response.

**Workarounds:**

* Respond with `200` quickly and do the work afterwards.
* After an outage, re-read the affected pipelines or contacts from the API.

**How Hookdeck Can Help:** Hookdeck accepts each event as soon as it arrives and delivers it to your service with [retries](/docs/retries) you configure, so recovery doesn't depend on Streak's schedule.

### One webhook per event

**The Problem:** Watching a pipeline fully means managing many webhooks.

**Why It Happens:** Each webhook has a single `event`.

**Workarounds:**

* Create webhooks from a script that holds the list of events, URLs and tokens, so they stay consistent.
* Point them all at one endpoint, with the event in the query string.

**How Hookdeck Can Help:** All the webhooks can share one Hookdeck source, and Hookdeck [filters](/docs/filters) can route each event to a different destination based on its path or query.

## Best practices

### Put the event in the URL

Add `?event=...` to each `targetUrl` so your receiver knows which event fired without relying on the body.

### Handle arrays

The body is an array of updated objects. Process each one, and don't assume there is only one.

### Make processing idempotent

Retries can repeat an event. Key your work on the object's key and the state you are applying, and skip work you have done. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Acknowledge fast and process asynchronously

Check the token, store the event, return `200`, and do the work afterwards. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Streak webhooks are created through the API, one event per webhook, and send the updated objects in an array, authenticated only by an optional token in the `X-Streak-Webhook-Token` header. Set a token on every webhook, compare it whole in constant time, put the event in the target URL, and never answer `410` unless you want the webhook gone.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, checks the token, and keeps every event until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Streak webhooks reliably in minutes.