Cal.com can trigger scheduling events, but it should not hold the credentials that send mail. This guide shows how to send email from Cal.com through a signed webhook, a small server-side relay, and Volanea’s transactional email API.

The important architectural detail comes first: Volanea does not have a native Cal.com marketplace app or plug-in. Instead, Cal.com can send an outbound webhook when a booking event occurs, and your webhook receiver turns that event into an authenticated Volanea API request. That gives you control over templates, sender identity, retries, duplicate prevention, and the data you include in each message.

The integration architecture: Cal.com webhook to Volanea API

The reliable path is:

  1. A scheduling event happens in Cal.com.
  2. Cal.com sends an HTTPS webhook to a server endpoint you control.
  3. Your endpoint verifies the X-Cal-Signature-256 signature using the webhook secret configured in Cal.com.
  4. Your endpoint extracts the booking recipient and event details.
  5. The endpoint calls POST https://api.volanea.com/v1/send with a Volanea secret key and a stable Idempotency-Key.
  6. Volanea queues and sends the transactional email from your authenticated sending domain.

For a straightforward appointment confirmation, the concrete Cal.com trigger is Booking Created, represented in webhook payloads as BOOKING_CREATED. Cal.com also supports booking cancellations, reschedules, requests, paid bookings, meeting events, and form submissions. The right trigger depends on when an email should be true and useful, not simply when it is technically possible to send one.

For example, BOOKING_CREATED is a good trigger for a confirmation or internal notification. BOOKING_RESCHEDULED is appropriate for a new meeting-time notice. BOOKING_CANCELLED should normally send a cancellation confirmation or a rebooking link, rather than reusing the original confirmation copy.

This architecture is intentionally server-to-server. A Cal.com booking page, embed, browser script, or client-visible environment variable must never call Volanea directly. A browser cannot safely keep an email API key private, and a leaked sending key can be abused to send mail from your account.

What Cal.com actually provides for outbound events

Cal.com provides native webhooks. In the Cal.com dashboard, webhook subscriptions are configured at /settings/developer/webhooks. A subscription has a subscriber URL, one or more event triggers, an optional shared secret, and an optional custom payload template.

For Cal.com SaaS, the subscriber URL must be an HTTPS endpoint. Localhost, HTTP URLs, private IP ranges, and cloud metadata addresses are blocked. That means a local development endpoint must be exposed through an HTTPS tunnel or deployed to a reachable preview environment before Cal.com can deliver to it.

The native webhook is the right direct trigger, but it is not a direct Cal.com-to-Volanea call. Cal.com’s webhook configuration gives you a destination URL and a secret for signing deliveries; it is not a generic HTTP client where you should paste a Volanea bearer key into an authorization-header field. Put a relay between the two systems.

Why a relay is necessary

A webhook receiver is more than a compatibility shim. It is the boundary where you can:

  • verify that the request was genuinely sent by Cal.com;
  • reject unwanted triggers and malformed payloads;
  • choose the recipient using your business rules;
  • escape booking data before inserting it into email HTML;
  • apply consent and suppression logic before attempting a send;
  • assign one stable idempotency key per logical email;
  • return a fast successful response to Cal.com after safely accepting the work;
  • log a correlation ID without retaining more booking data than you need.

It also prevents the common mistake of connecting a scheduling tool to an email provider with no authentication or duplicate-send strategy. Calendar events are operational data. Treat them as events that must be validated, normalized, and processed at least once—not as trusted form data that can be copied blindly into an email request.

When Zapier or Make is a better fit

If you do not operate a server endpoint, Cal.com also documents Zapier for no-code automation, and Make provides Cal.com modules that can watch booking-created, booking-cancelled, booking-rescheduled, and meeting-ended activity. Those routes can work for lower-risk operational notifications.

However, a small server-side receiver is usually preferable for attendee-facing email. It lets you validate Cal.com’s raw webhook signature, keep the Volanea key outside an automation editor, use a stable idempotency key, and control HTML rendering. If you use an automation platform, keep the same principles: store the Volanea key in that platform’s encrypted connection or secret store, do not expose it in a public webhook URL, and make duplicate handling explicit.

Configure the Cal.com Booking Created webhook

Create the webhook before writing email code so that you know exactly which event shape your receiver must accept. In Cal.com, go to the developer webhook settings and create a subscription with these values:

  • Subscriber URL: your public HTTPS receiver, such as https://app.example.com/webhooks/cal-com.
  • Event trigger: Booking Created.
  • Secret: a long, random value created specifically for this webhook.
  • Custom Payload: leave this unset initially so your receiver can work with Cal.com’s standard payload shape.

The webhook secret is not the Volanea API key. It is a shared secret used to prove that a delivery originated from the Cal.com subscription you configured. Your server calculates an HMAC-SHA256 digest over the exact raw request body and compares it against Cal.com’s X-Cal-Signature-256 header.

Do not use a short memorable phrase as the secret. Generate a random value, store it in the same protected server-side secret manager as your other integration credentials, and rotate it if you suspect exposure.

The payload you should expect

A standard booking webhook has an outer event envelope and a nested payload object. The key fields for this use case are the event name, timestamp, booking UID, title, start and end times, organizer information, attendee data, and booking responses.

A representative BOOKING_CREATED event looks like this:

{
  "triggerEvent": "BOOKING_CREATED",
  "createdAt": "2026-10-01T15:30:00.538Z",
  "payload": {
    "uid": "cal-booking-7f3b2a",
    "type": "30min",
    "title": "30min between Acme Scheduling and Jamie Lee",
    "description": "Initial implementation call",
    "additionalNotes": "Please discuss the API migration.",
    "startTime": "2026-10-03T17:00:00.000Z",
    "endTime": "2026-10-03T17:30:00.000Z",
    "organizer": {
      "id": 5,
      "name": "Acme Scheduling",
      "email": "calendar@example.com",
      "timeZone": "America/New_York"
    },
    "attendees": [
      {
        "name": "Jamie Lee",
        "email": "jamie@example.net",
        "timeZone": "America/Los_Angeles"
      }
    ],
    "responses": {
      "name": {
        "label": "your_name",
        "value": "Jamie Lee"
      },
      "email": {
        "label": "email_address",
        "value": "jamie@example.net"
      }
    }
  }
}

Treat this as a shape to map defensively, not a promise that every booking has every optional field. Booking forms can be customized. A team event can have different attendee behavior than a personal event. Some historical payload versions and custom payload templates may expose data differently. In the receiver below, the first attendee email is preferred and the email response is used as a fallback.

If your scheduling flow allows more than one attendee, decide whether each attendee should receive an individual email. Do not put multiple external addresses into one visible recipient list simply because they arrived in the same booking payload. For a per-attendee message, make one Volanea API call per address and use a distinct idempotency key per booking, trigger, and recipient.

Keep the Volanea API key out of Cal.com

The Volanea API key does not belong in Cal.com’s webhook configuration. Cal.com holds the destination URL and optional webhook signing secret; your relay owns the Volanea secret key.

Store the sending key in a server-side environment variable or managed secret store, for example:

CAL_WEBHOOK_SECRET="replace-with-a-long-random-secret"
VOLANEA_API_KEY="sk_live_replace_with_your_secret_key"
VOLANEA_FROM_EMAIL="notifications@example.com"
VOLANEA_FROM_NAME="Acme Scheduling"

The deployment environment that runs your webhook receiver should expose these variables only to server code. Do not prefix them with a framework convention that makes variables public to the browser. For example, avoid NEXT_PUBLIC_, VITE_, or any equivalent public client configuration prefix for VOLANEA_API_KEY.

An API key in a browser bundle is not protected by being in an environment file. Once shipped to a user’s browser, it can be read through developer tools, downloaded JavaScript, source maps, browser extensions, or intercepted requests. The same rule applies to client-side Cal.com embed scripts, frontend automation code, and publicly shared code snippets.

Your sending address must also be configured deliberately. Use an address on a domain you have authenticated for sending in Volanea. A scheduling confirmation sent from an unverified or unrelated domain is more likely to be rejected, filtered, or confusing to recipients. Review the email API reference and setup guides when configuring the sending domain and sender identity.

Build the signed webhook receiver

The following Node.js example is written as a small server-side handler. It uses only the Node standard library and fetch, so the core logic can be adapted to Express, Fastify, Next.js route handlers, Cloudflare-compatible server code with equivalent crypto APIs, or another server framework.

The crucial implementation details are:

  1. Read the raw body before parsing JSON.
  2. Verify x-cal-signature-256 against that raw body.
  3. Accept only BOOKING_CREATED for this route.
  4. Validate the recipient email rather than assuming it exists.
  5. Escape dynamic booking values before inserting them into HTML.
  6. Build a stable idempotency key from the booking UID, event type, and recipient.
  7. Call Volanea from server code using the secret key.
import crypto from "node:crypto";

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

function safeText(value = "") {
  return String(value).replace(/[\r\n]+/g, " ").trim();
}

function getRecipient(booking) {
  const attendee = booking.attendees?.[0];
  const email = attendee?.email ?? booking.responses?.email?.value;
  const name = attendee?.name ?? booking.responses?.name?.value ?? "there";

  if (typeof email !== "string" || !email.includes("@")) {
    return null;
  }

  return { email: email.trim(), name: safeText(name) };
}

function verifyCalSignature(rawBody, providedSignature) {
  if (!providedSignature) return false;

  const expected = crypto
    .createHmac("sha256", process.env.CAL_WEBHOOK_SECRET)
    .update(rawBody, "utf8")
    .digest("hex");

  const expectedBuffer = Buffer.from(expected, "utf8");
  const providedBuffer = Buffer.from(providedSignature, "utf8");

  return (
    expectedBuffer.length === providedBuffer.length &&
    crypto.timingSafeEqual(expectedBuffer, providedBuffer)
  );
}

export async function handleCalComWebhook(request) {
  const rawBody = await request.text();
  const signature = request.headers.get("x-cal-signature-256");

  if (!verifyCalSignature(rawBody, signature)) {
    return new Response("Invalid Cal.com signature", { status: 401 });
  }

  let event;
  try {
    event = JSON.parse(rawBody);
  } catch {
    return new Response("Invalid JSON", { status: 400 });
  }

  if (event.triggerEvent !== "BOOKING_CREATED") {
    return new Response(null, { status: 204 });
  }

  const booking = event.payload;
  const recipient = getRecipient(booking);
  const bookingUid = booking?.uid;

  if (!bookingUid || !recipient) {
    console.error("Cal.com booking is missing uid or recipient", {
      triggerEvent: event.triggerEvent,
      bookingUid
    });
    return new Response("Missing required booking fields", { status: 422 });
  }

  const title = safeText(booking.title || booking.type || "your meeting");
  const start = safeText(booking.startTime || "the scheduled time");
  const organizerName = safeText(booking.organizer?.name || "our team");

  const html = `
    <p>Hi ${escapeHtml(recipient.name)},</p>
    <p>Your booking is confirmed.</p>
    <ul>
      <li><strong>Event:</strong> ${escapeHtml(title)}</li>
      <li><strong>Starts:</strong> ${escapeHtml(start)}</li>
      <li><strong>Host:</strong> ${escapeHtml(organizerName)}</li>
    </ul>
    <p>If you need to make a change, use the scheduling link in your Cal.com booking details.</p>
  `;

  const text = [
    `Hi ${recipient.name},`,
    "",
    "Your booking is confirmed.",
    `Event: ${title}`,
    `Starts: ${start}`,
    `Host: ${organizerName}`
  ].join("\n");

  // One logical confirmation per booking, trigger, and recipient.
  const idempotencyKey = `calcom:${bookingUid}:BOOKING_CREATED:${recipient.email.toLowerCase()}`;

  const volaneaResponse = await fetch("https://api.volanea.com/v1/send", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.VOLANEA_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": idempotencyKey
    },
    body: JSON.stringify({
      fromEmail: process.env.VOLANEA_FROM_EMAIL,
      fromName: process.env.VOLANEA_FROM_NAME,
      to: [
        {
          email: recipient.email,
          name: recipient.name
        }
      ],
      subject: `Confirmed: ${title}`,
      html,
      text
    })
  });

  const responseBody = await volaneaResponse.text();

  if (!volaneaResponse.ok) {
    console.error("Volanea send failed", {
      bookingUid,
      status: volaneaResponse.status,
      responseBody
    });

    // Return a retryable response only for failures you want Cal.com
    // or your queue layer to attempt again.
    return new Response("Email provider error", { status: 502 });
  }

  console.info("Booking confirmation accepted by Volanea", {
    bookingUid,
    recipient: recipient.email,
    idempotencyKey
  });

  return new Response(null, { status: 204 });
}

The field mapping in this example is explicit:

Cal.com fieldVolanea email fieldPurpose
payload.attendees[0].emailto[0].emailPrimary attendee recipient
payload.responses.email.valuefallback for to[0].emailSupports bookings where attendee data is not available in the expected place
payload.attendees[0].nameto[0].nameRecipient personalization
payload.responses.name.valuefallback for to[0].nameFallback name from booking responses
payload.titlesubject, html, textEvent description
payload.startTimehtml, textScheduled start time
payload.organizer.namehtml, textHost identity
payload.uidIdempotency-KeyStable booking-level send identity

Do not use the mutable booking title as the only idempotency value. Two bookings can have the same event title, and a title can be edited. The booking UID is the intended event-level identifier. Include the trigger name too, so a cancellation email does not collide with the original confirmation.

Format booking times for the recipient

The sample receiver inserts Cal.com’s ISO timestamp directly because it keeps the mapping clear. In production, format the timestamp in the recipient’s time zone before generating the email.

Cal.com booking payloads can include organizer and attendee time-zone context. Use the attendee’s time zone when it is available and valid; otherwise, choose a documented fallback such as the organizer’s time zone or UTC. Avoid silently formatting every booking in the server’s local time zone. That can produce correct-looking but incorrect emails when the server runs in a different region.

A recipient-friendly confirmation should generally include:

  • the event name;
  • the date and time with a named time zone;
  • the duration or end time when useful;
  • the host or team name;
  • a reschedule or cancellation path when your booking flow supports one;
  • meeting-location details only when they are appropriate to disclose by email.

Keep additional notes and custom booking answers out of confirmation emails by default. Those fields can contain sensitive information, internal instructions, or untrusted text. If you do include a custom answer, apply an allowlist of known field names, escape it for HTML, and ensure that the recipient is entitled to see it.

Use templates without losing event-level control

The example sends rendered HTML and plain text because it is easy to inspect while building the integration. For a stable production flow, use a well-tested email layout and keep Cal.com values as small, controlled substitutions.

A good separation is:

  • application code: chooses the trigger, validates data, calculates idempotency, and decides whether email should be sent;
  • email template: controls branding, layout, accessibility, and message copy;
  • Cal.com webhook: supplies booking facts, not email markup or sending credentials.

This arrangement reduces the chance that a calendar field unexpectedly changes your email structure. It also makes it easier for a support or operations team to update confirmation wording without modifying booking logic.

If your confirmation messages are business-critical, include both HTML and text content. The text alternative helps recipients whose email clients block HTML and provides a more readable fallback for assistive technologies and plain-text clients.

When this breaks: failures specific to this hop

A Cal.com-to-Volanea flow crosses two HTTP boundaries: Cal.com to your receiver, then your receiver to Volanea. Design for partial failure. The booking may have been created even if email has not been sent; the email may have been accepted even if your receiver did not receive the API response; and Cal.com can retry a delivery when it does not get a timely successful response.

Cal.com retries can create duplicate sends

Webhook delivery is not the same as exactly-once processing. If your endpoint times out after Volanea accepted the request, Cal.com may retry the webhook. Without duplicate protection, that retry can produce a second confirmation email.

The Idempotency-Key in the code is the first protection. It derives from the stable Cal.com booking UID, trigger event, and recipient email. When the same logical delivery reaches Volanea again with the same key and request body, Volanea can replay the earlier result rather than create another message.

For stronger protection, also persist an event record in your own database before or while sending. Store the booking UID, trigger event, recipient, idempotency key, processing state, Volanea response identifier, and timestamps. A durable record helps when duplicate deliveries occur outside the API idempotency window, when your code changes the email body, or when you need to answer a support question later.

Webhook timeouts should not wait on slow work

Your webhook route should verify the signature, validate the minimum fields, save or enqueue the work, and return promptly. Avoid doing unrelated CRM lookups, PDF generation, analytics calls, or long-running template work before responding to Cal.com.

For higher-volume systems, use this sequence:

  1. Verify the raw webhook signature.
  2. Parse and validate the event.
  3. Write an idempotent job record keyed by booking UID and trigger.
  4. Return a 2xx response.
  5. Let a background worker call Volanea with the same stable idempotency key.

That changes the webhook route from a fragile synchronous email sender into an event-ingestion endpoint. It also lets you retry provider failures independently, with controlled exponential backoff, without asking Cal.com to redeliver the entire event.

Payload fields can be missing or customized

Do not assume every payload contains attendees[0], responses.email.value, a location, notes, or a particular custom question. Event types can use different booking fields, teams can have different scheduling configurations, and a custom payload template can remove fields that your receiver expects.

The safe rule is to validate the fields that your particular email requires. For a basic confirmation, that typically means a booking UID, recipient email, and start time. If a required field is missing, log the minimal diagnostic information, return or record a clear failure, and route the event for manual review rather than emailing an organizer address by accident.

Before turning on the webhook for all event types, test at least these cases:

  • a normal one-to-one booking;
  • a booking with optional fields omitted;
  • a reschedule and cancellation, if you will enable those triggers;
  • a team or round-robin event type, if applicable;
  • a booking with unusual characters in a name or event title;
  • a duplicate delivery using the same raw body;
  • a provider error or network timeout.

Signature verification fails after middleware parses JSON

HMAC verification must run against the untouched raw request body. Some frameworks automatically parse JSON and discard the original bytes unless you configure a raw-body route. Re-serializing parsed JSON changes whitespace and key ordering, so the signature no longer matches.

If every Cal.com delivery returns 401 Invalid Cal.com signature, inspect whether your framework has consumed the body before the verification function runs. Also confirm that the webhook secret is the one configured on that specific Cal.com subscription, not your Volanea key or a different environment’s secret.

A successful API response is not an inbox placement guarantee

A successful Volanea send response means the API accepted the message for its sending pipeline. It does not mean every recipient will see it in the inbox. Domain authentication, recipient suppression, invalid addresses, mailbox-provider filtering, and recipient-side rules still affect delivery.

Use a verified sending domain, make the sender recognizable, avoid putting sensitive booking details in a subject line, and inspect delivery events or sending logs when a recipient reports a missing message. For scheduling mail, consistency and clarity matter more than promotional design.

Add reschedule and cancellation messages deliberately

Once the BOOKING_CREATED path works, create separate logic for other Cal.com triggers instead of sending the same template for every event.

A BOOKING_RESCHEDULED email should state that the time changed, show the new time prominently, and avoid language that sounds like a new booking. A BOOKING_CANCELLED email should say the meeting is cancelled and should not include a stale meeting link as though it remains active.

Use separate idempotency keys such as:

calcom:{bookingUid}:BOOKING_CREATED:{recipientEmail}
calcom:{bookingUid}:BOOKING_RESCHEDULED:{recipientEmail}
calcom:{bookingUid}:BOOKING_CANCELLED:{recipientEmail}

That allows one message for each meaningful lifecycle event while still making retries of the same event safe. If Cal.com exposes a unique reschedule sequence or updated timestamp that matters to your business, persist it in your event table so that an older delayed event cannot overwrite or confuse a newer state.

Operational and privacy checklist

Before enabling the webhook in production, use this checklist:

  • Authenticate the domain used in VOLANEA_FROM_EMAIL.
  • Use a dedicated Cal.com webhook secret, separate from all API keys.
  • Verify X-Cal-Signature-256 against the untouched raw request body.
  • Permit only the Cal.com trigger events your endpoint is designed to process.
  • Validate recipient email addresses and required booking identifiers.
  • Escape all dynamic values inserted into HTML.
  • Use a stable Idempotency-Key based on booking UID, trigger, and recipient.
  • Keep Volanea credentials in server-side secrets only.
  • Log booking IDs and provider results, but minimize stored attendee data.
  • Test failure paths and duplicate webhook deliveries before enabling the integration broadly.

The sender should also have a clear owner. Someone should know where to rotate the Cal.com webhook secret, where to rotate the Volanea key, what mailbox receives delivery alerts, and how to disable the webhook if an incident occurs. A small integration becomes much more reliable when those operational responsibilities are defined before it is needed.

Conclusion

The dependable way to send email from Cal.com with Volanea is not a marketplace installation. It is a signed Booking Created webhook delivered to your server, followed by a verified, idempotent call to Volanea’s POST /v1/send endpoint.

That extra server-side hop is valuable. It keeps the Volanea secret out of Cal.com and the browser, validates the booking event, handles missing fields safely, prevents duplicate sends during retries, and gives you a durable place to control templates and delivery behavior. Once the confirmation path is working, the same pattern can support reschedules, cancellations, internal alerts, and post-meeting follow-ups without turning calendar events into uncontrolled email sends.

FAQ

Can Cal.com send directly to Volanea without my server?

Not securely through the native webhook configuration. Cal.com webhooks deliver events to a subscriber URL and can sign those deliveries with a shared secret, while Volanea sending requires a secret API key. Use a server-side relay or a trusted automation platform secret store rather than exposing the Volanea key in client-visible configuration.

What Cal.com event should trigger a confirmation email?

Use Booking Created (BOOKING_CREATED) for an initial confirmation. Add separate handlers and templates for BOOKING_RESCHEDULED and BOOKING_CANCELLED so recipients receive accurate lifecycle-specific messages.

How do I stop duplicate booking emails?

Verify the webhook, then send each logical message with a stable Volanea Idempotency-Key, such as a combination of Cal.com booking UID, trigger event, and recipient email. For stronger guarantees, persist processed events in your own database or queue before sending.

Where should the Volanea API key be stored?

Store it only in a server-side environment variable or managed secret store used by the webhook receiver. Never put it in Cal.com custom payloads, browser JavaScript, Cal.com embeds, a public repository, or a client-exposed environment variable.

What happens if the attendee email is missing from the webhook payload?

Do not send a fallback email to the organizer automatically. Validate the attendee email from payload.attendees[0].email or the booking response fallback, record the event as incomplete, and investigate the event type’s booking fields or custom payload configuration.