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

# Scribe webhook use cases: sync conversions, alert on failed installs

> What teams build with Scribe webhooks: push conversions into a CRM, build Google Ads and Meta retargeting audiences, and alert on failed signature installs.

Scribe webhooks push a signed HTTPS request to your own endpoint every time something happens in your workspace: a conversion is attributed, a signature fails to install, a teammate is added or removed, a campaign goes live. Instead of polling the API on a schedule and hoping your window was tight enough, you receive the event and act on it.

This page walks through the workflows teams build most often, with the events to subscribe to and the code to handle each one. Set your endpoint up first in the [overview](/webhooks/overview), then browse the [event catalog](/webhooks/events) for the exact payload of every event.

## What you can build

<CardGroup cols={2}>
  <Card title="Attribute revenue in your CRM" icon="chart-line">
    Push signature-attributed conversions into your CRM or warehouse as they happen, with the teammate, template, and campaign that earned them.
  </Card>

  <Card title="Retarget engaged visitors" icon="target">
    Feed Google Ads Customer Match and Meta Custom Audiences with people who clicked an email signature, and suppress them once they convert.
  </Card>

  <Card title="Alert on broken installs" icon="triangle-alert">
    Tell IT the moment a signature fails to install in a mailbox, with the error code and the mailbox that needs attention.
  </Card>

  <Card title="Repair integrations fast" icon="plug-zap">
    Catch a revoked Google Workspace or Microsoft 365 connection before signatures go stale, and route an admin straight to the reconnect screen.
  </Card>

  <Card title="Automate onboarding" icon="users">
    Mirror teammate joins and departures into your ITSM, HR tool, or internal directory without a nightly sync.
  </Card>

  <Card title="Coordinate campaigns" icon="megaphone">
    Announce a banner going live in Slack, and annotate your analytics dashboards with the exact start and end times.
  </Card>

  <Card title="Keep an audit trail" icon="database">
    Archive every workspace event with its timestamp, so you can answer compliance questions months later.
  </Card>
</CardGroup>

## Which webhook events to subscribe to

Subscribe each endpoint to only what it needs. A narrow subscription means less traffic to verify, fewer payloads to ignore, and a delivery log you can actually read.

| Your goal                      | Subscribe to                                                                                      |
| ------------------------------ | ------------------------------------------------------------------------------------------------- |
| Revenue attribution            | `conversion.recorded`, `visitor.identified`                                                       |
| Ad retargeting and suppression | `visitor.identified`, `conversion.recorded`                                                       |
| Deployment health alerts       | `signature.installation.failed`, `integration.access_lost`                                        |
| Onboarding and offboarding     | `teammate.created`, `teammate.deleted`                                                            |
| Campaign coordination          | `campaign.scheduled`, `campaign.started`, `campaign.paused`, `campaign.resumed`, `campaign.ended` |
| Audit trail and compliance     | `signature.published`, `integration.connected`, `integration.disconnected`                        |

## How to receive a Scribe webhook

All the examples below sit behind the same skeleton: verify the signature, acknowledge immediately, then do the real work outside the request. Scribe expects a `2xx` within 15 seconds, so anything slower than a database write belongs in a queue.

```javascript theme={null}
import express from "express";
import { Webhook } from "svix";

const app = express();
const wh = new Webhook(process.env.SCRIBE_WEBHOOK_SECRET);

// Verification needs the raw body, so do not parse JSON before it.
app.post("/webhooks/scribe", express.raw({ type: "application/json" }), async (req, res) => {
  let event;
  try {
    event = wh.verify(req.body, req.headers);
  } catch {
    return res.sendStatus(400);
  }

  // Acknowledge first. Retries are expensive for both sides.
  res.sendStatus(200);

  // svix-id is stable across retries of the same message: use it to deduplicate.
  await queue.add("scribe-event", { id: req.headers["svix-id"], event });
});
```

<Warning>
  Never trust a payload you have not verified. An unverified endpoint accepts anything the internet posts at it, including fake conversions and fake departures.
</Warning>

## Sync signature conversions into your CRM or warehouse

Subscribe to `conversion.recorded` to push signature-attributed revenue into your CRM as it lands. The event fires for every attributed, deduplicated conversion and carries the attribution chain with it: which teammate's signature, which template, which email, which campaign. That is enough to close the loop between an email signature and a closed deal without a single join in your warehouse.

```javascript theme={null}
async function onConversionRecorded(event, deliveryId) {
  const { visitor, attribution, properties } = event;

  await crm.upsertOpportunity({
    // event_id is the idempotency key you passed when tracking. It can be null,
    // so fall back to the svix-id of the delivery.
    externalId: event.event_id ?? deliveryId,
    contactExternalId: visitor.user_id,
    name: event.name,
    amount: event.value,
    currency: event.currency,
    closedAt: event.occurred_at,
    source: "Email signature",
    ownerUuid: attribution.teammate_uuid || null,
    campaignUuid: attribution.marketing_campaign_uuid || null,
    plan: properties?.plan,
  });
}
```

Pair it with `visitor.identified`, which fires the first time a signature-attributed visitor resolves to a known `user_id`. That is the event that turns an anonymous click into a person, so it is the natural moment to create the contact record the conversion will later attach to. Attribution uuids arrive as empty strings when they could not be resolved, so treat them as optional.

<Tip>
  Writing to a warehouse instead of a CRM? Land the raw payload in a staging table keyed on the `svix-id` header, then model it downstream. Replays and duplicate deliveries collapse on their own, and you keep the original event if your schema changes later.
</Tip>

See [conversion tracking](/en/conversion-tracking) for how conversions get recorded in the first place.

## Build retargeting audiences in Google Ads and Meta

Subscribe to `visitor.identified` to feed a first-party retargeting audience the moment someone who clicked an email signature identifies themselves. These are people your team already emailed and who then engaged, which makes them a warmer audience than anything a pixel collects from cold traffic, and it costs you no extra tracking on your site.

The event carries the visitor's `traits`, so when your site identifies people with an email address you have everything both platforms need. Google Ads Customer Match and Meta Custom Audiences both accept a SHA-256 hash of the normalized address, so the raw email never leaves your systems.

```javascript theme={null}
import { createHash } from "node:crypto";

// Both platforms expect SHA-256 of the trimmed, lowercased address.
const hashEmail = (email) => createHash("sha256").update(email.trim().toLowerCase()).digest("hex");

async function onVisitorIdentified(event) {
  const email = event.visitor.traits?.email;
  if (!email) return;

  // Only upload people whose consent covers advertising audiences.
  if (!(await consent.allowsAdvertising(email))) return;

  const hashedEmail = hashEmail(email);

  await Promise.all([
    googleAds.customerMatch.add({
      userListId: process.env.GOOGLE_ADS_USER_LIST_ID,
      hashedEmail,
    }),
    meta.customAudiences.add({
      audienceId: process.env.META_AUDIENCE_ID,
      schema: ["EMAIL_SHA256"],
      data: [[hashedEmail]],
    }),
  ]);
}
```

Then use `conversion.recorded` for the other half of the job: taking people out of the audience once they convert, so you stop paying to advertise to customers you already won. That payload identifies the visitor by `user_id` and `anonymous_id` rather than by email, so resolve the address from your own records before you suppress.

```javascript theme={null}
async function suppressConverted(event) {
  const person = await db.people.findByUserId(event.visitor.user_id);
  if (!person?.email) return;

  const hashedEmail = hashEmail(person.email);

  await Promise.all([
    googleAds.customerMatch.remove({
      userListId: process.env.GOOGLE_ADS_USER_LIST_ID,
      hashedEmail,
    }),
    googleAds.customerMatch.add({
      userListId: process.env.GOOGLE_ADS_CONVERTED_LIST_ID,
      hashedEmail,
    }),
  ]);
}
```

Three things worth building on top of that:

* **Segment by what they clicked.** The attribution in both payloads names the `signature_template_uuid` and `marketing_campaign_uuid` behind the click, so you can keep one audience per banner campaign and match the ad creative to the signature that earned the visit.
* **Seed value-based lookalikes.** `conversion.recorded` carries `value` and `currency`, which is exactly what Google Ads and Meta want for value-based Similar segments and Lookalike Audiences. Send converters with their value rather than a flat list.
* **Close the loop server side.** Meta's Conversions API accepts a server event with the hashed email in `user_data` plus the value in `custom_data`, so a signature-attributed conversion can be reported without relying on a browser pixel firing.

<Warning>
  Uploading customer identifiers is regulated. You need a lawful basis and, in most of Europe, explicit consent covering advertising before an address goes to an ad platform, and both platforms hold you to their own customer data policies. Always hash, always check consent first, and honor deletions in your audiences as well as in your own database.
</Warning>

<Note>
  Neither platform serves ads against a tiny list. Both enforce a minimum matched audience size, so a new audience needs time to fill before campaigns can run against it. Check the current threshold in the Google Ads and Meta documentation when you size your first list.
</Note>

## Alert your team when a signature install fails

Subscribe to `signature.installation.failed` to catch a broken install the moment it happens, rather than when somebody notices their emails look wrong. The event fires once per failure and names the mailbox, the template, the integration, and a machine-readable `error_code`.

```javascript theme={null}
async function onInstallationFailed(event) {
  const { email, signature, integration, error_code } = event;

  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      text: [
        `Signature install failed for *${email.address}*`,
        `Template: ${signature.name} (${signature.kind})`,
        `Integration: ${integration.kind}`,
        `Reason: \`${error_code}\``,
      ].join("\n"),
    }),
  });
}
```

Route by error code so the alert reaches someone who can fix it. `user_not_found` usually means a mailbox was deleted or renamed and belongs with IT. `unauthorized_client` and `forbidden` point at OAuth scopes or admin consent. `signature_too_long` and `multi_signatures_not_supported` are design problems for whoever owns the template. The catalog lists the [current error codes](/webhooks/events), and new ones can appear, so always keep a default branch.

Because the event fires on the transition into failure rather than on every retry, a mailbox that stays broken will not page you daily. Track your own open items and close them when a later install succeeds.

## Reconnect a broken Google Workspace or Microsoft 365 integration

Subscribe to `integration.access_lost` to hear the moment Scribe loses access to a connected Google Workspace, Microsoft 365, or directory integration. When credentials are revoked or expire, Scribe pauses installs and teammate syncs through that integration, and every hour it stays broken, more people carry an out of date signature. The event arrives with a `reconnect_url` that deep-links to the exact screen an admin needs.

```javascript theme={null}
async function onIntegrationAccessLost(event) {
  const { integration, error_message, reconnect_url } = event;

  await pagerduty.trigger({
    summary: `Scribe lost access to ${integration.name}`,
    severity: integration.category === "signatures" ? "error" : "warning",
    details: error_message,
    links: [{ href: reconnect_url, text: "Reconnect in Scribe" }],
  });
}
```

Escalate on `category`, not on the integration name. Losing a `signatures` integration stops deployments to real mailboxes and deserves a page. Losing a `smart_fields` or `assets` integration usually means stale content, which can wait for business hours. Subscribe to `integration.connected` and `integration.disconnected` alongside it to resolve the incident automatically when someone reconnects.

## Automate teammate onboarding and offboarding

Subscribe to `teammate.created` and `teammate.deleted` to drive onboarding and offboarding from the same signal Scribe uses. Both fire whether the change came from the dashboard, the API, or a directory sync importing and removing people, which makes them a single source of truth for downstream automation no matter how the person entered your workspace.

```javascript theme={null}
async function onTeammateEvent(event) {
  const primary = event.emails.find((e) => e.primary) ?? event.emails[0];

  if (event.type === "teammate.created") {
    await itsm.createTask({
      title: `Onboarding: ${event.teammate.display_name}`,
      assignee: "it-team",
      body: `Scribe teammate created for ${primary.address}. Confirm their signature installs.`,
    });
    return;
  }

  // teammate.deleted is your last chance to read their identifiers.
  await directory.markOffboarded({
    teammateUuid: event.teammate.uuid,
    email: primary.address,
    offboardedAt: event.occurred_at,
  });
}
```

The deletion payload includes the emails and uuid of the person being removed, which the API can no longer return once they are gone. Store what you need from the payload itself rather than planning a follow-up fetch.

## Track campaign status when banners go live

Subscribe to the five `campaign` events to follow a banner from scheduled to archived. They share one payload, so a single handler covers the whole lifecycle. The common pattern is a Slack post when a banner starts and ends, plus an annotation pushed into your analytics tool so the traffic spike has a label the next time somebody asks what caused it.

```javascript theme={null}
const CAMPAIGN_EVENTS = new Set([
  "campaign.scheduled",
  "campaign.started",
  "campaign.paused",
  "campaign.resumed",
  "campaign.ended",
]);

async function onCampaignEvent(event) {
  if (!CAMPAIGN_EVENTS.has(event.type)) return;
  const { campaign } = event;

  if (event.type === "campaign.started" || event.type === "campaign.ended") {
    await analytics.createAnnotation({
      label: `${campaign.name} ${event.type === "campaign.started" ? "live" : "ended"}`,
      timestamp: event.occurred_at,
      templates: campaign.signature_template_uuids,
    });
  }

  await slack.post("#marketing", `${campaign.name} is now ${campaign.status}`);
}
```

Start and end transitions are detected on a schedule, so `occurred_at` can trail the campaign's own `start_time` by a few minutes. Use `start_time` and `end_time` from the payload when you need the intended schedule, and `occurred_at` when you need the moment the change actually took effect.

## Connect Scribe webhooks to Zapier, Make, or n8n

You do not need a server to use Scribe webhooks. Zapier, Make, and n8n each give you a catch hook URL you can register as an endpoint, which turns any Scribe event into a step in a workflow. It is the fastest path from event to spreadsheet row, ticket, or notification.

<Steps>
  <Step title="Create a catch hook in your automation tool">
    In Zapier, Make, or n8n, add a webhook trigger and copy the URL it gives you.
  </Step>

  <Step title="Register it in Scribe">
    Open **Settings → Webhooks**, click **Manage webhooks**, add the URL, and subscribe it to the events that workflow needs.
  </Step>

  <Step title="Send a test delivery">
    Use the portal's test feature to send a sample event, so your tool can learn the payload shape before you build the rest of the steps.
  </Step>

  <Step title="Map the fields you care about">
    Point `event.type` at a router step, then map fields such as `conversion.value` or `email.address` into the action.
  </Step>
</Steps>

<Note>
  Most automation platforms do not verify webhook signatures for you. Anyone who learns the URL can post to it, so keep these endpoints for low-stakes notifications, or add a verification step before any action that writes to a system of record.
</Note>

## Keep an audit trail of workspace changes

Subscribe one endpoint to every event type and write each payload to append-only storage, and you get a full workspace history for free: who published which signature and when, which integrations were connected or removed, when each teammate arrived and left. Store the raw JSON plus the `svix-id` and `svix-timestamp` headers, and you can answer compliance questions months later without reconstructing anything.

## Webhook best practices for a reliable receiver

<CardGroup cols={2}>
  <Card title="Deduplicate on svix-id" icon="fingerprint">
    Delivery is at-least-once. The same message can arrive twice, and `svix-id` is stable across retries of it.
  </Card>

  <Card title="Order by occurred_at" icon="clock">
    Events are delivered independently and can arrive out of order. Sequence on the payload timestamp, never on arrival time.
  </Card>

  <Card title="Ignore what you do not know" icon="shield-check">
    New event types and new fields are added over time. Handle the types you subscribed to and skip the rest instead of failing.
  </Card>

  <Card title="Replay from the portal" icon="rotate-ccw">
    An outage on your side loses nothing. Fix the endpoint, then replay the failed deliveries from the webhook portal.
  </Card>
</CardGroup>

## Frequently asked questions

### Do I need to build a server to receive Scribe webhooks?

No. Any HTTPS URL works, including the catch hook URL from an automation platform such as Zapier, Make, or n8n. Build your own endpoint when you need signature verification and control over how failures are retried, which anything writing to a system of record deserves.

### How do I stop the same event from being processed twice?

Deduplicate on the `svix-id` header. Delivery is at-least-once, so a message can occasionally arrive more than once, and that header stays the same across every retry of the same message.

### What happens if my endpoint is down?

Nothing is lost. Failed deliveries are retried automatically with exponential backoff over roughly a day, and endpoints that keep failing are eventually disabled. Once yours is healthy, re-enable it and replay the missed deliveries from the webhook portal.

### Can I test a webhook before sending real traffic to it?

Yes. The webhook portal sends test deliveries to any endpoint and logs the full request and response of every attempt, so you can confirm your verification and routing work before you subscribe to live events.

### Which events fire for a signature that was never installed?

Only `signature.published`. Installation events describe a specific mailbox, so `signature.installation.failed` fires when an install into that mailbox is attempted and fails, not when a template sits unused.

### Who can create webhook endpoints in Scribe?

Workspace owners and admins, on a plan that includes API access. Endpoints are managed from **Settings → Webhooks** in the dashboard, covered in [managing your webhooks](/en/webhooks).

## Next steps

<CardGroup cols={2}>
  <Card title="Set up an endpoint" icon="webhook" href="/webhooks/overview">
    Register a URL, verify signatures, and learn the delivery guarantees.
  </Card>

  <Card title="Event catalog" icon="list" href="/webhooks/events">
    Every event Scribe sends, with a full payload example.
  </Card>

  <Card title="API use cases" icon="braces" href="/api-reference/use-cases">
    What to build with the REST API, for the workflows you pull instead of receive.
  </Card>

  <Card title="Conversion tracking" icon="chart-line" href="/en/conversion-tracking">
    How conversions are recorded and attributed before they reach your endpoint.
  </Card>
</CardGroup>
