# Guide to Salesloft Webhooks: Features and Best Practices

Salesloft webhooks tell your systems when something changes in a team's sales engagement data: a person is created or updated, a call is logged, an email changes, a meeting is booked, or someone is added to a cadence. If you build on Salesloft, webhooks are how that activity reaches your CRM sync, analytics and automation without polling the API.

This guide covers how Salesloft webhook subscriptions are created and signed, the event types and the scopes they need, the gaps in Salesloft's documentation, and the best practices for running a receiver in production.

## What are Salesloft webhooks?

A Salesloft webhook subscription pairs one event type with a callback URL and a `callback_token`, a string you choose. Subscriptions are made on behalf of a team, and there is no dashboard for them: you create them through the Webhook Subscriptions API with an OAuth token or API key.

When a subscribed event fires, Salesloft sends an HTTP `POST` to the callback URL. The body is the affected resource, for example the new person object for `person_created`. Two headers come with it:

* `x-salesloft-event` names the event type that triggered the delivery.
* `x-salesloft-signature` is the HMAC hex digest of the body, generated with SHA-1 and the subscription's `callback_token` as the key.

Salesloft's docs describe the signature as covering "the response body", which in context means the body of the request Salesloft sends. Salesloft also includes the `callback_token` in each payload so a receiver can compare it directly, but the HMAC header is the stronger check, because it proves the body has not changed.

## Salesloft webhook features

| Feature | Details |
| --- | --- |
| Configuration | API only, through the Webhook Subscriptions API. No dashboard |
| Subscription model | One `event_type` per subscription, made on behalf of a team |
| Event header | `x-salesloft-event` |
| Signature header | `x-salesloft-signature` |
| Scheme | HMAC-SHA1, hex digest (40 characters), no prefix |
| Signed content | The request body |
| Secret | The `callback_token` you set on the subscription |
| Replay window | None. No timestamp is sent or signed |
| Body | The resource object for the event. No envelope is documented |
| Retries | Three more attempts, 15 seconds apart, then marked as failed |
| IP allowlist | None published |

## Common events

Salesloft documents 37 event types. Most need a matching OAuth scope, and some also need user permissions such as **View Team Calls** or **View All People**.

| Area | Events | Scope |
| --- | --- | --- |
| People | `person_created`, `person_updated`, `person_deleted`, `success_created` | `people:read` |
| Accounts | `account_created`, `account_updated`, `account_deleted` | `accounts:read` |
| Cadences | `cadence_created`, `cadence_updated`, `cadence_deleted`, `cadence_membership_created`, `cadence_membership_updated`, `step_created`, `step_updated`, `step_deleted` | `cadences:read` |
| Calls | `call_created`, `call_updated`, `call_data_record_created`, `call_data_record_updated` | `calls:read` |
| Conversations | `conversation_created`, `conversation_recording_created`, `conversation_transcript_created` | `conversations:read` |
| Email | `email_updated`, `link_swap` | `emails:read` |
| Email content | `email_with_body_and_subject_updated` | `email_contents` |
| Meetings | `meeting_booked`, `meeting_updated` | `meetings:read` |
| Notes | `note_created`, `note_updated`, `note_deleted` | `notes:read` |
| Tasks | `task_created`, `task_updated`, `task_completed`, `task_deleted` | `tasks:read` |
| Users | `user_created`, `user_updated` | `team:read` |
| Bulk jobs | `bulk_job_completed` | None listed |

Salesloft describes each event as delivering the resource: "When a person is created, receive the new person object". The fields are those of the matching object in the Salesloft API reference. Salesloft publishes no example webhook body, so log the first delivery of each type before you rely on a field.

`link_swap` is different from the rest. It fires when an email is sent, carrying the links from the email body "so they may be replaced with new links before the email is sent". Salesloft's pages do not say how a replacement is returned, so check with Salesloft before building on it.

> **Inspect Salesloft webhooks as they arrive.** Use a [Hookdeck Inspect](https://inspect.hookdeck.com) URL as a subscription's `callback_url` to inspect and replay real deliveries, with no account or setup required.

## Setting up Salesloft webhooks

1. Generate a long random `callback_token`, for example with `openssl rand -hex 32`, and store it server-side as `SALESLOFT_CALLBACK_TOKEN`.
2. Make sure your OAuth app or API key has the scopes for the events you need.
3. Create a subscription for each event type:

```bash
curl -X POST https://api.salesloft.com/v2/webhook_subscriptions \
  -H "Authorization: Bearer $SALESLOFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "callback_url": "https://example.com/webhooks/salesloft",
    "callback_token": "'"$SALESLOFT_CALLBACK_TOKEN"'",
    "event_type": "person_created"
  }'

```

Repeat for `person_updated`, `call_created`, `meeting_booked` and any other event you need. All of them can point at the same URL, because `x-salesloft-event` tells your handler which one arrived. The same API lists, updates and deletes subscriptions.

If you use the same `callback_token` on every subscription that targets one URL, a single verifier covers them all. If you give each subscription its own token, your receiver has to know which token goes with which event.

If you receive Salesloft webhooks through Hookdeck, enter the `callback_token` as the Hookdeck source's secret.

## Securing Salesloft webhooks

Compute the hex HMAC-SHA1 of the raw request body with your `callback_token` as the key, and compare it with `x-salesloft-signature` in constant time.

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

function verifySalesloftSignature(rawBody, signatureHeader, callbackToken) {
  if (!signatureHeader || !callbackToken) return false;
  const expected = crypto.createHmac('sha1', callbackToken).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/salesloft', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifySalesloftSignature(req.body, req.get('x-salesloft-signature'), process.env.SALESLOFT_CALLBACK_TOKEN)) {
    return res.sendStatus(401);
  }

  const eventType = req.get('x-salesloft-event');
  const resource = JSON.parse(req.body.toString('utf8'));
  // Route on eventType, then hand off to a queue.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hashlib
import hmac

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

```

To check a captured delivery by hand, `printf '%s' "$BODY" | openssl dgst -sha1 -hmac "$TOKEN"` should print the same digest as the header.

> **Make Salesloft webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies the `x-salesloft-signature` header with your `callback_token`, stores every person, call and meeting event, and retries delivery for far longer than Salesloft's 45 seconds.

## Salesloft webhook limitations and pain points

### Retries are over in 45 seconds

**The Problem:** A deploy, a restart or a brief outage on your side loses events for good.

**Why It Happens:** Salesloft retries a failing webhook three more times, 15 seconds apart, and then marks it as failed. That covers about 45 seconds.

**Workarounds:**

* Return `200` as soon as the signature checks out, and do the work afterwards.
* Run a periodic sync through the Salesloft API to catch anything a webhook missed.

**How Hookdeck Can Help:** Hookdeck accepts each delivery as soon as it arrives and stores it, so Salesloft's attempts succeed even during your downtime. Hookdeck then delivers with [automatic retries](/docs/retries) you configure and lets you replay anything that still fails.

### Undocumented payloads

**The Problem:** You write a handler from the API reference, and the webhook body turns out to differ.

**Why It Happens:** Salesloft says each event delivers the resource object, but publishes no example webhook body and does not say where in it the `callback_token` appears.

**Workarounds:**

* Capture one real delivery per event type before you write the handler.
* Read fields defensively, and fetch the resource from the API when you need guaranteed fields.

**How Hookdeck Can Help:** Hookdeck keeps the headers and raw body of every request, so your first live deliveries become fixtures you can inspect and replay against your handler.

### SHA-1, keyed with your own token

**The Problem:** Verification fails even though the code looks right.

**Why It Happens:** Most webhook providers use SHA-256, so it is easy to copy a SHA-256 verifier. Using an OAuth token or API key as the HMAC key, rather than the `callback_token`, fails the same way.

**Workarounds:**

* Use `sha1` and the `callback_token` from the subscription.
* Fetch the subscription from the API to confirm which token it uses.

**How Hookdeck Can Help:** Hookdeck's Salesloft source is configured for HMAC-SHA1 hex on `x-salesloft-signature`, so you only enter the token.

### No event ID or timestamp

**The Problem:** A retried delivery looks identical to the original, and a captured request still verifies if it is sent again.

**Why It Happens:** Salesloft sends no delivery ID and signs no timestamp.

**Workarounds:**

* Build an idempotency key from `x-salesloft-event`, the resource `id` and a field that changes with each update, such as an update timestamp if the resource has one.
* Make updates in your systems upserts keyed on the Salesloft resource ID.

**How Hookdeck Can Help:** Hookdeck's [deduplication](/docs/deduplication) can drop repeated deliveries based on the header and body fields you choose.

## Best practices

### Verify against the raw body

The signature covers the exact bytes Salesloft sent. In Express, use `express.raw({ type: '*/*' })`. In Next.js, use `await request.text()`. In FastAPI, use `await request.body()`.

### Route on `x-salesloft-event`

The event type is in the header, not documented in the body. Switch on the header and acknowledge any value you do not handle.

### Request only the scopes you need

Each event needs its read scope, and the authorizing user needs the matching permissions or the data will not be included. Subscribe only to events your integration uses.

### Acknowledge fast and process asynchronously

With 45 seconds of retries, slow handlers lose events. Verify, store, return `200`, and process afterwards. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Make handlers idempotent

Retries can repeat a delivery. Key your processing on the event type and resource ID. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

Salesloft signs each webhook with a hex HMAC-SHA1 of the body in `x-salesloft-signature`, keyed with the `callback_token` you chose when you created the subscription, and names the event in `x-salesloft-event`. Create subscriptions through the API, verify against the raw bytes with SHA-1, route on the header, and acknowledge quickly, because Salesloft's retries are over in under a minute.

[Hookdeck Event Gateway](https://hookdeck.com) verifies every Salesloft signature, stores each event, and retries on your schedule, so a short outage does not cost you call and meeting data.

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