Gareth Wilson Gareth Wilson

Guide to Outreach Webhooks: Features and Best Practices

Published


Outreach webhooks tell your systems what is happening in your sales engagement data: a prospect is created, a prospect is added to a sequence or finishes one, an email bounces or gets a reply, a call is logged, or a task is completed. If you build on Outreach, webhooks are how those changes reach your CRM sync, analytics or internal tools without polling the Outreach API.

This guide covers how Outreach webhooks are created and signed, the resources and actions you can subscribe to and the two payload versions, Outreach's policy of never retrying an HTTP error, and the best practices for running an Outreach receiver in production.

What are Outreach webhooks?

In Outreach, a webhook is an API resource like any other. You create one with POST /api/v2/webhooks, giving it an HTTPS URL and, optionally, the resource and action it should respond to. Both filters default to *, which means every applicable resource and action is delivered.

When a matching change happens, Outreach sends a JSON:API-styled POST to the URL. The body has a data object describing the resource and a meta object with deliveredAt and eventName. The event name has the form <resource>.<action>, for example task.completed.

Signing is opt-in. If you set a secret attribute on the webhook, every delivery carries an Outreach-Webhook-Signature header: the hex HMAC-SHA256 of the raw body, keyed with that secret. If you don't, there is no signature at all.

Outreach webhook features

FeatureDetails
ConfigurationThe REST API: POST https://api.outreach.io/api/v2/webhooks, listed with GET /api/v2/webhooks
Filtersresource and action, each defaulting to *
Event namemeta.eventName, as <resource>.<action>
Payload formatJSON:API-styled data and meta
Payload versionspayloadVersion 1 (default) or 2, which adds beforeUpdate
Signature headerOutreach-Webhook-Signature, only when the webhook has a secret
SchemeHMAC-SHA256, bare lowercase hex
Signed contentThe raw request body, nothing else
Replay windowNone. No timestamp is sent or signed
Cleanup headeroutreach-webhook-cleanup-token, a token that can only delete this webhook
Response window5 seconds
RetriesNetwork failures only: 4 attempts, one second apart
HTTP errorsAny status, including 500 and 429, counts as an acknowledgement and is not retried
RedirectsNot followed

The URL must be HTTPS. Outreach states that it does not provide a fixed list of IP addresses for webhooks, so the signature is the only way to authenticate a delivery.

Common events

Outreach's docs list the resources and actions you can filter on. Each combination is delivered as <resource>.<action>.

ResourceActions
accountcreated, updated, destroyed
callcreated, updated, destroyed
emailAddresscreated, updated, destroyed
importcreated, finished
kaiaRecordingcreated
mailingcreated, updated, destroyed, bounced, delivered, opened, replied
opportunitycreated, updated, destroyed
opportunityProspectRolecreated, updated, destroyed
prospectcreated, updated, destroyed
sequencecreated, updated, destroyed
sequenceStatecreated, updated, destroyed, advanced, finished
taskcreated, updated, destroyed, completed
usercreated, updated

The only event name Outreach shows in a payload is task.completed. The rest follow from the grid, so log the eventName values you actually receive before you rely on an exact spelling.

Create and update events carry only the attributes that were created or changed, not the full resource. Destroyed events carry the last known attributes and relationships. Here is Outreach's task.completed example in the default version 1 format, written as JSON:

{
  "data": {
    "type": "task",
    "id": 1,
    "attributes": {
      "completed": true,
      "completedAt": "2017-01-01T00:00:00",
      "state": "completed"
    }
  },
  "meta": {
    "deliveredAt": "2017-01-01T00:00:01",
    "eventName": "task.completed"
  }
}

Version 2 adds a beforeUpdate object with the same type and id and the previous values of the changed attributes, for example "state": "in progress". payloadVersion is a setting on the webhook and is not sent with the delivery, so a version 2 body is recognisable only by its beforeUpdate key.

data.id is a number in Outreach's examples, and the timestamps have no time zone suffix.

Setting up Outreach webhooks

Outreach webhooks are created through the API with an access token for https://api.outreach.io. Always include a secret. Without one, Outreach sends no Outreach-Webhook-Signature header, and a verifier, including Hookdeck's Outreach source, has nothing to check, so every delivery fails verification.

curl -X POST "https://api.outreach.io/api/v2/webhooks" \
  -H "Authorization: Bearer $OUTREACH_ACCESS_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{
    "data": {
      "type": "webhook",
      "attributes": {
        "url": "https://example.com/webhooks/outreach",
        "resource": "task",
        "action": "completed",
        "secret": "a-long-random-string"
      }
    }
  }'
  1. Generate a long random secret and store it server-side, for example as OUTREACH_WEBHOOK_SECRET.
  2. Create the webhook with your URL, the resource and action you want (or leave them out for everything), and the secret.
  3. Add "payloadVersion": 2 if you want the previous values in beforeUpdate.
  4. Keep the cleanupToken from the response if you are building a partner integration.

If you use Hookdeck, create an Outreach source, set the same secret on it, and use the Hookdeck source URL as the webhook's url. If you choose to leave a webhook unsigned, leave verification off on the source too.

Keep your own copy of the secret. Outreach redacts it when the webhook is listed by regular users or by applications other than the one that created it; only admin users and the creating application can see it.

Securing Outreach webhooks

Compute an HMAC-SHA256 of the raw request body with your secret, encode it as hex, and compare it with Outreach-Webhook-Signature in constant time. The header is the bare digest with no prefix.

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

function verifyOutreachSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;

  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');

  const received = Buffer.from(signatureHeader.trim().toLowerCase(), 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/outreach', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.OUTREACH_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyOutreachSignature(req.body, req.get('outreach-webhook-signature'), secret)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Route on event.meta.eventName. Persist the event, then process it asynchronously.
  res.sendStatus(200);
});

The same check in Python:

import hashlib
import hmac


def verify_outreach_signature(raw_body: bytes, signature_header, secret: str) -> bool:
    if not signature_header or not secret:
        return False
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    received = signature_header.strip().lower().encode("utf-8")
    return hmac.compare_digest(received, expected.encode("utf-8"))


# In FastAPI: raw_body = await request.body()
# verify_outreach_signature(raw_body, request.headers.get("outreach-webhook-signature"),
#                           os.environ["OUTREACH_WEBHOOK_SECRET"])

Outreach's own Ruby example compares with ==, which is not constant time. Use crypto.timingSafeEqual or hmac.compare_digest instead. Outreach doesn't document the delivery's Content-Type, and a JSON:API body may not be labelled plain application/json, so express.raw({ type: '*/*' }) keeps the raw bytes whatever it says.

Make Outreach webhooks production-ready. Hookdeck Event Gateway verifies the Outreach-Webhook-Signature header, accepts every prospect, sequence and task event as soon as it arrives, and durably queues it, so an error on your side doesn't lose an event Outreach will never resend.

Outreach webhook limitations and pain points

Errors are never retried

The Problem: An event that hits your endpoint during an outage or a bug is gone for good.

Why It Happens: Outreach treats any HTTP status as an acknowledgement. A 500 or a 429 is not retried, and neither is a redirect. Only network failures, such as socket errors and connection resets, are retried, with 4 attempts one second apart. Outreach warns that proxies which answer 429 when a capacity limit is hit cause the same silent loss.

Workarounds:

  • Keep the receiver small and highly available: verify, store, return 200.
  • Don't put a rate-limiting proxy in front of the endpoint, or monitor it for 429s.
  • Reconcile important resources through the Outreach API after any incident.

How Hookdeck Can Help: Hookdeck acknowledges Outreach as soon as the request is stored, then retries delivery to your service on a schedule you control and lets you replay any event, so an error in your service no longer drops the event.

A 5 second timeout

The Problem: Slow handlers miss the response window, and Outreach does not document whether a timed-out delivery is retried.

Why It Happens: Outreach waits 5 seconds for a response, and recommends finishing processing within that time or handing the work to a message bus. Even if a timeout counts as a network failure, the 4 attempts are only one second apart.

Workarounds:

  • Return 200 straight after verification and storage, and process asynchronously.
  • Keep database writes on the request path to a single insert.

How Hookdeck Can Help: Hookdeck answers Outreach as soon as the request is stored, without waiting for your service, and your service can take as long as it needs to process each event.

No secret, no signature

The Problem: Deliveries arrive with no Outreach-Webhook-Signature header, and every one fails verification.

Why It Happens: secret is optional on the webhook, and Outreach signs only when it is present. A webhook created without one sends unsigned requests.

Workarounds:

  • Always include secret when you create a webhook.
  • Audit existing webhooks with GET /api/v2/webhooks and replace any that were created without a secret.

How Hookdeck Can Help: Hookdeck's Outreach source verifies the header once the secret is set on both sides, and records unsigned or mis-signed requests as failed verification instead of delivering them.

Webhooks are disabled when the hostname doesn't resolve

The Problem: Events stop arriving after a DNS change.

Why It Happens: If the hostname resolves to a private network address, Outreach disables the webhook for an hour. If it doesn't resolve after 3 DNS attempts, the webhook is disabled for a minute. Webhooks that can't recover for a week or more are disabled permanently.

Workarounds:

  • Use a public hostname with stable DNS.
  • Check the active, disabledReason, disabledSince and disabledUntil attributes returned by GET /api/v2/webhooks, and alert when a webhook is disabled.

How Hookdeck Can Help: A Hookdeck source URL is a stable public hostname, so DNS changes on your side don't affect Outreach's webhooks.

No event id

The Problem: You can't reliably tell a network retry from a new event.

Why It Happens: meta carries only deliveredAt and eventName, and nothing in the payload identifies the delivery.

Workarounds:

  • Build a key from eventName, data.type, data.id and the changed attributes, such as completedAt.
  • Make handlers idempotent, so applying the same change twice does no harm.

How Hookdeck Can Help: Hookdeck's deduplication can drop identical repeated deliveries before they reach your service, based on the fields you choose.

Best practices

Verify against the raw body

The signature covers the exact bytes Outreach sent. Mount express.raw() on the webhook route and parse only after verification, or read await request.text() in a Next.js route handler and await request.body() in FastAPI.

Filter at the source

Leaving resource and action at * sends every change in the account to your endpoint. Create one webhook per resource and action you need, so less traffic arrives and each handler stays simple.

Expect partial resources

Update events contain only the changed attributes. Don't overwrite a full record with the attributes object. Merge the fields that are present, or fetch the full resource from the API when you need it. Use payloadVersion 2 when you need to know what a value changed from.

Route on meta.eventName

data.type names the resource but not the action, so the same task type arrives for created, updated, destroyed and completed events. Switch on meta.eventName, and log and acknowledge anything you don't recognise.

Keep the cleanup token if you're an integrator

Every delivery carries outreach-webhook-cleanup-token. If a customer revokes your access, POST https://api.outreach.io/api/v2/webhooks/cleanup with that token as a bearer token removes the webhook (204), or tells you it's already gone (404) or that the token is invalid (401). The token can do nothing else.

Acknowledge fast and process asynchronously

See why to process webhooks asynchronously and our guide to webhook idempotency.

Conclusion

Outreach signs a webhook delivery only when the webhook has a secret, as a bare hex HMAC-SHA256 of the raw body in Outreach-Webhook-Signature. Always set the secret through the API, verify against the raw bytes, route on meta.eventName, merge partial attributes rather than replacing records, and answer within 5 seconds, because Outreach never retries an HTTP error.

Hookdeck Event Gateway sits in front of your receiver, checks every Outreach signature, and holds each event until your service has processed it, so an error on your side costs latency instead of data.

Get started with Hookdeck for free and handle Outreach webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.