PartnerStack can notify your application when a partnership event happens, and Volanea can deliver the corresponding transactional message. This guide shows how to send email from PartnerStack without pretending there is a native marketplace app: PartnerStack posts a webhook to your server, and your server securely calls Volanea’s email API.

There is no native PartnerStack app, plugin, or marketplace installation for Volanea. That is not a limitation you should try to work around by putting an email API key in a browser script, a public URL, or an automation field visible to ordinary users. The dependable architecture is a server-side webhook relay that owns the credentials, validates the incoming data, applies your messaging rules, and makes a single idempotent send request.

This walkthrough uses PartnerStack’s agreement.accepted webhook event as the concrete trigger. It fires when a partner accepts your program’s terms of service. That makes it a useful moment to send a practical next-step message: where to find their referral link, what resources are available, how payouts work in your program, or how to reach your partner team.

What this integration does

The finished flow is deliberately simple:

  1. A partner accepts your program agreement in PartnerStack.
  2. PartnerStack sends a JSON POST request for the agreement.accepted event to your webhook endpoint.
  3. Your endpoint extracts the partner’s email address, name, partner key, group, and event timestamp from the payload.
  4. Your server creates a stable idempotency key for that specific agreement event.
  5. Your server calls Volanea’s POST /v1/send endpoint with a verified sender and a personalized transactional message.
  6. Volanea queues the message through its sending pipeline while your application records enough context to investigate delivery later.

The important boundary is step 3 through step 5. PartnerStack is the event source. Your relay is the decision-maker and secret holder. Volanea is the email delivery API. Keeping those responsibilities separate prevents accidental duplicate sends, credential exposure, and brittle field mappings.

Why use a webhook relay instead of a direct connection?

PartnerStack supports outbound webhooks: when a subscribed event occurs, it can send a JSON POST request to an endpoint you control. Its webhook documentation specifically describes an endpoint that accepts HTTP POST requests and explains that webhook subscriptions are registered through its Webhooks API.

That outbound HTTP capability is enough to build a direct technical integration, but it does not mean PartnerStack should call Volanea directly. An outgoing webhook sends event data to a receiving endpoint. It is not a secure place to store or interpolate a third-party sending credential.

A relay solves several problems at once:

  • Credential isolation: the Volanea secret key stays in your server, worker, or secret manager.
  • Message policy: your code decides which PartnerStack events actually warrant an email.
  • Field normalization: optional fields, custom fields, and unexpected null values are handled before a send is attempted.
  • Duplicate protection: one PartnerStack event maps to one stable Volanea Idempotency-Key.
  • Observability: you can log the PartnerStack partner key and the Volanea response together without placing sensitive data in a third-party automation step.
  • Change control: your email copy, sender identity, suppression policy, and rollout logic live in version-controlled code.

This is also why a browser-only approach is wrong. Anyone who can inspect the client-side application could copy a Volanea API key and use it to submit mail as your project. Treat the key as a server credential, not as configuration for a public page or partner portal.

The concrete PartnerStack trigger: agreement.accepted

PartnerStack lists agreement.accepted under Agreement Events. The event represents a partner accepting terms of service for your program. Its sample webhook payload is a JSON object containing partnership information, the relevant group, team details, and fields such as email, first_name, last_name, partner_key, key, created_at, and updated_at.

For an onboarding-style email, this is more precise than sending whenever an application is merely created. A new application can still be awaiting review or may eventually be declined. An accepted agreement indicates that the partner has crossed a meaningful program boundary and can receive useful, operational next steps.

A simplified version of the documented event shape looks like this:

{
  "approved": true,
  "group": {
    "name": "Referral",
    "slug": "referral",
    "key": "grup_hs8kJ3YZJEoXl7"
  },
  "first_name": "Jane",
  "last_name": "Smith",
  "email": "jane.smith@partnerstack.com",
  "partner_key": "janesmith2319",
  "fields": [],
  "field_data": {},
  "key": "part_f2a02a0a3d9c47e29",
  "created_at": 1460036266235,
  "updated_at": 1707403393365
}

Do not treat this example as a universal schema contract for every webhook in your program. The agreement.accepted payload is centered on the partnership and agreement. Other PartnerStack event types have different shapes. For example, application events use application content, and events related to optional product capabilities can expose different object structures.

The safest design is to make one endpoint and one mapping for one event type at first. Once that works, add other event routes deliberately rather than writing one generic “send an email for anything” handler.

Map PartnerStack fields to a transactional email

For a first agreement-confirmation email, the essential mapping is straightforward:

PartnerStack fieldVolanea useWhy it matters
emailto.emailThe recipient address for this partner.
first_nameRecipient name and greetingPersonalizes the message without requiring a separate lookup.
partner_keyIdempotency-key input and internal correlationA stable PartnerStack identifier for the partnership.
keyAdditional correlation inputIdentifies the PartnerStack partnership record.
updated_atIdempotency-key inputDistinguishes the delivered agreement event from unrelated activity.
group.nameMessage bodyLets you tailor next steps to a partner group.
approvedGuard conditionThe example handler sends only when it is true.

There are two rules worth following here. First, send to a single recipient for a single partner event. It makes recipient-level consent, delivery tracking, and support investigations much clearer. Second, keep any custom-field dependency optional until you have observed production payloads.

For example, field_data is present in the sample as an object, but its keys are defined by your program’s configuration. You should not write code that assumes field_data.website or field_data.audience_size exists everywhere. A form field can be removed, made optional, unavailable in a particular group, or not included in a payload associated with a capability your PartnerStack program does not use.

Set up the webhook subscription safely

Start by deploying an HTTPS endpoint you control, such as:

https://integrations.example.com/webhooks/partnerstack/agreement-accepted

Then create a PartnerStack webhook subscription for the agreement.accepted event using PartnerStack’s Webhooks API and configure that endpoint as the target URL. PartnerStack describes webhook registration as an API operation, so use its current webhook reference for the exact subscription request and event configuration rather than relying on an assumed dashboard label or an unofficial setup flow.

Use a dedicated endpoint for this event during the first implementation. Separate paths make it easier to apply an allowlist of expected shapes, monitor event volume, and prevent a mapping meant for an agreement event from accidentally handling an application or commission event.

Your endpoint should meet these baseline requirements:

  • Accept HTTPS POST requests with JSON bodies.
  • Enforce a small request-body size limit.
  • Validate that required fields have the expected primitive types.
  • Reject malformed data without making an email call.
  • Log opaque identifiers such as the partnership key, not full raw payloads by default.
  • Return a successful response only after your system has durably accepted responsibility for processing the event.

For low-volume implementations, “durably accepted” can mean that the relay completed the Volanea call and persisted the outcome. For a larger program, it usually means the endpoint wrote the normalized job to a database or queue before returning success, while a worker performs the email request afterwards.

Keep the Volanea API key out of PartnerStack

The Volanea API key does not live in PartnerStack. PartnerStack’s role in this design is to deliver event data to your endpoint. The key belongs in the runtime environment of the server-side relay that receives that data.

Store it as a secret, for example:

VOLANEA_API_KEY=your-secret-key
EMAIL_FROM=Partner Team <partners@example.com>

The EMAIL_FROM address must use a sending domain you have configured and verified for Volanea. Keep the sender address in server configuration too, rather than accepting it from the webhook payload. A partner event should never be allowed to choose who an email appears to come from.

The same principle applies if you deploy the relay as a serverless function, edge worker, container, or application route:

  • Use the platform’s encrypted secret or environment-variable facility.
  • Restrict access to deployment and operations roles that genuinely need it.
  • Never commit the key to source control.
  • Never return the key in logs, HTTP responses, error reports, or client-visible configuration.
  • Rotate the key promptly if it is exposed.

Volanea uses secret-key Bearer authentication for the sending API. That is intentionally simple, but it also means possession of the key is sufficient to make authenticated requests. Your webhook receiver is therefore a trust boundary, not merely a formatting function. Review the Volanea API reference and setup guides before production so the sender domain, key scope, and send request are tested independently of PartnerStack.

Working Node.js relay: PartnerStack to Volanea

The following Express example maps the documented agreement.accepted fields to a Volanea transactional message. It expects PartnerStack to post only this event’s payload to this dedicated route.

The code uses the partnership record key plus updated_at to make a stable idempotency key. If the same webhook is delivered again because the first attempt timed out or the response was not received, the retry uses the same Volanea key and does not create a second logical send.

import express from "express";

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

const { VOLANEA_API_KEY, EMAIL_FROM, PORT = "3000" } = process.env;

if (!VOLANEA_API_KEY) throw new Error("VOLANEA_API_KEY is required");
if (!EMAIL_FROM) throw new Error("EMAIL_FROM is required");

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

function isEmail(value) {
  return typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}

app.post("/webhooks/partnerstack/agreement-accepted", async (req, res) => {
  const partner = req.body;

  // Field mapping from PartnerStack agreement.accepted payload.
  const partnerEmail = partner?.email;
  const firstName = typeof partner?.first_name === "string" && partner.first_name.trim()
    ? partner.first_name.trim()
    : "there";
  const partnerKey = partner?.partner_key;
  const partnershipKey = partner?.key;
  const eventVersion = partner?.updated_at;
  const groupName = typeof partner?.group?.name === "string"
    ? partner.group.name
    : "your partner program";

  if (partner?.approved !== true) {
    return res.status(202).json({ accepted: false, reason: "not an approved agreement" });
  }

  if (!isEmail(partnerEmail) || !partnerKey || !partnershipKey || !eventVersion) {
    return res.status(400).json({
      error: "Invalid agreement.accepted payload: required partner fields are missing"
    });
  }

  const safeName = escapeHtml(firstName);
  const safeGroup = escapeHtml(groupName);

  // Reuse this exact value if PartnerStack redelivers this same event.
  const idempotencyKey =
    `partnerstack:agreement.accepted:${partnershipKey}:${eventVersion}`;

  const emailPayload = {
    from: EMAIL_FROM,
    to: {
      email: partnerEmail,
      name: firstName === "there" ? undefined : firstName
    },
    subject: `Welcome to ${groupName}`,
    html: `
      <p>Hi ${safeName},</p>
      <p>Thanks for accepting the agreement for ${safeGroup}.</p>
      <p>You can now review your partner resources and begin sharing your referral link.</p>
      <p>If you need help, reply to this email and our partner team will assist.</p>
    `,
    text: [
      `Hi ${firstName},`,
      "",
      `Thanks for accepting the agreement for ${groupName}.`,
      "",
      "You can now review your partner resources and begin sharing your referral link.",
      "",
      "If you need help, reply to this email and our partner team will assist."
    ].join("\n")
  };

  try {
    const response = await fetch("https://api.volanea.com/v1/send", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${VOLANEA_API_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey
      },
      body: JSON.stringify(emailPayload)
    });

    const responseBody = await response.text();

    if (!response.ok) {
      console.error("Volanea send failed", {
        partnerKey,
        partnershipKey,
        status: response.status,
        responseBody
      });

      // Return non-2xx so your webhook processing policy can retry or alert.
      return res.status(502).json({ error: "Email provider request failed" });
    }

    console.info("PartnerStack agreement email submitted", {
      partnerKey,
      partnershipKey,
      idempotencyKey
    });

    return res.status(200).json({ accepted: true });
  } catch (error) {
    console.error("Volanea request error", {
      partnerKey,
      partnershipKey,
      message: error instanceof Error ? error.message : String(error)
    });

    return res.status(503).json({ error: "Temporary send failure" });
  }
});

app.listen(Number(PORT), () => {
  console.log(`PartnerStack relay listening on port ${PORT}`);
});

The mapping is intentionally visible rather than hidden in a template helper. You can see exactly where email becomes the recipient, where first_name becomes the greeting, and where key plus updated_at become the idempotency input.

Before deploying, send a direct test request to Volanea using the same from, to, subject, html, and text structure. That separates email-domain and API configuration issues from PartnerStack webhook issues. If you are testing recipient addresses supplied by partners, use the email address verification tool as one input to your validation workflow, but do not mistake address validity for consent or permission to send marketing content.

Why the idempotency key is not optional

Webhook delivery is not exactly-once messaging. A sender can retry when it does not receive a timely successful response, and a receiver can lose a response after the downstream provider already accepted the request. Both situations can make the same business event arrive more than once.

Without duplicate protection, this sequence is possible:

  1. PartnerStack posts agreement.accepted to your relay.
  2. Your relay successfully submits the email to Volanea.
  3. The connection between PartnerStack and your relay closes before PartnerStack receives the success response.
  4. PartnerStack retries the webhook.
  5. Your relay submits a second welcome email.

Volanea’s POST /v1/send supports the Idempotency-Key header. A repeated request using the same key and same body can replay the stored outcome rather than creating another send. That makes it the right protection at the delivery hop.

The key must describe the business event, not the HTTP attempt. A random UUID created inside the handler on every request would defeat the purpose because each retry would receive a fresh value. Conversely, do not use only the recipient email address. A partner may legitimately accept a revised agreement or receive a different operational email later.

For this event, combining the PartnerStack partnership key and updated_at creates a useful logical-event identifier. If your production system has its own durable webhook-event table, store the exact generated idempotency key with the normalized event record. That gives support and operations teams a direct answer to “Did we attempt this send already?”

Production design: acknowledge quickly, send from a queue

The example above calls Volanea before returning a response because it is easy to understand and useful for a controlled first deployment. In production, especially if your webhook endpoint can receive bursts of partner activity, move the email operation to an asynchronous worker.

A robust pattern looks like this:

  1. Receive the PartnerStack JSON payload.
  2. Validate the expected event shape and required fields.
  3. Normalize it into a small internal record containing the partnership key, recipient, event timestamp, and intended message type.
  4. Insert the record into a database with a unique constraint on your idempotency key.
  5. Return success once the record is committed.
  6. Have a worker submit the Volanea request using the stored key.
  7. Persist the Volanea result, status, and provider message identifier if returned.
  8. Retry only transient failures with the same idempotency key.

This design shortens the amount of work performed while PartnerStack is waiting for a response. It also gives you a recoverable backlog if Volanea is temporarily unavailable, your deployment platform restarts, or an email change introduces a validation error.

Do not queue raw payloads indefinitely without a retention plan. PartnerStack payloads may contain personal information such as names and email addresses. Store only the fields needed for the send, protect access, define retention, and redact address data from application logs where possible.

When this breaks

Every webhook-to-email integration eventually encounters a failure that is ambiguous from one side of the connection. Build the response plan before the first partner asks why they received two messages or no message at all.

PartnerStack retries cause duplicate sends

A retry can happen when PartnerStack does not observe a successful response from your endpoint, even when your process already submitted the email request. The fix is not “hope the webhook arrives once.” The fix is a stable idempotency key that is derived from the partnership event and reused for all attempts.

Also make your local queue idempotent. Volanea protects the provider request, but your own systems should still avoid enqueueing needless duplicate jobs. A uniqueness constraint on partnerstack:agreement.accepted:<partnership key>:<updated_at> is a practical first defense.

Webhook timeouts happen before a send result is known

A timeout does not prove that an email failed. It only proves that one system did not obtain a timely answer from another system. If your handler waits on slow database queries, remote template rendering, several API calls, and the email send itself, it increases the chance of timeout and redelivery.

Keep the inbound path narrow. Validate, persist, and acknowledge. Push nonessential enrichment into the worker. If you do call Volanea synchronously, set a conservative upstream timeout and preserve the idempotency key when you retry after a network failure.

Required payload fields are missing or blank

Not every PartnerStack event, group configuration, or enabled capability has the same optional data. Custom form data can be absent, empty, renamed, or unavailable for a program configuration. Do not use optional field_data entries as recipient data unless you have a fallback and an observed contract for the exact event you subscribed to.

For the agreement flow, treat email, partner_key, key, and updated_at as required by your relay because the email cannot be safely addressed or deduplicated without them. Treat first_name and group.name as graceful-degradation fields. The example uses “there” and “your partner program” when those display values are unavailable.

If a required field is missing, return an error or persist the record as “needs review”; do not guess an email address from a team object or from a custom field. Sending to the wrong person is worse than delaying an onboarding note.

The sender domain is not ready

An API request can be structurally valid while the sender identity is not configured for production delivery. Verify the domain used in EMAIL_FROM before enabling the PartnerStack subscription. Make sure the value is not a placeholder left over from staging and that it aligns with the program brand recipients recognize.

Keep staging and production secrets separate. A test partner accepting an agreement should not trigger mail from your live sender domain unless that is an intentional end-to-end test.

A payload field becomes unsafe in HTML

Partner names, group names, and custom data are external input. A value that appears harmless in plain text can break HTML layout or inject markup when concatenated directly into an HTML message. Escape every value that comes from a webhook before interpolating it into an HTML body.

The example uses escapeHtml for the first name and group name. Keep doing that even if PartnerStack’s UI validation currently prevents angle brackets or unusual values. Integration code should defend itself at the boundary.

Extend the pattern without turning it into a blast engine

Once the agreement confirmation works, you can apply the same architecture to other PartnerStack events. The key is to define the business message before writing another handler.

Good examples include:

  • An application-created acknowledgment that says the submission was received, if your program’s process calls for it.
  • An application-approved next-steps email, if the event data provides a valid recipient and approval is the moment access truly begins.
  • A partner lifecycle notice tied to a deliberate operational event in your program.
  • An internal alert to your partner team when a high-value application arrives, sent to a fixed internal distribution address.

Avoid automatically converting every partner record update into outbound email. Profile changes, tag changes, or manager reassignment can be frequent and may not be useful to the recipient. Event volume is not a communication strategy.

For any new event, repeat the same questions:

  1. What exact PartnerStack event starts this message?
  2. Which documented payload fields are required to address and personalize it?
  3. What stable identifier defines one logical event?
  4. Is the content transactional, operational, or marketing?
  5. Does the recipient reasonably expect it?
  6. What happens if the field is absent, blank, or stale?

Those questions keep an integration understandable long after its initial launch.

Testing checklist before enabling production sends

A webhook integration should be tested as a chain, not only as isolated code. Test PartnerStack delivery, your receiver, the Volanea request, and the mailbox result.

Use this checklist:

  • Confirm the Volanea sender domain and sender address are configured before testing the webhook.
  • Send a direct Volanea API request from a secure local or staging environment.
  • Deploy the relay behind HTTPS and verify that it accepts JSON POST requests.
  • Create the agreement.accepted webhook subscription using the current PartnerStack Webhooks API reference.
  • Trigger a test agreement acceptance with a test partner record.
  • Confirm the relay logs the partnership key and idempotency key without printing your API key or full email body.
  • Repeat the identical webhook request and confirm it does not produce a second logical send.
  • Test a payload without first_name and confirm the fallback greeting works.
  • Test a payload without email and confirm the relay rejects it instead of guessing.
  • Test an unreachable Volanea endpoint or temporary network failure in a non-production environment and confirm that retries reuse the same idempotency key.
  • Inspect the received HTML and plain-text alternatives in a real inbox.

Record the expected behavior for each test. The goal is not merely a green HTTP status; it is proof that a retried event does not create duplicate mail and that incomplete data cannot create a misaddressed message.

Conclusion

To send email from PartnerStack with Volanea, use PartnerStack’s outbound webhook capability as the event source and a server-side relay as the secure connector. For the agreement.accepted event, map the partnership’s email, first_name, partner_key, key, and updated_at into one transactional email request, then protect that request with a stable Volanea idempotency key.

The architecture may be small, but the operational details matter: keep the API key outside PartnerStack and outside the browser, validate payloads conservatively, escape external values in HTML, acknowledge webhooks quickly, and make every retry safe. That produces a dependable onboarding or operational email flow without requiring a native PartnerStack plugin.

FAQ

Does Volanea have a native PartnerStack marketplace app?

No. This integration uses PartnerStack’s outbound webhooks and a server-side relay that calls Volanea’s REST API. There is no marketplace-install flow to rely on.

Which PartnerStack event should start the email?

This guide uses agreement.accepted, the agreement event sent when a partner accepts your program’s terms of service. It is a good trigger for practical onboarding steps because it occurs after agreement acceptance rather than at initial application submission.

Where should the Volanea API key be stored?

Store it only as a secret in the server, worker, or serverless environment that receives the PartnerStack webhook. Do not place it in PartnerStack fields, browser code, public environment variables, client-side automation configuration, or source control.

How do I prevent a PartnerStack webhook retry from sending two emails?

Create one deterministic Idempotency-Key per logical PartnerStack event and reuse it for every retry. For the agreement example, combine the PartnerStack partnership key and updated_at, then send that same value in Volanea’s Idempotency-Key header.

What if a custom PartnerStack field is unavailable?

Treat custom and optional fields as optional. Use safe fallbacks for display data, and reject or queue for review when required sending fields such as the recipient email address or partnership identifier are missing.