> ## Documentation Index
> Fetch the complete documentation index at: https://docs.revyops.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sendkit

> Connect Sendkit to RevyOps to sync email activity, lead status, and Do-Not-Contact suppression.

## Overview

Sendkit is a cold email sending platform. The integration is two-way: RevyOps ingests email send/reply/bounce activity and lead status changes from Sendkit via webhook, and pushes RevyOps' unified Do-Not-Contact list out to Sendkit's blacklist.

Unlike some sequencer integrations, Sendkit setup needs both an API key (so RevyOps can register the webhook and read your tags) and the webhook itself — RevyOps registers the webhook for you automatically as part of setup.

## Prerequisites

* An active Sendkit account
* A Sendkit API key (**Sendkit Settings → API Keys → Create API Key**)
* Your RevyOps `client_id` (find it under **Settings → Workspace**)
* An admin role in RevyOps — connecting the integration and triggering the DNC push are admin-only actions

## Setup

<Steps>
  <Step title="Open the Sendkit integration panel">
    In RevyOps, open **Integrations** from the sidebar (admin only) and select **Sendkit**.
  </Step>

  <Step title="Enter your API key">
    Paste your Sendkit API key and click **Verify & Connect**. RevyOps checks it against Sendkit's `GET /tags` endpoint before continuing.
  </Step>

  <Step title="Webhook is registered automatically">
    Once the key verifies, RevyOps calls Sendkit's webhook API on your behalf and creates a webhook named `RevyOps`, subscribed to:

    `email.sent`, `email.opened`, `email.clicked`, `email.replied`, `email.bounced`, `email.unsubscribed`, `email.positive_reply`, `lead.tag_changed`

    If automatic registration fails (for example, an API key without webhook permissions), RevyOps falls back to a manual step: copy the URL below and add it yourself under **Sendkit Settings → Webhooks → Add Webhook**.

    ```
    https://api.revyops.com/public/sendkit-webhook/{client_id}
    ```
  </Step>

  <Step title="Classify your tags">
    RevyOps pulls your existing Sendkit tags and lets you mark each one **Interested**, **Blocked** (Do Not Contact), both, or leave it unclassified. This mapping controls what happens when a `lead.tag_changed` event arrives — see [Status and Tag Mapping](#status-and-tag-mapping) below.
  </Step>
</Steps>

<Tip>
  RevyOps subscribes to `email.opened` and `email.clicked` when it creates the webhook, but neither event is currently processed — RevyOps acknowledges and ignores them. Only the events listed below result in a record or status change.
</Tip>

## Agency Setup (Global Webhook)

For agencies managing multiple client workspaces off a single Sendkit account, use the global webhook instead of connecting each client separately:

```
https://api.revyops.com/public/global-sendkit-webhook/{agency_id}
```

This one webhook receives every event and routes each one to the right client workspace by matching the event's `campaignName` against the campaign keywords you've assigned to each client (configured on the **Campaign Mapping** page — case-sensitive substring match). A client only receives events this way if Sendkit is checked as a mapped integration for that client and at least one keyword matches.

<Tip>
  Routing depends entirely on `campaignName` being present in the event payload — an event that arrives without one isn't routed to any client and is dropped. If you're using the agency-level webhook and a particular event type from Sendkit never seems to arrive, confirm that event type actually includes a campaign name.
</Tip>

<Tip>
  If a client has both the agency-level webhook configured for Sendkit and its own per-client webhook still registered in Sendkit, the per-client webhook silently no-ops on direct hits from Sendkit — events for that client are only processed once, via the agency route.
</Tip>

## Events and what they do

| Sendkit Event          | RevyOps Behavior                                                                                                                                                              |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `email.sent`           | Logs an outbound email record against the contact                                                                                                                             |
| `email.replied`        | Logs an inbound email record                                                                                                                                                  |
| `email.bounced`        | Logs a bounced email record; if **auto-map labels to status** is on, also sets the contact's status to `Bounced`                                                              |
| `email.positive_reply` | Logs an email record and sets the email's disposition to `Interested`; if auto-map is on, also sets the contact's status to `Interested`                                      |
| `lead.tag_changed`     | Looks up the tag value against your Interested / Other / Do Not Contact lists (see below) and applies the matching action(s); a tag not in any of your three lists is ignored |
| `email.unsubscribed`   | Marks the matching contact Do Not Contact, if DNC sync is enabled for Sendkit and a contact with that email already exists in RevyOps                                         |

**Auto-map labels to status** is on by default but configurable — turning it off keeps the email-level disposition/label change but stops it from overwriting the contact's `status` field directly (RevyOps still records the mapped value as a `sendkit_status` custom field on the contact instead).

Any other event type Sendkit might send is acknowledged with `200 OK` and dropped without further processing.

<Tip>
  Because of how the mapping logic works today, `email.positive_reply` is stored as an **OUTBOUND**-type email record rather than INBOUND, even though it represents a reply. If you're inspecting raw email records by type, keep this in mind.
</Tip>

## Status and Tag Mapping

Sendkit tags are matched against three lists you configure from the integration panel (**Edit Statuses**), each pulled from Sendkit's `/tags` endpoint:

* **Interested** — a `lead.tag_changed` event with a matching tag sets the email's disposition to that tag's name (and the contact's status too, if auto-mapping is on)
* **Other / labeled** — sets the email's non-interested disposition (label) to the tag's name
* **Blocked (Do Not Contact)** — marks the contact Do Not Contact

Interested and Blocked are set independently in the classifier, so the same tag can be marked both — in that case a matching `lead.tag_changed` event does both: it sets the Interested disposition/status *and* marks the contact Do Not Contact. A tag that's neither Interested nor Blocked is treated as "Other" and only sets the label.

## Do-Not-Contact Sync

**Inbound** (Sendkit → RevyOps): an `email.unsubscribed` event, or a `lead.tag_changed` event whose tag is in your Blocked list, marks the matching contact (by email) Do Not Contact in RevyOps — this also marks every other contact record that shares the same underlying person (e.g. the same lead with multiple email addresses). This direction requires a contact with that email to already exist in RevyOps; if none is found, the event is logged and dropped.

**Outbound** (RevyOps → Sendkit): RevyOps unifies Do-Not-Contact signals from every connected platform into one suppression list. Whenever a contact is newly marked DNC anywhere in RevyOps — regardless of which integration triggered it — RevyOps pushes that contact's email to Sendkit's `POST /dnc` endpoint in real time (batched in groups of up to 5,000 when multiple contacts are marked at once). If the DNC mark originated from a Sendkit event itself, Sendkit is skipped to avoid pushing it right back. Removing a contact from Do Not Contact in RevyOps similarly issues a `DELETE /dnc` to take them back off Sendkit's blacklist.

All of the above — inbound handling, the outbound real-time push, and the historical backfill below — share one toggle: **Cross-Platform Do Not Contact Sync** in the Sendkit panel, on by default. Turning it off disables DNC sync in every direction for Sendkit.

A separate **Sync Historical DNC List** button backfills contacts that were already marked Do Not Contact in RevyOps before Sendkit was connected — it queues a background job that pushes them to Sendkit's blacklist in the same batches of 5,000, retrying automatically on rate limits. It also respects the toggle above: if DNC sync is off, the backfill is skipped rather than run.

## Data Mapped

| Sendkit Field                                | RevyOps Field                                                                                                                                             |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data.lead.email`                            | Contact email                                                                                                                                             |
| `data.lead.firstName` / `data.lead.lastName` | Contact first / last name                                                                                                                                 |
| `data.fromEmail`                             | Sending agent email                                                                                                                                       |
| `data.subject`                               | Email subject                                                                                                                                             |
| `data.bodyHtml`                              | Email HTML body (also used as the truncated body text for outbound sends)                                                                                 |
| `data.replyContent`                          | Email body text, for `email.replied` and `email.bounced` (outbound sends, and `email.positive_reply` per the OUTBOUND note above, use `bodyHtml` instead) |
| `data.campaignName` / `data.campaignId`      | Campaign name / ID                                                                                                                                        |
| `data.sequenceStep`                          | Sequence step                                                                                                                                             |
| `data.variant`                               | Sequence step variant                                                                                                                                     |
| `data.messageId`                             | Raw message ID                                                                                                                                            |
| `event`                                      | Email type (OUTBOUND / INBOUND / BOUNCED — see the note above on `email.positive_reply`)                                                                  |

Sendkit webhooks don't include a company field, so company data is not populated from this integration.

## Custom Fields

Any entries under `data.lead.customFields` in the webhook payload are attached to the resulting email record as custom fields — this applies to standard send/reply/bounce events and `email.positive_reply`, not to `lead.tag_changed`.

## Troubleshooting

<AccordionGroup>
  <Accordion title="Automatic setup failed during connection">
    RevyOps couldn't create the webhook through the Sendkit API — usually because the API key doesn't have permission to manage webhooks. Add the webhook manually in Sendkit under **Settings → Webhooks** using the URL shown in the setup panel, then continue.
  </Accordion>

  <Accordion title="API key shows as invalid">
    RevyOps checks the key against `GET /tags`. A `401`/`403` response marks it invalid — regenerate the key in Sendkit and re-enter it.
  </Accordion>

  <Accordion title="Tag changes aren't updating contact status">
    Only tags you've explicitly classified as Interested or Blocked in the integration panel trigger an action — an unclassified tag is logged and ignored. Open **Edit Statuses** and confirm the tag is assigned to one of the lists, and that **auto-map labels to status** is enabled if you expect the contact's status field (not just the email disposition) to change.

    If the tag is classified correctly and still nothing changes, check whether RevyOps has any email history for that lead at all. Both `lead.tag_changed` and `email.positive_reply` look for an existing inbound email to attach the status/disposition to; if none exists, RevyOps tries to fall back to the lead's most recent outbound email to build one — but if there's no outbound email on file for that contact either, the update is silently skipped.
  </Accordion>

  <Accordion title="Events arriving twice for a client">
    Check whether the client has both a direct per-client Sendkit webhook and agency-level mapping enabled for Sendkit on the Campaign Mapping page. The per-client webhook should no-op automatically in that case — if you're still seeing duplicates, confirm the client isn't also registered under a second Sendkit workspace pointed at both URLs.
  </Accordion>

  <Accordion title="Contacts aren't being pushed to Sendkit's blacklist">
    Confirm the Sendkit API key is valid and the integration shows as configured, and that **Cross-Platform Do Not Contact Sync** is toggled on for Sendkit. For contacts marked DNC before the integration was connected, use **Sync Historical DNC List** to backfill them.
  </Accordion>
</AccordionGroup>
