Drift does not offer a native Volanea app or marketplace plugin, but you can still send email from Drift reliably with a private Drift app, an outbound webhook, and a small server-side relay. The relay receives the Drift event, looks up the contact details Drift does not put in the webhook, then calls Volanea’s REST API with a stable idempotency key.

This is the right architecture because Drift webhooks are designed to notify an external HTTPS endpoint when something happens in Drift. That endpoint can safely hold secrets, enrich the event through Drift’s API, apply your sending rules, and submit one transactional message to Volanea. It also keeps your Volanea API key out of browser code, chat widget settings, and any client-visible configuration.

What this integration does

The example in this guide sends a follow-up email when a Drift conversation becomes ready for downstream processing. The specific Drift trigger is the conversation_push webhook event.

Drift describes conversation_push as a combined stream for conversations that are ready to be consumed by another system. In practice, it can happen when a conversation is closed manually, becomes inactive under Drift’s configured rules, is closed by a playbook rule, or is manually pushed for sync. That makes it a practical trigger for a post-chat recap, next-steps email, requested resource, or internal handoff notification.

The full path is:

  1. A visitor chats with your team or playbook in Drift.
  2. The conversation reaches Drift’s conversation_push state.
  3. Drift sends an HTTPS POST to your relay endpoint.
  4. Your relay verifies Drift’s verification token.
  5. Your relay fetches the conversation from Drift to obtain contactId.
  6. Your relay fetches that contact and reads attributes.email and attributes.name.
  7. Your relay applies your eligibility rules, such as requiring an email address and excluding test contacts.
  8. Your relay calls POST https://api.volanea.com/v1/send.
  9. Volanea accepts the email for its sending pipeline, while the relay records the result for debugging.

This approach is intentionally not a browser-to-email-API connection. Drift’s widget runs in a visitor’s browser, and anything placed in front-end JavaScript can be inspected, copied, and abused. The send belongs on a server.

Why use a Drift webhook relay instead of a direct client-side call

Drift supports webhook subscriptions for custom apps. In the Drift developer app configuration, you provide a request URL and choose the events your app should receive. Drift then posts a JSON event to that URL.

That is outbound HTTP capability, but it is not a secure place to configure a Volanea secret key. The Drift configuration contains the webhook destination and the Drift app’s own credentials; it is not a general-purpose secret vault for an arbitrary downstream email API. A relay is therefore not an unnecessary extra layer. It is where identity verification, secret storage, data enrichment, content decisions, duplicate prevention, and observability belong.

A direct request from the page where the Drift widget is installed would create several problems:

  • A Volanea secret key would be exposed in JavaScript, browser developer tools, network requests, or a built bundle.
  • Any visitor could reuse that key to attempt unauthorized email sends.
  • You would not have a trusted way to determine whether a request actually originated from Drift.
  • The browser may lack the contact information and permissions needed to construct the message correctly.
  • Retry behavior would be difficult to control, making duplicate sends more likely.

The server-side relay avoids these problems. It stores secrets in environment variables or your platform’s secret manager, receives only server-to-server webhooks, and makes the Volanea request with a server-controlled idempotency key.

For a quick proof of concept, you can also use a middleware tool such as Zapier to catch a Drift webhook, retrieve the related Drift conversation and contact, and then make an authenticated Volanea request. Drift documents a Zapier custom-webhook pattern for conversation_push. For production email that needs robust duplicate controls and precise error handling, a small relay you own is usually the clearer option.

The concrete Drift trigger: conversation_push

This guide uses conversation_push, not a generic “form submitted” or “pipeline stage changed” trigger. Drift’s model is conversation-centric: the event indicates that a conversation is ready for another application to consume.

Choose conversation_push when the email should be sent after the interaction is effectively complete. Typical examples include:

  • A visitor asked for documentation and an agent promised a resource.
  • A sales rep completed qualification and wants to send a recap.
  • A playbook collected enough information to send a tailored next step.
  • A support conversation ended and you want to send a case summary or follow-up survey.
  • A team needs an internal email alert when a conversation is ready for review.

This trigger is not equivalent to “every message in a chat.” If you need immediate event-by-event processing, Drift also has webhook event types such as new_message, new_conversation, and contact_identified. Those events have different semantics and different noise profiles. A new_message trigger can create many sends for one conversation unless your relay adds strict filtering.

What Drift sends to your webhook

Drift sends an HTTPS POST containing a common envelope with these top-level fields:

  • type: the webhook event type.
  • token: the verification token assigned to your Drift app.
  • orgId: the Drift organization ID.
  • data: event-specific data.

For a conversation_push event, use the conversation ID in the data object as the handoff identifier. The webhook is deliberately lightweight: it tells your integration which conversation is ready, rather than embedding every contact attribute and transcript field you may need.

A representative conversation_push payload looks like this:

{
  "type": "conversation_push",
  "token": "your-drift-verification-token",
  "orgId": 123456,
  "data": {
    "conversationId": 116119985
  }
}

Do not treat the webhook body as a complete email profile. Instead, use data.conversationId to retrieve the conversation from Drift. Drift’s conversation endpoint returns the conversation model, including contactId; then retrieve the contact by ID. In the contact model, email and name live under attributes, including attributes.email and attributes.name.

That lookup sequence matters because a conversation can exist even when Drift has no usable email address for the visitor. Your relay must handle that state deliberately rather than trying to send to undefined, an empty string, or an arbitrary custom field.

Set up the private Drift app and webhook subscription

Create a private Drift app for your own Drift organization. A private app is appropriate when the integration is only for your account; you do not need to publish an app or create a public marketplace listing.

Your app needs permission to read the objects used by the relay. At minimum, the flow needs the ability to receive and inspect the conversation event, retrieve the conversation, and retrieve the contact. Configure the scopes required for conversation_read and contact_read in your Drift app, then generate or reconnect the token after scope changes if Drift requires that for the new permissions to take effect.

In the app’s webhook event configuration:

  1. Enter the public HTTPS URL for your relay, such as https://automation.example.com/webhooks/drift.
  2. Subscribe to conversation_push.
  3. Save the app configuration and make sure the app is connected to your Drift organization.
  4. Copy the app’s verification token into your server-side secret store as DRIFT_VERIFICATION_TOKEN.
  5. Store the Drift private bearer token as DRIFT_ACCESS_TOKEN in the same server-side secret store.

Your relay should compare the token Drift supplies with the expected verification token. Drift supplies its verification token in the body as token and in the X-Verification-Token header. Checking the header first and falling back to the body is practical, but the important rule is that you verify it before treating the event as trusted.

Do not rely on a fixed allowlist of Drift IP addresses. Drift documents that its outbound IP addresses can change. The verification token is the stable mechanism designed for authenticating incoming webhook traffic.

Test with a real conversation, not only a test payload

Drift can send a test webhook payload, which is useful for confirming that the URL is reachable. But the test payload may not contain the live conversationId data your application needs for its full lookup chain.

After confirming connectivity, run one real test:

  1. Start a chat as a site visitor.
  2. Provide a real test email address in the conversation or ensure the contact already has one.
  3. Close the conversation or otherwise cause it to be pushed.
  4. Inspect the webhook log at your relay.
  5. Confirm that the relay retrieved both the conversation and contact.
  6. Confirm that Volanea accepted the send.
  7. Check the resulting email’s content, from address, links, and deliverability status.

Use a test recipient you control. Avoid testing on a production lead without clear consent and a message that is appropriate for the customer journey.

Field mapping from Drift to Volanea

The webhook itself provides the event identity, while the subsequent Drift API calls supply the recipient data. The relevant mapping is straightforward:

PurposeDrift sourceVolanea field
Event identitytypeUsed for relay routing and validation
Drift organizationorgIdUsed in the idempotency key and logs
Conversation identifierdata.conversationIdUsed in the idempotency key and Drift conversation request
Contact identifierconversation.data.contactIdUsed to fetch the contact
Recipient emailcontact.data.attributes.emailto
Recipient namecontact.data.attributes.nameUsed in subject and HTML/text personalization
Verified senderServer configfrom
Message contentServer templatesubject, text, and html

The from value should be an address on a domain you have verified in Volanea. Do not copy a visitor’s address into from; that would conflict with authentication and reply behavior. If you want a human reply path, configure a mailbox you control as replyTo only after confirming the field you use in your Volanea implementation and message design.

For this example, the relay sends a short follow-up that acknowledges the Drift conversation and tells the visitor that a team member will follow up. In a real deployment, your content might be chosen by conversation tag, playbook ID, an agent command, a custom contact attribute, or a separate business system.

Working Node.js relay: Drift webhook to Volanea REST API

The following Express handler verifies the Drift event, uses conversationId to fetch the conversation and contact, checks that a recipient email is available, and sends one message through Volanea.

It uses the Volanea single-send endpoint, POST /v1/send, with Bearer authentication, JSON content, and an Idempotency-Key header. The stable key drift-conversation-push:<orgId>:<conversationId> is intentional: if Drift or your infrastructure repeats the same event, Volanea can recognize the send operation as the same one instead of creating another email.

import "dotenv/config";
import crypto from "node:crypto";
import express from "express";

const app = express();
app.use(express.json({ limit: "256kb" }));

const {
  DRIFT_VERIFICATION_TOKEN,
  DRIFT_ACCESS_TOKEN,
  VOLANEA_API_KEY,
  VOLANEA_FROM_EMAIL
} = process.env;

for (const name of [
  "DRIFT_VERIFICATION_TOKEN",
  "DRIFT_ACCESS_TOKEN",
  "VOLANEA_API_KEY",
  "VOLANEA_FROM_EMAIL"
]) {
  if (!process.env[name]) throw new Error(`Missing required secret: ${name}`);
}

function tokensMatch(actual, expected) {
  if (typeof actual !== "string" || typeof expected !== "string") return false;
  const a = Buffer.from(actual);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

async function driftGet(path) {
  const response = await fetch(`https://driftapi.com${path}`, {
    headers: {
      Authorization: `Bearer ${DRIFT_ACCESS_TOKEN}`
    }
  });

  if (!response.ok) {
    throw new Error(`Drift GET ${path} failed: ${response.status} ${await response.text()}`);
  }

  return response.json();
}

app.post("/webhooks/drift", async (req, res) => {
  const event = req.body;
  const headerToken = req.get("X-Verification-Token");
  const suppliedToken = headerToken || event?.token;

  if (!tokensMatch(suppliedToken, DRIFT_VERIFICATION_TOKEN)) {
    return res.status(401).json({ error: "Invalid Drift verification token" });
  }

  if (event?.type !== "conversation_push") {
    return res.status(204).end();
  }

  const conversationId = event?.data?.conversationId;
  const orgId = event?.orgId;

  if (!Number.isFinite(conversationId) || !Number.isFinite(orgId)) {
    return res.status(400).json({ error: "Missing numeric orgId or data.conversationId" });
  }

  try {
    // Drift response shape: { data: { id, contactId, status, ... } }
    const conversationResponse = await driftGet(`/conversations/${conversationId}`);
    const contactId = conversationResponse?.data?.contactId;

    if (!Number.isFinite(contactId)) {
      console.info("Skipping Drift conversation without contactId", { orgId, conversationId });
      return res.status(202).json({ skipped: "missing_contact_id" });
    }

    // Drift response shape: { data: { id, attributes: { email, name, ... } } }
    const contactResponse = await driftGet(`/contacts/${contactId}`);
    const attributes = contactResponse?.data?.attributes || {};
    const recipientEmail = attributes.email;
    const recipientName = attributes.name || "there";

    if (typeof recipientEmail !== "string" || recipientEmail.trim() === "") {
      console.info("Skipping Drift contact without email", { orgId, conversationId, contactId });
      return res.status(202).json({ skipped: "missing_email" });
    }

    const subject = "Thanks for chatting with us";
    const text = `Hi ${recipientName},\n\nThanks for chatting with our team. We have received your conversation and will follow up with the next steps shortly.\n\nBest,\nThe team`;
    const html = `<p>Hi ${escapeHtml(recipientName)},</p><p>Thanks for chatting with our team. We have received your conversation and will follow up with the next steps shortly.</p><p>Best,<br>The team</p>`;

    const idempotencyKey = `drift-conversation-push:${orgId}:${conversationId}`;

    const sendResponse = await fetch("https://api.volanea.com/v1/send", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${VOLANEA_API_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey
      },
      body: JSON.stringify({
        from: VOLANEA_FROM_EMAIL,
        to: recipientEmail.trim(),
        subject,
        text,
        html
      })
    });

    const sendBody = await sendResponse.text();

    if (!sendResponse.ok) {
      throw new Error(`Volanea send failed: ${sendResponse.status} ${sendBody}`);
    }

    console.info("Volanea email accepted", {
      orgId,
      conversationId,
      contactId,
      recipientEmail,
      idempotencyKey,
      response: sendBody
    });

    return res.status(202).json({ accepted: true });
  } catch (error) {
    console.error("Drift-to-Volanea workflow failed", {
      orgId,
      conversationId,
      error: error instanceof Error ? error.message : String(error)
    });

    // A non-2xx response tells Drift delivery was not successfully processed.
    return res.status(500).json({ error: "Could not process Drift webhook" });
  }
});

function escapeHtml(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}

app.listen(process.env.PORT || 3000, () => {
  console.log("Listening for Drift webhooks");
});

The relay makes three outbound calls after receiving one valid event: one to retrieve the conversation, one to retrieve the contact, and one to send the email. That is a reasonable starting point, but it is still synchronous work. As volume grows, move the event into a durable queue after token validation and return a successful response quickly; have a worker run the Drift lookups and Volanea send.

For more endpoint detail and sending options, refer to the Volanea email API reference.

Keep Volanea credentials server-side

The Volanea API key belongs in your relay’s secret manager or environment configuration, not in Drift’s widget script, a front-end build variable, a public Git repository, or a Drift custom contact attribute.

In the code example, these values are server-only environment variables:

DRIFT_VERIFICATION_TOKEN=replace-with-drift-app-verification-token
DRIFT_ACCESS_TOKEN=replace-with-drift-private-token
VOLANEA_API_KEY=sk_replace-with-volanea-secret-key
VOLANEA_FROM_EMAIL=team@your-verified-domain.example

Each secret has a distinct job:

  • DRIFT_VERIFICATION_TOKEN proves that the request reaching your endpoint matches the Drift app you configured.
  • DRIFT_ACCESS_TOKEN authorizes the relay to retrieve the conversation and contact records needed to prepare the email.
  • VOLANEA_API_KEY authorizes the send request to Volanea.
  • VOLANEA_FROM_EMAIL defines the sender address and should be controlled by your deployment configuration.

Never put VOLANEA_API_KEY in a client-visible configuration value such as NEXT_PUBLIC_*, VITE_*, a script tag, a browser extension configuration, or a JavaScript snippet added through a tag manager. Those mechanisms are for public identifiers, not mail-sending authority.

Use separate Volanea keys for development, staging, and production where possible. A test environment should send only to approved test recipients or use a non-production route, not send live messages to leads from copied Drift data.

Design the email rules before enabling the webhook

A completed conversation is a useful signal, but it is not automatically permission to send any type of email. Decide exactly what qualifies a contact for the follow-up and encode the criteria on the server.

A simple starting rule might be: send only when the Drift contact has an email address, the conversation is associated with a selected playbook, and the email is a transactional follow-up the visitor reasonably expects. For marketing content, apply your consent model and subscription rules before creating the send.

Useful server-side guards

Before calling Volanea, consider adding checks for:

  • A missing, malformed, or known test email address.
  • Internal company domains and QA contacts.
  • A conversation tag that signifies spam, disqualification, or no follow-up.
  • A custom contact attribute such as email_followup_opt_in.
  • A specific relatedPlaybookId that identifies the intended playbook journey.
  • An existing completed-send record for the same conversation and message purpose.
  • A required resource URL, meeting link, or owner assignment that has not yet been created.

Do not make the email content depend on unescaped chat text. If you include a Drift-supplied name or field in HTML, escape it as the example does. If you include transcript excerpts, treat them as untrusted content and consider sending a link to a secure internal system instead of embedding the entire transcript in an email.

Use business-purpose idempotency keys

The example’s idempotency key represents one defined business action: one follow-up send for one Drift organization and one pushed conversation.

That is better than using a random UUID per request. A random UUID makes every retry appear to be a new action, so it cannot prevent duplicate sends. Conversely, a key that is too broad, such as drift-followup, would incorrectly collapse emails from different conversations.

If you intentionally send several email types after one conversation, scope the key by purpose:

conversation-recap:<orgId>:<conversationId>
meeting-confirmation:<orgId>:<conversationId>
internal-owner-alert:<orgId>:<conversationId>

The recipient is not required in the example key because the conversation is already the business entity. If your recipient selection can change after the first attempt, store a send record with the original recipient and purpose, then choose whether the changed recipient should receive a separate, explicitly defined send.

When this breaks

Every webhook workflow should assume that failures will happen between systems. The goal is not to promise exactly-once execution across Drift, your host, Drift’s API, and Volanea. The goal is to make retries safe, expose failures quickly, and prevent a recoverable error from becoming duplicate email or lost follow-up.

Drift retries can create duplicate delivery attempts

A webhook sender may retry when it receives a timeout, connection failure, or non-successful HTTP response. Your endpoint can also process the event successfully but lose the network response before Drift receives it. From Drift’s perspective, that can look like a failure even though Volanea has already accepted the email.

The first defense is the stable Volanea Idempotency-Key. Reuse the exact same key whenever the same conversation_push event is retried. The example derives it from the event type, organization, and conversation ID, so a repeat delivery makes the same send request rather than a new send operation.

For stronger operational control, store a database row keyed by the same business key before sending. Include states such as received, processing, accepted, skipped_missing_email, and failed. A unique database constraint gives you a second deduplication layer and allows you to inspect whether a webhook was ignored, retried, or accepted.

Webhook timeouts can make an otherwise valid flow look failed

The synchronous version does two Drift API lookups and one Volanea request before returning 202. A slow network, transient API latency, cold start, or logging bottleneck can make that path slow enough for the webhook sender to treat it as timed out.

For low volume, the code is easy to understand and can be sufficient. For higher volume or critical workflows, split ingestion from processing:

  1. Verify the Drift token.
  2. Validate that the event is conversation_push and has a conversation ID.
  3. Persist the event or enqueue it durably using orgId and conversationId.
  4. Respond promptly with a success status.
  5. Let a worker fetch Drift data and call Volanea.

The worker should retain the same idempotency key across retries. This pattern reduces webhook timeout risk without sacrificing reliable sending.

Payload fields may be absent because the webhook is intentionally sparse

Do not assume the webhook payload contains an email address, contact name, transcript, playbook details, or all the attributes your email template needs. The confirmed common payload structure is an event envelope with type, token, orgId, and an event-specific data object. For the conversation workflow, use the conversation ID and then retrieve the related records.

Even after the lookups, contact.data.attributes.email may be absent. Drift contacts can be created before the visitor provides an email, and a conversation can exist without an email-ready contact. The right default is to skip and log the event, not to guess an address or route the message to an unrelated participant.

If the email is only appropriate after a visitor provides an address, you can use contact_identified as a separate signal or require that the relevant playbook collects email before the conversation reaches the follow-up path. Keep the business condition explicit: a contact existing is not the same as a contact being emailable.

Drift API permission errors can appear after app changes

If the relay can receive the webhook but gets an authorization error when retrieving conversations or contacts, check the app’s scopes and its current token. Drift’s documentation notes that changing scopes may require reconnecting or regenerating the token so the new permissions are granted.

Use least privilege, but include the read permissions you actually need. A webhook event subscription does not by itself grant the right to fetch every related object. Confirm that the private app has the conversation and contact read access required for the calls in your relay.

A Volanea acceptance response is not a delivered-in-inbox guarantee

A successful response from the send endpoint means Volanea accepted the message into its sending flow. It does not mean the recipient has read it, and it does not eliminate later outcomes such as a suppression skip, mailbox rejection, bounce, complaint, or provider delay.

Use a verified sending domain, keep your from address consistent with the use case, and make the email expected and relevant to the just-completed Drift interaction. For send-volume and plan considerations, review transactional email pricing.

Template changes can accidentally create a new business action

If you change the message content and replay a past event with the same idempotency key, Volanea should treat it as the same original send operation. That is normally the correct behavior for retry safety.

If you truly need to send a second, newly approved message about the same conversation, define a distinct business purpose in the key. For example, followup-v2:<orgId>:<conversationId> is not a retry of the earlier follow-up; it is a different action and should be reviewed as such.

A no-code alternative using Zapier

If you cannot deploy a relay immediately, Drift documents a custom webhook pattern using Zapier. The sequence is similar: configure a Drift custom app, subscribe it to conversation_push, paste a Zapier Catch Hook URL into Drift, and retrieve the conversation and contact using the Drift API before performing downstream work.

The important limitation remains: the webhook payload is not your final email payload. Your Zap still needs to use the conversation ID to retrieve the conversation, use the returned contactId to retrieve the contact, and only then map attributes.email and attributes.name into the Volanea request.

A practical Zap design is:

  1. Webhooks by Zapier — Catch Hook: receive the Drift conversation_push event.
  2. Filter: continue only when type equals conversation_push and a conversation ID exists.
  3. Webhooks by Zapier — Custom Request: GET the Drift conversation with the Drift Bearer token.
  4. Webhooks by Zapier — Custom Request: GET the Drift contact using the returned contact ID.
  5. Filter: continue only when the contact’s email is present.
  6. Webhooks by Zapier — Custom Request: POST to Volanea with the Bearer key, JSON body, and an idempotency key derived from the Drift conversation ID.

Keep the Volanea key in Zapier’s authenticated connection or encrypted secret configuration, never in a field passed from Drift or exposed in a public webhook URL. Also test Zapier retry behavior. The same idempotency principle applies: retries must reuse the same key for the same conversation and email purpose.

Testing and operating the integration

Treat this integration as an event-driven production service, even if it begins as a short serverless function. A few basic controls make it much easier to maintain.

First, log identifiers rather than full sensitive payloads. A useful structured log includes orgId, conversationId, contactId, event type, recipient domain or a redacted recipient, idempotency key, Drift lookup status, and Volanea response status. Avoid recording full conversation transcripts or secret tokens in standard logs.

Second, create an alert for repeated failures. If Drift requests begin returning 500, your team should know quickly. If a particular missing field becomes common, that may signal a playbook change or a new lead-capture path that no longer collects email.

Third, test the unhappy paths on purpose:

  • Send a webhook with an invalid verification token and confirm it receives 401.
  • Send a valid-shaped event with no conversation ID and confirm it receives 400.
  • Use a conversation whose contact has no email and confirm it is safely skipped.
  • Simulate a temporary Volanea failure and confirm the job can retry with the same idempotency key.
  • Submit the same valid event twice and confirm it does not create two independently sent emails.
  • Change a Drift app scope in a test environment and verify that your deployment procedure refreshes the token where required.

Finally, version the content and rules in source control. The code path that decides whether to email a person is business logic, not merely an integration detail. Review it as carefully as you would a payment or account-provisioning workflow.

Conclusion

To send email from Drift with Volanea, use Drift’s real outbound webhook model rather than looking for a native plugin that does not exist. Subscribe a private Drift app to conversation_push, receive the event at a server-side relay, verify Drift’s token, retrieve the conversation and contact, map the contact’s email and name into Volanea’s POST /v1/send request, and use a stable Idempotency-Key based on the conversation.

That design keeps your Volanea secret key private, accounts for the fact that Drift webhook payloads are event notifications rather than complete recipient records, and makes duplicate deliveries much less likely when a webhook is retried. Start with a narrow, expected follow-up use case, log every decision, and move to a queue-backed worker when the workflow becomes business-critical or high-volume.

FAQ

Does Drift have a native Volanea integration?

No. Drift does not provide a native Volanea app, marketplace listing, or install flow for this connection. Use Drift’s webhook subscriptions with a server-side relay, or use a middleware workflow such as Zapier.

What Drift event should trigger the email?

For a follow-up after a completed or ready-to-sync conversation, use conversation_push. It provides the conversation ID your relay needs to retrieve the conversation and its associated contact.

Can I send directly from a Drift playbook to Volanea?

Do not put a Volanea secret key in a playbook, widget script, or client-visible configuration. Send through a server-side webhook receiver that keeps the key in environment secrets and calls Volanea from trusted backend code.

Why does the relay call Drift again after receiving the webhook?

The webhook is an event notification, not a full recipient profile. The relay uses data.conversationId to retrieve the conversation, obtains contactId, then retrieves the contact whose attributes contain values such as email and name.

How do I stop duplicate emails if Drift retries the webhook?

Use the same Volanea Idempotency-Key for every attempt to process the same email purpose and Drift conversation. A key such as drift-conversation-push:<orgId>:<conversationId> makes retries identifiable as the same send operation rather than separate sends.