Send email from Shippo when a label is purchased or a tracking status changes by connecting Shippo webhooks to a small server-side endpoint that calls Volanea’s transactional email API. This is a direct webhook-to-middleware-to-email architecture—not a native Shippo marketplace app or plugin.
What this Shippo and Volanea integration does
Shippo can notify an external URL when a shipping event happens. Volanea can accept a transactional email request over HTTPS. Put a lightweight endpoint between them and you can turn a newly created label or a carrier tracking update into a customer email.
The workflow is straightforward:
- A label is purchased in Shippo, creating a Transaction, or a carrier status changes for a tracked shipment.
- Shippo sends an HTTP POST webhook to an endpoint you control.
- Your endpoint validates the request, extracts the relevant shipping fields, and decides whether an email should be sent.
- The endpoint calls
POST /v1/sendon the Volanea API using a server-side API key. - Volanea accepts the message for delivery from your verified sending domain.
There is no native Volanea app to install in Shippo, and you should not attempt to place a Volanea secret key in a Shippo webhook URL, browser code, storefront code, or a public automation configuration. Shippo supplies the outbound event; your middleware owns the authentication, business rules, idempotency, and email request.
This pattern is useful for operational messages such as:
- Shipping-confirmation emails immediately after a label is purchased.
- Tracking emails when a carrier marks a parcel as in transit, out for delivery, delivered, delayed, or returned.
- Internal exception alerts when a transaction reports an error or a shipment needs attention.
- Customer-support notifications that include the tracking number, carrier tracking URL, and order reference.
A shipping platform knows that a label exists. Your commerce system usually knows which customer owns the order. The integration must deliberately connect those two facts rather than assume Shippo’s transaction object contains a recipient email address.
The concrete Shippo trigger: transaction_created
For a shipping-confirmation email, use Shippo’s transaction_created webhook event. Shippo describes a Transaction as the object created when a shipping label is purchased. When the event fires, Shippo POSTs a JSON envelope whose data property is the newly created Transaction object.
This makes transaction_created the practical trigger for “your order has shipped” messaging. It happens after a label purchase, so the payload can include a tracking number, a carrier tracking URL, a label URL, the Shippo transaction ID, an order ID when one is associated, and transaction metadata.
In the Shippo web app, the documented setup path is Setting, then API, then the Webhooks section. Choose Add webhook, set the Event Type to the transaction-created event, choose the appropriate test or production mode, and enter your public HTTPS receiver URL. Shippo also supports creating webhook subscriptions through its Webhooks API.
Why use transaction_created instead of transaction_updated?
Use transaction_created when one successful label purchase should create one shipping email. It is narrower and easier to reason about.
transaction_updated is useful when you need subsequent label-related changes, but it can produce more than one notification for the same logical shipment. If your goal is a single confirmation, starting with transaction_created reduces the number of cases that need deduplication.
When track_updated is the better event
Use track_updated for delivery-progress emails. Shippo posts a Tracking object when carrier tracking information changes. That is appropriate for messages such as “Your package is out for delivery” or “Delivered.”
Do not send a customer email on every tracking update without filtering. Carriers can report multiple intermediate scans, repeat statuses, or updates that do not matter to a buyer. Build a policy such as sending only for TRANSIT, OUT_FOR_DELIVERY, DELIVERED, FAILURE, or RETURNED, depending on the normalized status values your Shippo payload provides.
For the first version of this guide, the send begins with transaction_created, because it is a clear label-purchased event and its payload includes the shipping details needed for a confirmation.
Understand the Shippo webhook payload before mapping fields
A Shippo transaction-created webhook is an envelope with an event, a test indicator, and a data object. The data object is the Transaction that Shippo created.
A representative payload from Shippo’s documentation has this shape:
{
"event": "transaction_created",
"test": true,
"data": {
"object_state": "VALID",
"status": "SUCCESS",
"object_created": "2024-06-17T14:31:40.000Z",
"object_updated": "2024-06-17T14:31:40.000Z",
"object_id": "70ae8117ee1749e393f249d5b77c45e0",
"test": true,
"rate": "fe9a78973c5b409498af1fd9baefbe34",
"tracking_number": "ZW70QJC",
"tracking_status": "UNKNOWN",
"eta": "2024-06-22T14:31:40.347Z",
"tracking_url_provider": "https://tools.usps.com/go/TrackConfirmAction_input?origTrackNum=ZW70QJC",
"label_url": "https://shippo-delivery.s3.amazonaws.com/...",
"commercial_invoice_url": "",
"messages": [],
"order": "7535a41cac64426fa87ebfa24ad11123",
"metadata": "customer_ID:12, order_ID:558a",
"parcel": "faf460225c4d49699e18384dc32a1cc6",
"billing": {
"payments": []
}
}
}
The most useful fields for a shipping email are generally:
| Shippo field | Typical use in the email flow |
|---|---|
event | Confirm this is transaction_created. |
data.status | Send only when the transaction is successful. |
data.object_id | A durable unique ID for deduplicating the shipment email. |
data.tracking_number | Show the customer their tracking number. |
data.tracking_url_provider | Use as the primary tracking button destination. |
data.eta | Optional estimated-delivery text when present. |
data.order | Match the Shippo transaction to an order in your system. |
data.metadata | Carry your own customer or order references into the webhook. |
data.messages | Log transaction errors or warnings; do not turn these directly into customer-facing copy. |
test or data.test | Prevent test events from reaching real customers. |
The important missing field: customer email
The Shippo Transaction payload shown above does not provide a customer email field. That is not a problem to patch with guesswork. It is a design decision your integration must solve safely.
The reliable options are:
- Look up the email in your order database using
data.order, your own order reference in metadata, or another server-side mapping. - Pass a non-sensitive reference in Shippo transaction metadata when purchasing the label, then use that reference to retrieve the customer record from your database.
- Use an opaque internal order ID in metadata, not a raw email address, so webhook payloads and diagnostic logs do not unnecessarily replicate customer contact data.
Shippo supports metadata on transactions and includes that metadata in the resulting transaction webhook payload. A value such as order_id=ord_12345 is better than embedding a recipient email, full address, or other customer data in a metadata string.
Build the secure webhook receiver
Your receiver is the integration boundary. It receives a request from Shippo, validates it, determines whether the payload represents a sendable event, resolves the recipient from your system of record, and makes the Volanea API call.
The example below uses Node.js with Express. It assumes that your order service can resolve a recipient using an internal order reference. Replace lookupOrderByReference and alreadyProcessed with real database code.
import express from "express";
import crypto from "node:crypto";
const app = express();
app.use(express.json({ limit: "256kb" }));
const {
VOLANEA_API_KEY,
SHIPPO_WEBHOOK_TOKEN,
SHIPPING_FROM_EMAIL = "shipping@example.com",
SHIPPING_FROM_NAME = "Example Store"
} = process.env;
if (!VOLANEA_API_KEY || !SHIPPO_WEBHOOK_TOKEN) {
throw new Error("Missing required server-side secrets");
}
// Replace with a database query keyed by your own order reference.
async function lookupOrderByReference(reference) {
// Example record returned by your commerce database.
return {
id: reference,
customerEmail: "buyer@example.net",
customerName: "Alex Customer",
orderNumber: "#10482"
};
}
// Replace with a unique database constraint or durable idempotency table.
async function alreadyProcessed(eventKey) {
return false;
}
async function markProcessed(eventKey, details) {
console.log("Persist event key before or alongside queueing", eventKey, details);
}
function escapeHtml(value = "") {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function readOrderReference(metadata) {
// Prefer JSON metadata if your label-creation code standardizes it.
// This example supports Shippo's documented string metadata style:
// "customer_ID:12, order_ID:558a"
const match = String(metadata || "").match(/order_ID\s*:\s*([^,]+)/i);
return match?.[1]?.trim() || null;
}
app.post("/webhooks/shippo", async (req, res) => {
// Shippo can send a self-generated token as a query parameter.
// Reject requests that do not contain the secret stored on this server.
if (req.query.token !== SHIPPO_WEBHOOK_TOKEN) {
return res.status(401).json({ error: "Unauthorized webhook" });
}
const payload = req.body;
const transaction = payload?.data;
// Acknowledge events that this route intentionally does not process.
if (payload?.event !== "transaction_created") {
return res.status(204).end();
}
// Keep Shippo test traffic separate from production customer mail.
if (payload.test === true || transaction?.test === true) {
return res.status(204).end();
}
// A label object can exist without representing a usable successful purchase.
if (transaction?.status !== "SUCCESS" || !transaction?.object_id) {
return res.status(204).end();
}
const eventKey = `shippo:transaction_created:${transaction.object_id}`;
if (await alreadyProcessed(eventKey)) {
return res.status(200).json({ duplicate: true });
}
const orderReference = transaction.order || readOrderReference(transaction.metadata);
if (!orderReference) {
console.error("No internal order reference in Shippo transaction", {
transactionId: transaction.object_id
});
return res.status(202).json({ skipped: "missing_order_reference" });
}
const order = await lookupOrderByReference(orderReference);
if (!order?.customerEmail) {
console.error("No recipient found for Shippo transaction", {
transactionId: transaction.object_id,
orderReference
});
return res.status(202).json({ skipped: "missing_recipient" });
}
const trackingNumber = transaction.tracking_number || "Not yet available";
const trackingUrl = transaction.tracking_url_provider || "";
const eta = transaction.eta
? new Intl.DateTimeFormat("en-US", {
dateStyle: "long",
timeZone: "UTC"
}).format(new Date(transaction.eta))
: null;
const safeName = escapeHtml(order.customerName || "there");
const safeOrderNumber = escapeHtml(order.orderNumber || orderReference);
const safeTrackingNumber = escapeHtml(trackingNumber);
const safeTrackingUrl = escapeHtml(trackingUrl);
const html = `
<h1>Your order has shipped</h1>
<p>Hi ${safeName},</p>
<p>Your order ${safeOrderNumber} is on its way.</p>
<p><strong>Tracking number:</strong> ${safeTrackingNumber}</p>
${eta ? `<p><strong>Estimated delivery:</strong> ${escapeHtml(eta)}</p>` : ""}
${trackingUrl ? `<p><a href="${safeTrackingUrl}">Track your package</a></p>` : ""}
<p>If you have questions, reply to this email and include your order number.</p>
`;
const text = [
"Your order has shipped",
`Hi ${order.customerName || "there"},`,
`Your order ${order.orderNumber || orderReference} is on its way.`,
`Tracking number: ${trackingNumber}`,
eta ? `Estimated delivery: ${eta}` : "",
trackingUrl ? `Track your package: ${trackingUrl}` : ""
].filter(Boolean).join("\n\n");
// This is the Volanea REST API call. Reuse this idempotency key only
// when retrying this exact Shippo transaction-created event.
const response = await fetch("https://api.volanea.com/v1/send", {
method: "POST",
headers: {
"Authorization": `Bearer ${VOLANEA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": eventKey
},
body: JSON.stringify({
from: `${SHIPPING_FROM_NAME} <${SHIPPING_FROM_EMAIL}>`,
to: [order.customerEmail],
subject: `Your order ${order.orderNumber || orderReference} has shipped`,
html,
text
})
});
const result = await response.json().catch(() => ({}));
if (!response.ok) {
console.error("Volanea send failed", {
status: response.status,
transactionId: transaction.object_id,
result
});
// A 5xx response tells Shippo the processing was not completed.
// Retry behavior must still be safe because the Volanea key is stable.
return res.status(502).json({ error: "Email provider request failed" });
}
await markProcessed(eventKey, {
transactionId: transaction.object_id,
recipient: order.customerEmail,
volaneaResult: result
});
return res.status(200).json({ sent: true });
});
app.listen(3000, () => {
console.log("Shippo webhook receiver listening on port 3000");
});
This code deliberately maps each field rather than copying the entire Shippo payload into an email. It uses data.object_id as the idempotency identity, reads tracking_number, tracking_url_provider, and eta from the transaction, and resolves customer email independently.
Configure the label flow so the webhook can find the right customer
A webhook receiver needs a stable correlation key. The best time to create it is when your application purchases the Shippo label.
When you create a Shippo transaction, include your internal order reference in the transaction’s metadata field. Shippo’s documentation shows metadata as a string, and that string is included in the transaction_created payload.
For example, a label-purchase request can use metadata like this:
{
"rate": "a5c9d1bec93149f5bedc3a9374b50970",
"async": false,
"label_file_type": "PDF",
"metadata": "order_ID:ord_10482"
}
Your webhook code can then use either data.order or the order_ID value in data.metadata to look up the order. If your Shippo integration always has access to the Shippo Order object, data.order may be sufficient. Metadata is especially valuable when your own platform has a separate internal order identifier that must survive the shipping handoff.
Use opaque references, not personal data
Do not put a customer email, customer name, street address, or authentication token into Shippo metadata just because it is convenient. Webhook payloads can appear in logs, support diagnostics, observability platforms, and webhook test tools.
An opaque reference such as order_ID:ord_10482 lets the receiver retrieve the current customer email from the source-of-truth database. It also means correcting a customer email before the webhook arrives does not require editing data already attached to Shippo.
Decide what “shipped” means for your business
A successful label purchase does not always mean a parcel has physically left the warehouse. Some businesses send “Your shipping label is ready” internally at this point and wait for a carrier acceptance scan before customer notification.
If your promise is “shipped” only after handoff to the carrier, build the customer email from a filtered track_updated event instead. The middleware approach stays the same; only the Shippo trigger, payload mapping, and send policy change.
Keep the Volanea API key out of Shippo
The Volanea API key lives in the server-side environment of your webhook receiver—not on the Shippo side.
That distinction matters. Shippo’s webhook configuration holds a destination URL. It is not a secure secret-management system for a downstream email-provider credential, and a webhook is an inbound notification mechanism rather than a place to execute authenticated API calls on your behalf.
Store the key as an environment variable or deployment-platform secret, such as:
VOLANEA_API_KEY=sk_live_replace_with_your_secret
SHIPPO_WEBHOOK_TOKEN=long-random-webhook-secret
SHIPPING_FROM_EMAIL=shipping@example.com
SHIPPING_FROM_NAME="Example Store"
Your host should inject these values into the server process. They should not be committed to Git, embedded in a static site, sent to a browser, passed in a public client-side environment variable, or exposed in a Shippo URL.
Before sending production email, verify the domain used by SHIPPING_FROM_EMAIL in Volanea. The sender should belong to a domain you control and have authenticated for email sending. The full setup, send schema, templates, and endpoint reference are available in the Volanea email API documentation.
Secure the Shippo-to-receiver hop
Shippo documents three ways to help protect webhook integrity: an inbound IP allowlist, a self-generated token appended to the webhook URL, and HMAC authentication arranged through Shippo. Use more than one control when practical.
A sensible baseline is:
- Serve the endpoint only over HTTPS.
- Add a long random query token to the Shippo webhook URL, such as
https://hooks.example.com/webhooks/shippo?token=.... - Compare that token server-side using a secret environment variable.
- Restrict inbound traffic using Shippo’s published webhook IP addresses if your hosting environment supports source-IP allowlisting.
- Use HMAC verification when it is available for your Shippo account and implementation.
- Avoid logging full webhook bodies in production, especially if your own metadata could identify customers.
Treat the query token as a shared secret. It is not a replacement for correct processing or idempotency, but it prevents arbitrary internet traffic from triggering the endpoint.
Make duplicate sends impossible in practice
Webhook delivery is at-least-once delivery from your application’s perspective. A network timeout can leave Shippo unsure whether your server completed processing. Your server can also time out after Volanea accepted an email but before your code records success. Both cases can lead to the same Shippo event being sent again.
The answer is not to hope retries never happen. The answer is to make repeated processing of the same logical event safe.
Use the immutable Shippo transaction identifier to build an idempotency key:
shippo:transaction_created:<data.object_id>
Pass it in Volanea’s Idempotency-Key header on POST /v1/send. Persist the same key in your own database with a unique constraint or in a durable event ledger. If your application retries the exact same transaction-created event, it must reuse the exact same key. A new label transaction is a different event and should receive a different key.
The order of operations matters
There are two broad approaches:
- Synchronous send: validate, call Volanea, save the completed event, then return a 2xx response to Shippo.
- Durable queue: validate, persist the event and unique key, enqueue a job, return a quick 2xx response, then let a worker send through Volanea.
The synchronous model is easier for a low-volume implementation. The durable queue is safer for production systems because the Shippo endpoint remains fast even if the email provider is slow, and a worker can retry controlled failures without asking Shippo to replay the webhook.
In either model, use both a durable uniqueness check and the Volanea idempotency key. The database protects your business workflow; the API header protects the send request at the email boundary.
Test without emailing real customers
Shippo lets you create webhooks in test mode and provides a way to send a sample payload from the webhook configuration area. Use those capabilities before enabling a production endpoint.
A practical test sequence is:
- Deploy the receiver on a public HTTPS URL.
- Create a Shippo webhook in test mode for
transaction_created. - Configure a test-only order lookup that returns an address you control.
- Use Shippo’s sample webhook feature and inspect server logs for the event name, transaction ID, metadata reference, and send result.
- Confirm that the Volanea message is accepted and that the recipient sees the correct tracking number and tracking link.
- Send the exact sample event again to verify duplicate protection.
- Test missing
tracking_url_provider, blank metadata, an unknown order reference, and a non-success transaction status. - Repeat the test in production mode only after your sending domain and production webhook receiver are ready.
Never point production Shippo webhooks at generic public request-inspection services with real shipping data. Shippo specifically cautions that those services share information with third parties; use only test data there, or inspect requests on infrastructure you control.
Test the actual customer experience
A technically accepted API request is not the end of the test. Review the email on mobile and desktop, inspect the tracking link, and make sure the subject does not expose sensitive information. Check the plain-text fallback too: it matters for clients that block HTML and for accessibility tooling.
Also test a tracking number with characters that need HTML escaping, an order number containing punctuation, and a customer name containing an apostrophe or angle bracket. The receiver should escape dynamic values before inserting them into HTML.
When this breaks
The integration has two hops: Shippo to your receiver, then your receiver to Volanea. Troubleshoot them separately. A message can fail before your code runs, while your code runs, or after the email API accepts the request.
Shippo retries create duplicate-send risk
If Shippo does not receive a timely successful response, it may retry delivery. Your endpoint may also process the event successfully but fail before returning its response. This is why an Idempotency-Key based on data.object_id is required rather than optional.
Do not generate a random UUID for every retry. That converts retries into new email sends. Use the same deterministic key for the same transaction_created event, and persist it.
Your webhook endpoint times out
A slow database lookup, template rendering operation, external order API, or Volanea call can make the webhook endpoint slow enough to trigger retries. Return quickly whenever possible.
For production volume, persist the validated event and enqueue a job before returning a 2xx response. The job can handle retries with exponential backoff and keep using the same Volanea idempotency key. This separates Shippo’s delivery window from your email provider’s response time.
Fields are absent, delayed, or not meaningful for your use case
Do not assume every transaction carries every optional field. eta, tracking_url_provider, order, and custom metadata can be absent depending on how the label was created and what data was supplied. Carrier behavior also affects tracking information.
Handle optional fields explicitly:
- If
tracking_numberis missing, skip the customer email or send a carefully worded alternative only if your business process supports it. - If
tracking_url_provideris missing, omit the tracking button rather than emitting a broken link. - If
etais missing, omit estimated-delivery copy. - If the order reference is missing, return a successful acknowledgment only after recording an actionable internal exception; do not guess the recipient.
- If a payload is marked as test data, do not send it to a real customer.
This is not a plan-tier issue you can safely solve by assuming fields will appear later. Build the receiver around the actual payload your Shippo account sends, and inspect test and production examples before making an email field mandatory.
The webhook was forged or misrouted
A publicly reachable endpoint will receive internet traffic that is not from Shippo. Require the URL token at minimum, and consider Shippo’s IP allowlist and HMAC option. Reject missing or invalid authentication before parsing the event into business logic.
A 401 response for an invalid token should not expose details about your database, email configuration, or expected webhook format.
Volanea rejects the email request
A failed Volanea request is commonly a configuration or payload issue: an invalid API key, an unverified sender domain, malformed recipient address, invalid JSON field, or an account-level sending limitation. Log the HTTP status and a sanitized error response, but never log the API key.
Use the Volanea response to distinguish a retryable provider or network failure from a permanent data problem. For example, an invalid recipient should go to an exception workflow, while a temporary network error can be retried with the same idempotency key.
Tracking status creates too much email
For track_updated, carrier systems may publish several updates before delivery. Shippo also notes that tracking updates vary by carrier and may arrive on different schedules. Sending an email for every update creates noise and customer confusion.
Persist the last notified status per tracking number or Shippo tracking object. Define a small status-transition policy, such as notifying for out-for-delivery and delivered but not for every transit scan. That policy belongs in your middleware, where it can evolve without changing the Shippo webhook configuration.
Extend the integration beyond shipping confirmation
Once the transaction-created path works, you can add events cautiously.
Delivery updates from track_updated
A dedicated tracking receiver can map a tracking object into a delivery email. Use an idempotency key that includes both a stable tracking identifier and a status-transition identifier, not merely the tracking number. A parcel can legitimately progress from transit to out for delivery to delivered, and those are separate messages.
Internal exception alerts
The messages field on a transaction can be useful for operational routing. Send an internal alert to a support or fulfillment address when a label purchase is not successful, but avoid copying raw carrier or API errors into a customer email.
Stored templates and consistent branding
Inline HTML is useful for showing the field mapping in an integration example. For a mature setup, centralize layout, typography, footers, and reusable content in your email workflow. That makes it easier to update branding without redeploying shipping code and reduces the risk that transactional messages drift apart.
Email verification before fulfillment communication
Shipping communication is high value, but it still depends on a usable address. If your checkout or order-import flow allows unverified addresses, validate them before putting a shipment into an email workflow. You can use the email address verification tool as a quick check during operational investigation, while your production system should keep a clear source-of-truth for address status and customer consent.
A production readiness checklist
Before enabling real shipping emails, verify each item below:
- A Shippo
transaction_createdwebhook points to your HTTPS endpoint. - The endpoint requires a Shippo URL token, HMAC verification, IP allowlisting, or an appropriate combination.
- The Volanea API key exists only in server-side secret storage.
- The From address uses a verified Volanea sending domain.
- The receiver rejects or safely ignores Shippo test events in production.
- The recipient comes from your order system, not from untrusted metadata.
- Metadata carries an opaque order reference when
data.orderis not enough. data.object_idbecomes a deterministic idempotency key.- Your database or job system records processed events durably.
- The message has both HTML and text content.
- Optional tracking fields are handled without blank labels or broken links.
- The system has a defined policy for missing recipients, failed sends, and carrier-status notifications.
- Logs contain enough correlation data to debug a shipment without recording secrets or unnecessary customer data.
The best implementation is deliberately boring: one clearly scoped Shippo event, one secure receiver, one deterministic key, and one email send request. That foundation is easier to audit, easier to test, and much safer than connecting a shipping webhook to a client-visible credential.
FAQ
Does Shippo have a native Volanea integration?
No. This setup does not install a Shippo marketplace app or a native plugin. It uses Shippo’s outbound webhooks, your own middleware endpoint, and Volanea’s REST API.
What Shippo event should trigger a shipping-confirmation email?
Use transaction_created when the event should occur after a shipping label is purchased. Use track_updated only when you need carrier-status communications such as out-for-delivery or delivered notifications.
Where should the Volanea API key be stored?
Store it as a server-side secret or environment variable in the middleware runtime. Do not store it in Shippo, a webhook URL, client-side JavaScript, a mobile app, or source control.
How do I stop Shippo retries from sending duplicate emails?
Use data.object_id to create a deterministic idempotency key, persist processed events in your database or queue, and send that same key in Volanea’s Idempotency-Key header whenever retrying the same event.
Can I put the customer email in Shippo metadata?
You can technically pass metadata through the transaction webhook, but it is safer to put an opaque internal order reference there and look up the recipient on your server. That minimizes customer data in webhook payloads and logs.