Square can notify your application when a payment changes, but it does not include a native Volanea app or marketplace plug-in. To send email from Square reliably, receive Square’s webhook at a server you control, validate it, map the payment data, and call Volanea’s REST API from that server.

This guide builds a practical post-payment email flow: Square emits payment.updated, your endpoint proceeds only when the payment is COMPLETED, and Volanea sends a receipt-adjacent thank-you or fulfillment message. The same architecture works for other Square events, but payment updates make the data, timing, and duplicate-delivery concerns especially clear.

What the Square-to-Volanea integration actually is

The integration has three components, not two:

  1. Square Webhooks posts an event notification to an HTTPS URL when a subscribed resource changes.
  2. Your middleware endpoint validates Square’s signature, applies business rules, prevents duplicate processing, and obtains any additional data required.
  3. Volanea’s REST API accepts the final recipient, sender, subject, and email content, then handles email delivery.

That middle layer is essential. Square webhooks are notifications intended for an application endpoint; they are not a point-and-click email automation editor and they cannot safely hold a Volanea secret. A browser, Square checkout page, or public front-end JavaScript also must never call the sending API with your account key.

For a production service, the middleware can be a Node/Express application, a serverless function, or a containerized worker. The important properties are less about framework choice than about behavior: it must be reachable over HTTPS, preserve the raw request body for signature verification, return successful responses promptly, and keep a durable record of processed Square event IDs.

The right trigger: payment.updated with COMPLETED

Square’s payment.updated webhook is the concrete event to subscribe to for this example. The webhook event envelope has type: "payment.updated", and its data.object.payment field contains a Payment object. A payment can be created before it is complete, updated more than once, delayed, canceled, or fail. Therefore, subscribing to payment.created and sending immediately is usually too early for a “thank you for your paid order” message.

Instead, inspect data.object.payment.status and send only when it is exactly COMPLETED. This makes the business rule explicit. It also avoids treating a pending, failed, or canceled payment as a sale.

A representative Square event has this shape. Values and optional fields vary by payment method and seller configuration, but the nesting is the part your handler must respect:

{
  "merchant_id": "ML123EXAMPLE",
  "type": "payment.updated",
  "event_id": "9d8f9a2e-1111-2222-3333-444455556666",
  "created_at": "2026-10-14T12:00:00.000Z",
  "data": {
    "type": "payment",
    "id": "payment-id-from-square",
    "object": {
      "payment": {
        "id": "payment-id-from-square",
        "status": "COMPLETED",
        "amount_money": {
          "amount": 2599,
          "currency": "USD"
        },
        "receipt_email": "buyer@example.com",
        "receipt_number": "123456",
        "created_at": "2026-10-14T11:59:12.000Z",
        "customer_id": "customer-id-if-associated"
      }
    }
  }
}

amount_money.amount is an integer in the currency’s smallest unit. For USD, 2599 means $25.99; do not render it as $2599 or use floating-point arithmetic. receipt_email is also optional, so a handler must not assume it exists merely because a payment completed.

Configure the Square webhook subscription

Create a webhook subscription in Square’s Developer Console for the Square application that receives payments. Set its notification URL to your public HTTPS endpoint, such as https://hooks.example.com/square/payment-updated, and subscribe to the payment.updated event type.

Square associates a signature key with the webhook subscription. Store that signature key as a server-side secret named something like SQUARE_WEBHOOK_SIGNATURE_KEY. Also save the exact notification URL used in the subscription as SQUARE_WEBHOOK_URL. Signature verification includes the URL, so changing a trailing slash, hostname, or proxy scheme without updating the stored value can make otherwise genuine notifications fail validation.

Before pointing a live subscription at your production handler, use Square’s Sandbox. Make a Sandbox payment and inspect the actual JSON received by your endpoint. This reveals which optional Payment fields your specific payment flow supplies and lets you test the COMPLETED transition without contacting real customers.

Webhook setup checks

Use this checklist before enabling customer-facing email:

  • The endpoint is publicly accessible over HTTPS; localhost needs a secure tunnel during local testing.
  • The endpoint URL in Square matches the URL used in signature verification byte-for-byte as a URL string.
  • The subscription includes payment.updated, not only payment.created.
  • Your server reads the raw request bytes before JSON parsing for the verification step.
  • Your Square and Volanea credentials are separate secrets with separate rotation procedures.
  • Sandbox and production use different Square credentials, URLs, and Volanea test or production sending configurations as appropriate.

Square webhook delivery should be treated as an at-least-once notification mechanism. Your application, not the subscription screen, decides whether an event creates an email. That distinction is what prevents a normal retry from becoming two thank-you messages.

Build the secure webhook receiver

The example below uses Express. It validates the x-square-hmacsha256-signature header using HMAC-SHA256 over the exact notification URL concatenated with the raw JSON body. Only after validation does it parse the JSON.

The code uses a placeholder hasProcessedEvent and markEventProcessed persistence layer. Replace those functions with a database transaction, Redis record with an appropriate expiry, or a queue-backed idempotency store. An in-memory Set is useful only for a local demonstration because it is lost on a restart and is not shared between instances.

import express from "express";
import crypto from "node:crypto";

const app = express();

const squareWebhookUrl = process.env.SQUARE_WEBHOOK_URL;
const squareSignatureKey = process.env.SQUARE_WEBHOOK_SIGNATURE_KEY;
const volaneaApiKey = process.env.VOLANEA_API_KEY;

function isValidSquareSignature(rawBody, suppliedSignature) {
  if (!suppliedSignature || !squareWebhookUrl || !squareSignatureKey) return false;

  const expectedSignature = crypto
    .createHmac("sha256", squareSignatureKey)
    .update(squareWebhookUrl + rawBody)
    .digest("base64");

  const expected = Buffer.from(expectedSignature, "utf8");
  const supplied = Buffer.from(suppliedSignature, "utf8");
  return expected.length === supplied.length && crypto.timingSafeEqual(expected, supplied);
}

// Implement these with durable storage. The event_id is the idempotency key.
async function hasProcessedEvent(eventId) {
  return false;
}
async function markEventProcessed(eventId) {
  // INSERT eventId with a unique constraint in a real database.
}

app.post(
  "/square/payment-updated",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const rawBody = req.body.toString("utf8");
    const signature = req.get("x-square-hmacsha256-signature");

    if (!isValidSquareSignature(rawBody, signature)) {
      return res.status(403).send("Invalid Square webhook signature");
    }

    const event = JSON.parse(rawBody);
    const payment = event?.data?.object?.payment;

    // Acknowledge irrelevant events and non-final payment states without sending.
    if (event.type !== "payment.updated" || payment?.status !== "COMPLETED") {
      return res.sendStatus(200);
    }

    // A completed payment does not guarantee an email address is present.
    if (!payment.receipt_email) {
      console.warn("Completed Square payment has no receipt_email", {
        eventId: event.event_id,
        paymentId: payment.id
      });
      return res.sendStatus(200);
    }

    if (await hasProcessedEvent(event.event_id)) {
      return res.sendStatus(200);
    }

    const currency = payment.amount_money.currency;
    const amount = new Intl.NumberFormat("en-US", {
      style: "currency",
      currency
    }).format(payment.amount_money.amount / 100);

    const emailPayload = {
      from: "Example Store <orders@example.com>",
      to: [payment.receipt_email],
      subject: `Thanks for your payment of ${amount}`,
      text: [
        `Thanks for your payment of ${amount}.`,
        `Square receipt number: ${payment.receipt_number ?? "not available"}.`,
        `Payment ID: ${payment.id}`
      ].join("\n"),
      html: `
        <p>Thanks for your payment of <strong>${amount}</strong>.</p>
        <p>Square receipt number: ${payment.receipt_number ?? "not available"}.</p>
        <p>Payment ID: ${payment.id}</p>
      `
    };

    const volaneaResponse = await fetch("https://api.volanea.com/v1/emails", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${volaneaApiKey}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify(emailPayload)
    });

    if (!volaneaResponse.ok) {
      const detail = await volaneaResponse.text();
      console.error("Volanea send failed", {
        status: volaneaResponse.status,
        detail,
        eventId: event.event_id
      });
      // Return a non-2xx response so Square can retry delivery.
      return res.status(502).send("Email provider send failed");
    }

    await markEventProcessed(event.event_id);
    return res.sendStatus(200);
  }
);

app.listen(process.env.PORT || 3000);

The mapping in this example is deliberate: payment.receipt_email becomes Volanea’s to recipient; a verified address on your own domain becomes from; amount_money becomes the message copy; and the Square payment ID and receipt number provide customer-service context. Do not use unescaped customer-entered values in HTML. If you add customer names, item descriptions, or notes to html, HTML-escape them first.

Review Volanea’s email API setup reference before deploying so the sender identity, request schema, and authentication details match your account and environment. The sender domain should be authenticated before live sending; a successful API request is not the same thing as optimal inbox placement.

Keep Volanea credentials off Square and out of the browser

The Volanea API key belongs only in your server’s secret manager or encrypted environment configuration. In the example, it is read from process.env.VOLANEA_API_KEY; it is never included in the Square webhook URL, a Square note, client-side source code, or a mobile application.

This is not merely a housekeeping recommendation. Any key placed in JavaScript shipped to a browser can be extracted. Any key embedded in a client-visible configuration variable, public repository, screenshot, or front-end build can be used to send mail from your account, damage your sender reputation, and expose customer messaging workflows.

Use separate secret values for each concern:

  • SQUARE_WEBHOOK_SIGNATURE_KEY verifies that incoming requests originated from Square.
  • SQUARE_WEBHOOK_URL is configuration needed to reproduce the signed payload input.
  • VOLANEA_API_KEY authorizes outbound email from your service.
  • Your database credentials support idempotency and operational logging.

Restrict who can read and rotate these values. If your hosting platform supports it, inject secrets at runtime rather than storing a .env file in source control. Log a Square event ID, payment ID, Volanea response status, and internal email job ID—but never log API keys, authorization headers, or full message bodies containing personal data.

Why not call Volanea directly from a Square page?

Square Webhooks calls your endpoint server-to-server. It does not expose a safe mechanism that turns a payment into an arbitrary authenticated REST request with your third-party API secret. Even if a client-side checkout callback can observe a payment result, it is not a trustworthy source of final payment state and it exposes credentials if it sends email directly.

A server-side webhook receiver solves both issues: Square signs the event, and your service holds the Volanea key. It also gives you a single place to enforce suppression rules, consent requirements, sending windows, fraud review holds, and support-team exclusions.

Decide what email is appropriate after a Square payment

A completed-payment message is usually transactional when it confirms a purchase, provides fulfillment details, or gives support information directly tied to that transaction. Keep it narrow. A customer should be able to understand why they received it from the message itself.

Promotional content is different. Adding a broad marketing campaign to a receipt-style email can trigger consent, unsubscribe, and regional compliance obligations. Separate the operational email from any later campaign workflow, and only enroll recipients when you have the appropriate permission and clear records of how it was collected.

Useful completed-payment messages

A good initial use case is one of the following:

  • A payment confirmation with the payment amount and support contact.
  • A service booking confirmation that includes next steps or preparation instructions.
  • A digital-delivery notice after your fulfillment system confirms access.
  • An internal alert to an operations address for unusually large orders, subject to privacy controls.
  • A request for missing delivery information, provided it is genuinely required to complete the transaction.

Avoid making the email a second copy of a sensitive payment instrument detail. Square’s Payment object can contain useful identifiers, but customer emails should include only the minimum needed for recognition and support. Square already supplies its own receipt mechanisms; position a Volanea message as your store’s confirmation or fulfillment communication rather than pretending to be Square’s official receipt.

When this breaks: retries, timeouts, and incomplete payloads

Every hop in this integration can fail independently. A robust design expects that outcome rather than interpreting it as an unusual edge case.

Square retries can create duplicate-send pressure

Square retries webhook notifications when your endpoint does not return a successful response. That is correct behavior: a timeout, crashed instance, transient network failure, or 502 can leave Square unable to know whether your application completed the work.

The difficult case is a failure after Volanea accepted the email but before your service stored the event as processed or returned 200. Square can retry the event, and a naive handler sends again. The defense is idempotency:

  1. Insert event.event_id into a durable table with a unique constraint before or as part of processing.
  2. Associate a state such as processing, sent, or failed with that record.
  3. Make subsequent delivery attempts inspect that record instead of blindly calling Volanea again.
  4. Reconcile processing records safely if a worker crashes mid-flight.

For higher volume, acknowledge a verified event quickly after storing it and hand email work to a queue. Your worker then retries Volanea failures under controlled rules. This reduces Square webhook timeout risk, but it makes durable queueing and idempotent jobs even more important.

Webhook timeouts and slow downstream calls

Do not perform slow, unrelated work in the request path: inventory reconciliation, CRM enrichment, PDF generation, or multiple external API lookups all make the Square response slower and less reliable. The receiver should verify, validate, record, enqueue or send, and respond.

Set a bounded timeout on the Volanea HTTP request in production. A request that waits indefinitely consumes workers and can trigger Square retries. If your timeout fires, record the outcome as uncertain rather than assuming neither side succeeded; that uncertainty is precisely why a queue and an event ledger are valuable.

Fields can be absent even for valid payments

Webhook payloads are not guaranteed to contain every field your template would like. receipt_email may be absent when no email was collected for the payment. customer_id may be absent when the payment was not associated with a Square customer. Receipt details and other feature-specific data can also vary by checkout flow, seller settings, region, product capability, permissions, and account plan.

Never make a template depend on an optional field without a fallback. The example acknowledges a completed payment without receipt_email and logs a structured warning rather than attempting a send to undefined. If customer email is essential, retrieve the Customer only when you have a valid customer_id, the necessary Square API access, and a lawful reason to use that contact information.

Do not silently substitute an internal employee address for a missing buyer email. That hides data-quality problems and can make test messages look like successful customer delivery.

Bad signatures and changed URLs

A sudden rise in 403 responses generally points to a signature-validation problem, not a Square payment issue. Common causes include parsing and reserializing JSON before calculating the HMAC, using the wrong subscription’s signature key, converting the raw body encoding, or changing the endpoint URL behind a proxy.

Keep raw-body middleware scoped to the webhook route. Global JSON parsing that runs first can make it impossible to reconstruct the exact signed bytes. During an incident, compare the configured notification URL, signature key environment, deployment route, and proxy behavior before disabling validation. Do not bypass signature verification as a permanent “fix.”

Add richer order context without making the webhook fragile

A Payment object is about a payment, not necessarily a complete order catalog. If your message needs line items, fulfillment dates, or a delivery address, design the relationship explicitly. Depending on how the sale was created, you may have an order ID in your own system or need to use Square’s Orders APIs to retrieve order data after receiving the webhook.

Do that retrieval in a worker when possible. First persist the payment ID, event ID, and completed state. Then fetch and validate the related data, construct the message, and send. This design prevents a temporary Orders API issue from causing repeated payment webhook deliveries and makes it easier to diagnose whether a message was blocked by missing order data or by the email provider.

Use stable identifiers as joins. Email addresses can change, may be absent, and should not be your primary key for payment reconciliation. A Square payment ID plus your own order reference is a much safer operational record.

Test the entire path before enabling live sends

A webhook that returns 200 is not necessarily a correct integration. Test the whole path, including delivery behavior, content, and duplicate protection.

Start with Square Sandbox and a Volanea test-safe recipient. Make a Sandbox payment, then verify that your logs show the raw Square event being validated, the COMPLETED filter passing, the event ID being recorded, the Volanea request being accepted, and the email arriving with the expected amount and subject.

Then test negative cases:

  • Send an event with a deliberately invalid signature and confirm the handler returns 403 without email.
  • Replay the same valid event ID and confirm only one email job is created.
  • Use a completed payment fixture with no receipt_email and confirm no customer email is attempted.
  • Send a non-COMPLETED payment update and confirm it is acknowledged but not mailed.
  • Temporarily force a Volanea failure and confirm the event is retried or queued without duplicate final sends.
  • Test currencies used by your business so integer minor units render correctly.

For production monitoring, alert on signature failures, webhook delivery failures, missing-recipient rates, queue age, Volanea non-2xx responses, and divergence between completed Square payments and completed email jobs. A dashboard that reports “webhook received” alone does not tell you whether the customer received the intended message.

Alternatives when you do not want to run middleware

If you do not want to maintain an endpoint, an automation intermediary can receive Square triggers and make an authenticated HTTP request to an email API. Square has integrations with workflow tools such as Zapier and Make, subject to the triggers, actions, plans, and fields those services expose in your account.

That route is useful for a low-volume prototype, but evaluate it as a production system rather than as a shortcut. Confirm that it can filter for COMPLETED, preserve a stable event identifier for deduplication, keep the Volanea key in a private connection or credential store, and surface delivery failures. Also check whether the tool receives the same data available in Square’s native webhook versus a simplified trigger payload.

A direct webhook receiver remains the most controllable option for payment-triggered transactional email. You own the signature validation, exact payload interpretation, retry policy, observability, and compliance logic. Middleware is also the clearer path when emails need information from your database or require a fulfillment check before sending.

Operational checklist for a dependable launch

Before turning on the workflow for live customers, confirm the following:

  • Your Volanea from domain is authenticated and the sender identity is intentional.
  • The message type is classified correctly as transactional or marketing for your jurisdiction and consent model.
  • Square’s webhook subscription points to the production HTTPS URL and uses the matching production signature key.
  • Raw-body signature verification occurs before JSON parsing.
  • The payment.updated handler sends only for payment.status === "COMPLETED".
  • A durable idempotency record exists for Square event_id values.
  • Missing optional fields have safe fallbacks and generate actionable logs.
  • Volanea failures, Square retry behavior, and uncertain outcomes are monitored.
  • Support staff can search a payment ID or event ID and see whether a message was queued, sent, skipped, or failed.

The result is more than a connection between two services. It is a controlled transaction-communications pipeline: Square states that a payment changed, your service decides whether it warrants an email, and Volanea sends from an authenticated domain with the delivery infrastructure appropriate to that message.

FAQ

Does Square have a native Volanea integration or marketplace app?

No. This implementation uses Square Webhooks plus a middleware endpoint that calls Volanea’s REST API. It is not an installation from the Square marketplace.

Which Square webhook should I use to send an order-confirmation email?

Use payment.updated and check that data.object.payment.status is COMPLETED. A payment can be created before it reaches its final successful state, so payment.created alone is usually not sufficient for a paid-order message.

Can I put the Volanea API key in Square or front-end JavaScript?

No. Store it only in server-side secret configuration used by your middleware or a trusted automation credential store. A client-visible key can be copied and abused to send email from your account.

Why did a completed Square payment not produce an email?

First check whether receipt_email was present, whether the event had status COMPLETED, whether the event was previously deduplicated, and whether Volanea accepted the request. Optional payment fields can be absent depending on the payment flow and account configuration.

How do I stop Square retries from sending duplicate emails?

Persist Square’s event_id in durable storage with a unique constraint and make processing idempotent. Return success for events already completed, and use a queue or stateful job record for uncertain downstream outcomes.