FreshBooks can trigger useful customer communication, but to send email from FreshBooks through a dedicated email API, you need a server-side bridge rather than a native marketplace plug-in. This guide shows how to connect FreshBooks invoice events to Volanea without exposing your sending credentials or treating an accounting webhook as a finished email.
FreshBooks does not provide a native Volanea app, marketplace listing, or one-click connection. The practical integration is: FreshBooks emits an event, your HTTPS endpoint receives and verifies the event, your service retrieves the authoritative invoice data when necessary, and the service sends a transactional message through Volanea’s REST API.
That architecture may sound more involved than adding an app, but it gives you controls that are essential for billing email: idempotency, event filtering, template versioning, a clear audit trail, and protection for your API keys. It also means you can use FreshBooks as the system of record for invoices while keeping email delivery, authentication, suppression handling, and observability in your sending infrastructure.
What this integration does
The example in this article uses the FreshBooks invoice.create event as the concrete starting point. When FreshBooks creates an invoice, it delivers a webhook notification to an HTTPS endpoint that you operate. Your endpoint then uses the event’s invoice identifier to obtain the invoice record and sends an email through Volanea.
This is appropriate when an invoice needs an accompanying message that is separate from, or more customized than, FreshBooks’ own invoice email. Common examples include:
- A branded “your invoice is ready” message from your product domain.
- A notice sent to an internal finance or account-management mailbox when a high-value invoice is created.
- A message to a customer contact selected by your application rather than the default FreshBooks client contact.
- A lifecycle email that includes invoice metadata, onboarding instructions, or a payment portal link generated by another system.
- A post-processing flow in which your application decides whether an invoice should result in an email at all.
Do not use this flow as a reason to email every invoice event blindly. A billing system can create drafts, update records, retry deliveries, and create invoices through more than one process. Your integration should make an explicit business decision: which event, invoice status, customer segment, and recipient are eligible for a send.
The architecture: FreshBooks event, middleware, Volanea send
A secure production path has four distinct parts:
- FreshBooks Events API: a webhook subscription is configured for the
invoice.createevent and points to your publicly reachable HTTPS URL. - Your webhook receiver: it accepts the event quickly, validates it according to your FreshBooks integration’s security model, and records a durable idempotency key.
- Your FreshBooks API client: it uses an OAuth access token to retrieve invoice and client details from the Accounting API when the notification does not contain all of the data needed to compose an email.
- Volanea: your worker calls the REST email endpoint with a verified sender, recipient, subject, HTML/text content, and a stable message identifier.
The key design point is that a webhook notification is an event signal, not necessarily a complete invoice document. Treat the notification as permission to look up the latest source-of-truth record. That avoids building an email from partial data and makes the integration resilient if FreshBooks changes which expanded fields are available in an event payload.
The send runs from your server, serverless function, queue consumer, or automation middleware—not from a browser. That distinction is non-negotiable because a Volanea API key authorizes email sending.
FreshBooks trigger: the invoice.create webhook event
FreshBooks’ Events API supports webhook subscriptions for account events. For this integration, subscribe to the invoice.create event for the relevant FreshBooks account. The target_url must be an HTTPS endpoint that you control.
A webhook subscription is created with your FreshBooks OAuth-authorized application, not from a Volanea installation screen. The FreshBooks access token authorizes access to the accounting account; it is separate from the Volanea key that authorizes sending email.
Conceptually, the subscription request looks like this:
curl --request POST \
--url "https://api.freshbooks.com/events/account/${FRESHBOOKS_ACCOUNT_ID}/webhooks" \
--header "Authorization: Bearer ${FRESHBOOKS_ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--data '{
"webhook": {
"event": "invoice.create",
"target_url": "https://billing.example.com/webhooks/freshbooks"
}
}'
Use the event name exactly as documented by FreshBooks. Do not substitute a label from the FreshBooks web application, such as “new invoice,” for the API event name in code. The UI and developer API are related, but their terminology and identifiers are not interchangeable.
What FreshBooks sends to your endpoint
FreshBooks webhook delivery is intentionally event-oriented. The payload identifies the account, the event, and the object affected; it should not be assumed to be a fully expanded invoice suitable for rendering into an email. A representative invoice-created notification has this shape:
{
"account_id": "A1B2C3",
"event": "invoice.create",
"object_id": 987654
}
The important mapping is straightforward:
| FreshBooks webhook field | Meaning | Integration use |
|---|---|---|
account_id | FreshBooks account that produced the event | Verify that the event belongs to the expected tenant/account. |
event | Event type, such as invoice.create | Reject any event your handler is not designed to process. |
object_id | ID of the affected invoice | Fetch the invoice from the FreshBooks Accounting API. |
A webhook can be delivered more than once. It can also arrive after another worker has already processed the same invoice. Therefore, account_id + event + object_id is a sensible starting point for an idempotency key, though you should add a version or event-delivery identifier if your implementation receives one and needs to distinguish legitimate later changes.
Fetch the invoice before composing the email
After receiving object_id, retrieve the invoice via the FreshBooks Accounting API. The invoice response is the data you use for a subject line, invoice number, amounts, dates, client lookup, and any link your business provides.
Keep in mind that an invoice’s recipient email may not be present in every invoice representation or may not be the recipient your business wants. If needed, retrieve the associated client using the invoice’s client/customer identifier. Do not infer an address from a display name, and do not silently send to a stale address merely because it is present in an old cache.
Build a webhook receiver that responds quickly
Webhook providers expect an acknowledgement promptly. Your endpoint should validate the request, record or enqueue the work, and return a successful response. It should not wait for template rendering, multiple FreshBooks API calls, PDF generation, or downstream email delivery if you can avoid it.
A durable queue is ideal. The webhook handler inserts a job keyed by the event identity, and a worker processes that job. If you are starting smaller, a database row with a unique constraint can provide the same basic duplicate protection.
Here is a Node.js example showing the full field mapping from the FreshBooks event to an invoice lookup and then to a Volanea REST send. It uses environment variables for all credentials and assumes your runtime provides fetch.
import express from "express";
const app = express();
app.use(express.json({ type: "application/json" }));
const {
FRESHBOOKS_ACCOUNT_ID,
FRESHBOOKS_ACCESS_TOKEN,
VOLANEA_API_KEY,
VOLANEA_FROM,
APP_BASE_URL
} = process.env;
// Replace these examples with durable storage in production.
const processedEvents = new Set();
app.post("/webhooks/freshbooks", async (req, res) => {
const { account_id, event, object_id } = req.body;
// Field mapping from the FreshBooks event envelope.
if (account_id !== FRESHBOOKS_ACCOUNT_ID) {
return res.status(403).json({ error: "Unexpected FreshBooks account" });
}
if (event !== "invoice.create" || !object_id) {
return res.status(204).end(); // Ignore events this endpoint does not handle.
}
const eventKey = `${account_id}:${event}:${object_id}`;
if (processedEvents.has(eventKey)) {
return res.status(200).json({ duplicate: true });
}
processedEvents.add(eventKey);
try {
// FreshBooks object_id -> invoice lookup.
const invoiceResponse = await fetch(
`https://api.freshbooks.com/accounting/account/${account_id}/invoices/invoices/${object_id}`,
{
headers: {
Authorization: `Bearer ${FRESHBOOKS_ACCESS_TOKEN}`,
Accept: "application/json"
}
}
);
if (!invoiceResponse.ok) {
throw new Error(`FreshBooks invoice lookup failed: ${invoiceResponse.status}`);
}
const invoiceJson = await invoiceResponse.json();
const invoice = invoiceJson.response.result.invoice;
// Use the invoice's client/customer ID to retrieve a current recipient.
const clientId = invoice.customerid;
const clientResponse = await fetch(
`https://api.freshbooks.com/accounting/account/${account_id}/users/clients/${clientId}`,
{
headers: {
Authorization: `Bearer ${FRESHBOOKS_ACCESS_TOKEN}`,
Accept: "application/json"
}
}
);
if (!clientResponse.ok) {
throw new Error(`FreshBooks client lookup failed: ${clientResponse.status}`);
}
const clientJson = await clientResponse.json();
const client = clientJson.response.result.client;
const recipient = client.email;
if (!recipient) {
throw new Error(`FreshBooks client ${clientId} has no email address`);
}
// FreshBooks fields -> Volanea email fields.
const invoiceNumber = invoice.number || String(invoice.invoiceid);
const amount = invoice.amount?.amount ?? invoice.amount;
const currency = invoice.amount?.code ?? "";
const subject = `Invoice ${invoiceNumber} is ready`;
const invoiceUrl = `${APP_BASE_URL}/invoices/${invoice.invoiceid}`;
const volaneaResponse = await fetch("https://api.volanea.com/v1/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${VOLANEA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `freshbooks-invoice-${account_id}-${invoice.invoiceid}`
},
body: JSON.stringify({
from: VOLANEA_FROM,
to: [recipient],
subject,
text: `Invoice ${invoiceNumber} is ready. Total: ${amount} ${currency}. View it at ${invoiceUrl}`,
html: `<p>Hello ${escapeHtml(client.fname || "there")},</p>
<p>Invoice <strong>${escapeHtml(invoiceNumber)}</strong> is ready.</p>
<p>Total: <strong>${escapeHtml(String(amount))} ${escapeHtml(currency)}</strong></p>
<p><a href="${escapeHtml(invoiceUrl)}">View your invoice</a></p>`,
tags: [
{ name: "source", value: "freshbooks" },
{ name: "event", value: "invoice.create" },
{ name: "invoice_id", value: String(invoice.invoiceid) }
]
})
});
if (!volaneaResponse.ok) {
const detail = await volaneaResponse.text();
throw new Error(`Volanea send failed: ${volaneaResponse.status} ${detail}`);
}
return res.status(202).json({ accepted: true, invoice_id: invoice.invoiceid });
} catch (error) {
processedEvents.delete(eventKey); // Let a durable queue/retry policy handle this in production.
console.error(error);
return res.status(500).json({ error: "Email processing failed" });
}
});
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
app.listen(3000);
The exact request fields supported by your Volanea account, including attachments, reply-to addresses, scheduling, and tags, are documented in the email API reference and setup guides. Keep the integration’s mail payload deliberately small at first: verified from, recipient, subject, HTML, text, and an idempotency key. Add complexity only after the basic event path is observable and reliable.
Map FreshBooks data deliberately
The technical mapping is simple; the business mapping is where most invoice-email integrations go wrong. Make each email field originate from an identified, reviewed source.
A practical mapping policy looks like this:
| Volanea field | Recommended source | Notes |
|---|---|---|
from | Server environment variable | Use a domain and sender identity verified in Volanea. Never take this from FreshBooks client data. |
to | Current FreshBooks client email, or your CRM’s approved billing contact | Validate recipient selection rules before production. |
subject | Invoice number plus a stable phrase | Avoid placing sensitive financial details in the subject. |
html and text | Versioned server-side template | Always send text alongside HTML. |
| Invoice amount | FreshBooks invoice object | Format currency and locale server-side; do not concatenate raw values carelessly. |
| Invoice URL | Your authenticated portal or FreshBooks-approved link strategy | Do not construct a public link to sensitive invoice data. |
| Idempotency key | Account ID plus invoice ID | Prevents a retry from becoming a second customer email. |
Avoid including full line-item descriptions in the message unless the recipient expects them and you have reviewed the privacy implications. Invoice line items can contain personal data, project names, health information, or internal references. A short notification with an authenticated “view invoice” link is often safer.
Also decide what to do when FreshBooks has a client record but no usable email. The safest default is to stop the send, log a structured error, and alert the team or create a task. Do not substitute a guessed address, and do not fall back to an unrelated user email without explicit business rules.
Where the Volanea API key belongs
The Volanea API key belongs only in server-side secret storage. In the sample above, it is read from process.env.VOLANEA_API_KEY, which should be populated by your hosting provider’s encrypted environment-variable or secrets facility.
It must not be stored in:
- JavaScript shipped to a browser.
- A FreshBooks invoice custom field, note, or client record.
- A public repository, template file, or committed
.envfile. - A front-end automation configuration that a user can inspect.
- A query string, webhook URL, or application log.
FreshBooks itself does not need the Volanea key. FreshBooks only needs the URL of your webhook receiver. Your receiver holds the Volanea secret and makes the outbound API call after it has decided the event is valid.
This separation limits blast radius. If someone learns your webhook URL, they still cannot send mail from your Volanea account. If a FreshBooks OAuth token is revoked, it does not automatically expose your email provider credentials. And if you rotate a Volanea key, you update one secret in your middleware rather than editing every accounting workflow.
Use separate keys for development, staging, and production. Restrict access to the production secret, rotate it when a team member with access leaves, and ensure logs redact Authorization headers. If your platform supports it, use a secret manager with audit logs rather than a shared password vault entry pasted into deployment settings.
Template, sender, and deliverability decisions
An invoice event is transactional in nature, but the message still has to meet normal deliverability expectations. Use a sender domain you control and authenticate it in Volanea with the DNS records specified during domain setup. Sending invoice notices from a verified, aligned domain is materially better than using a random or mismatched address.
Keep the message recognizably transactional
The message should explain why the recipient is receiving it, identify your company, and provide a direct path to support. A good invoice-ready email normally includes:
- Your recognizable business name in the From name.
- A subject such as “Invoice INV-1042 is ready” rather than an ambiguous “Action required.”
- The invoice number and a concise description of what happened.
- A secure link to review or pay, where applicable.
- A support reply path or contact URL.
- A plain-text alternative for recipients and clients that do not display HTML.
Do not quietly turn invoice events into promotional campaigns. If you want to add optional product announcements or marketing offers, keep those in a separate consent-aware campaign flow. This protects customer expectations and keeps billing communication easier to audit.
Version templates and preserve context
Store templates in source control or a transactional-template system, and record the template version with every send. When a customer asks what they received, “an invoice email was sent” is not enough. You want to know the invoice ID, recipient, message provider ID, template version, send time, and final delivery event.
Use your own internal correlation ID as well as the Volanea response identifier. The FreshBooks invoice ID is excellent for joining billing records to email events, but it is not a substitute for a provider-level message ID when investigating a bounce, delivery delay, or complaint.
When this breaks: FreshBooks-to-email failure modes
Every hop in this integration can fail differently. Designing for those failures before the first customer incident is much easier than trying to reconstruct an invoice-send history later.
FreshBooks retries can cause duplicate sends
Webhook systems retry when they do not receive a timely successful response or when there is a transient delivery failure. A timeout after your worker has already sent the email is especially dangerous: FreshBooks may retry, and your code may send the same message again.
Use two layers of protection:
- Persist an event or invoice key with a database uniqueness constraint before or at send time.
- Pass a stable Volanea idempotency key based on the FreshBooks account and invoice identifier.
Do not rely on an in-memory Set like the illustrative code for production. It disappears on deploys, does not work across multiple instances, and does not survive a crash. A database table, Redis with appropriately durable semantics, or a queue with deduplication is the correct place for this state.
Webhook timeouts and slow downstream work
A webhook receiver that calls FreshBooks, renders a document, sends an email, and waits for every response can exceed the sender’s delivery timeout. The sender then retries even though some work may have completed.
Return quickly after durable acceptance. A robust sequence is: validate the event, write a unique job row, return a 2xx response, then let a worker perform the invoice lookup and Volanea call. If the worker fails, retry the job with bounded exponential backoff and a dead-letter or manual-review path.
Payload fields may be missing or insufficient
An event notification may not include recipient data, amount formatting, client information, or the business fields you need. Availability can also vary by API resource, account configuration, permissions, and FreshBooks product changes.
Build the send from the invoice lookup, and conditionally fetch the client record. Treat absent fields as a business exception, not as an invitation to emit malformed mail. For example, if the invoice has no client ID, client lookup fails, or the client email is blank, mark the job as needs_attention and notify your team.
OAuth expiration and authorization changes
FreshBooks API access is OAuth-based. Access tokens expire, refresh tokens can be invalidated, and permissions can change when an administrator disconnects an app. Your job worker needs a refresh-token strategy and a clear alert when reauthorization is required.
Never solve an authorization failure by retrying indefinitely. Classify 401/403 responses separately from temporary 429/5xx failures. A revoked authorization needs human action; a rate limit may need backoff.
Volanea rejects or suppresses a recipient
A send request can be rejected because the sender is unverified, the request is invalid, the account has a sending restriction, or the address is suppressed after a previous hard bounce or complaint. These outcomes should be recorded against the invoice-send job, but they should not change the invoice state in FreshBooks automatically.
Your accounting record and your delivery record represent different facts: an invoice may be created successfully even when an email cannot be delivered. Escalate the delivery issue through your support or accounts-receivable process rather than rewriting accounting history.
A no-code alternative: FreshBooks through Zapier or Make
If you cannot operate a webhook endpoint or OAuth application, use an automation platform as middleware. FreshBooks has integrations on Zapier and Make that can watch for FreshBooks records, while the automation platform can call a webhook endpoint you control.
The recommended secure version is still:
FreshBooks trigger in Zapier or Make
-> HTTPS request to your middleware
-> Volanea REST API call from middleware
For example, choose the FreshBooks New Invoice trigger in Zapier, map the invoice ID and account context into a POST body to your service, and let the service retrieve data and call Volanea. In Make, use the relevant FreshBooks invoice-watching module, then send the normalized data to the same service.
Do not put the Volanea API key directly in a client-visible webpage or a shared, broadly editable automation field. While automation platforms can store connection credentials, a dedicated middleware service gives you better key rotation, duplicate protection, code review, and delivery logging. It is especially valuable when the automation trigger is polling rather than a first-party webhook, because polling can observe records after updates or at times that do not match your desired send moment.
The trade-off is operational simplicity versus control. Zapier or Make can be a good prototype route for low-volume internal notifications. For customer-facing invoice notices, a direct FreshBooks webhook plus a queue-backed worker is usually easier to audit and more predictable at scale.
Test before enabling production sends
Test this integration with a non-production FreshBooks account or carefully labeled test client records. Your test plan should cover more than a successful API response.
- Create an invoice for a client with a valid test inbox and confirm exactly one email is sent.
- Deliver the same webhook payload twice and verify that only one message is accepted for sending.
- Test a client with no email and ensure the job is held rather than sent to an invented fallback.
- Simulate a FreshBooks invoice lookup failure and verify the job retries or reaches review status.
- Simulate a Volanea 4xx error and confirm your system records the response without infinite retries.
- Simulate a temporary 5xx response and confirm bounded backoff works.
- Review HTML and text versions in several email clients, including a mobile client.
- Confirm the From domain is authenticated and that replies reach a monitored mailbox.
Add structured logs around each stage: webhook received, event deduplicated, invoice retrieved, recipient selected, Volanea request accepted, and provider message ID recorded. Avoid logging raw API keys, full authorization headers, or unnecessary invoice data. For operational metrics, track event-to-send latency, duplicate-event count, failed recipient lookups, Volanea acceptance rate, bounces, and complaint rate.
Conclusion
To send email from FreshBooks with Volanea, use FreshBooks’ invoice.create event as a signal, then let secure middleware perform the actual transactional send. The middleware should fetch the authoritative invoice and client information, render a controlled template, protect the Volanea API key in server-side secrets, and use idempotency to make retries safe.
This design is more reliable than trying to treat FreshBooks as an email delivery provider or exposing a sending key in an automation step. It preserves the strengths of both systems: FreshBooks remains the accounting source of truth, while Volanea handles the email API layer and your application owns the business rules between them.
FAQ
Can I install Volanea from the FreshBooks marketplace?
No. Volanea does not ship a native FreshBooks marketplace app or plug-in. Connect FreshBooks events to Volanea through your own middleware, or use Zapier or Make as an intermediary that calls secure middleware.
Which FreshBooks event should send the email?
For an invoice-ready notification, use invoice.create. If your policy is to send only after another business condition, receive the event but evaluate invoice status or your own application state before calling Volanea.
Does the FreshBooks webhook contain the customer email address?
Do not assume it does. Treat the webhook as an event envelope containing the affected object identifier, then retrieve the invoice and, when needed, its associated client through the FreshBooks Accounting API.
Where should I store the Volanea API key?
Store it only in server-side secret storage, such as encrypted environment variables or a secrets manager. FreshBooks needs only your webhook URL; it should never contain the Volanea key.
How do I prevent a FreshBooks retry from sending two emails?
Persist a unique event or invoice key in durable storage and send a stable idempotency key with the Volanea request. A database uniqueness constraint plus a queue-backed worker is a strong production pattern.