Notix
Integrations

Send email from a Vercel Function.

A Vercel Function that sends through the email API is a few lines: read the key from the environment, call send, return the id. What this page adds is the Vercel-specific part: which environment gets which key, why the SDK wants the Node runtime, what the duration limits mean for a send, and the one option that stops a retried invocation from emailing someone twice. Nothing to install from a marketplace; the integration is your code and one environment variable.

The function

One handler, three ways to write it.

The first two use notix-js and answer { data, error }, so a failed send is a value you check rather than an exception you forget. The third is the same call with fetch, for a project that wants no dependency or a route that runs at the edge.

app/api/send/route.ts
// app/api/send/route.ts
import { NextResponse } from "next/server";
import { Notix } from "notix-js";

const notix = new Notix(process.env.NOTIX_API_KEY!);

export async function POST(request: Request) {
  const { to, subject, html } = await request.json();

  const { data, error } = await notix.emails.send({
    from: "receipts@acme.com",
    to,
    subject,
    html,
  });

  if (error) {
    return NextResponse.json({ error }, { status: 502 });
  }
  return NextResponse.json({ id: data?.emailId });
}

The from address must be on a domain you have verified in Notix; the quickstart covers the DNS records. For server actions, React Email templates and the rest of an App Router project, see the Next.js guide.

Environment variables

One key per environment, none of them public.

Vercel keeps separate variables for production, preview and development. Give each its own Notix API key, so a preview deployment built from a pull request cannot send as production, and so a key you rotate for one does not break the others.

Never NEXT_PUBLIC_.

Next.js inlines any variable with that prefix into the browser bundle at build time. A key named NEXT_PUBLIC_NOTIX_API_KEY is readable by anyone who opens the site. Name it NOTIX_API_KEY and read it only in code that runs on the server: route handlers, server actions, and files under api/.

A sending key, not a full one.

Create the key in the dashboard with sending access only. It can create messages but cannot read contacts or delete domains, which is the right shape for something that lives in a deployment platform's settings.

Vercel CLI
# One command per environment; development cannot be combined
# with production or preview in the same command.
vercel env add NOTIX_API_KEY production
vercel env add NOTIX_API_KEY preview
vercel env add NOTIX_API_KEY development

vercel env add NOTIX_WEBHOOK_SECRET production

# Pull them into .env.local for `vercel dev` or `next dev`
vercel env pull .env.local
Runtime

Node by default; Edge only with fetch.

The SDK sends with fetch, which every runtime has. It also imports Node's crypto module to verify webhook signatures, and the Edge runtime does not provide that module, so importing notix-js in an edge route fails. Leave the runtime at its Node default for any route that uses the SDK. If a route must run at the edge, call the API with fetch directly.

Runtime export
// app/api/send/route.ts
// The default. notix-js needs it: the package imports Node's crypto
// module for webhook signatures, which the Edge runtime does not provide.
export const runtime = "nodejs";

// Only if the whole route must run at the edge: drop the SDK and use the
// fetch sample above. The send endpoint is a plain HTTPS POST.
// export const runtime = "edge";
Duration and retries

The timeout is not the risk. Running twice is.

Duration.

A send is one HTTPS request that completes in well under a second, so the default limit is enough. With Fluid compute, on by default for new projects, the default maximum is at least 60 seconds on every plan; the older serverless model allowed 10 seconds on Hobby and 15 on Pro. Export maxDuration from the route only for a handler that loops over many recipients, and consider the batch endpoint before you do.

Idempotency.

A function can run twice: a client retry, a deploy that lands mid-request, a double-submitted form. Without a key, that is two emails. Pass idempotencyKey with a value that names the thing being sent; the API answers a repeat with the original message and sends nothing new.

maxDuration and an idempotent send
// app/api/send/route.ts
// A single send finishes in well under a second; the default is enough.
// Raise it only for a handler that loops over many recipients.
export const maxDuration = 30;

// One key per thing that must never be sent twice. A function that is
// retried, redeployed mid-request or double-submitted by a form returns
// the original message instead of creating a second one.
const { data, error } = await notix.emails.send(
  { from: "receipts@acme.com", to, subject, html },
  { idempotencyKey: `order-${orderId}-receipt` },
);
Delivery

A webhook route, and why there is no queue.

The function's job ends when the API accepts the message. Delivery, retries on a slow receiving server, bounces and complaints happen on Notix's side afterwards, and each one arrives at a route you choose as a signed event. That is why a single send needs no queue on Vercel: there is nothing left for the function to wait on. Verify the signature over the raw body and record the outcome against your own id.

app/api/webhooks/notix/route.ts
// app/api/webhooks/notix/route.ts
import { Notix } from "notix-js";

const notix = new Notix(process.env.NOTIX_API_KEY!);
const webhooks = notix.webhooks(process.env.NOTIX_WEBHOOK_SECRET!);

export async function POST(request: Request) {
  // The raw body, not parsed JSON: the signature covers the exact bytes.
  const rawBody = await request.text();
  let event;
  try {
    event = webhooks.constructEvent(rawBody, { headers: request.headers });
  } catch (error) {
    return new Response((error as Error).message, { status: 400 });
  }

  switch (event.type) {
    case "email.delivered":
    case "email.bounced":
    case "email.complained":
      // Record the outcome against your own order or user id.
      break;
  }
  return new Response("ok");
}

Add the deployment's URL as the endpoint in the dashboard and store the secret as NOTIX_WEBHOOK_SECRET. A preview deployment has a different URL each time; point the webhook at production, or at a stable preview alias. Bounced and complained recipients are suppressed automatically, so the next send to them is refused before it costs anything; the deliverability check shows that state ahead of time.

FAQ

Questions, answered.

How do I send email from a Vercel Function?
Create a function, either a Next.js route handler under app/api or a file under api/ in a project without a framework, read NOTIX_API_KEY from the environment, and call notix.emails.send from notix-js or POST to https://app.usenotix.dev/api/v1/emails with fetch. The function answers with the message id; delivery is reported afterwards through a webhook.
Does notix-js run on the Edge runtime?
No. The send path is plain fetch, but the package also imports Node's crypto module for webhook signature checks, and the Edge runtime does not provide it, so the import fails. Keep the Node runtime, which is the default, for anything that uses the SDK. A route that must run at the edge can send with fetch alone; the API is an HTTPS POST with a bearer token.
Where do I put the API key on Vercel?
In the project's environment variables, once per environment: vercel env add NOTIX_API_KEY production, then the same for preview and development, or the same three fields in the dashboard. Name it NOTIX_API_KEY, never NEXT_PUBLIC_NOTIX_API_KEY: a NEXT_PUBLIC_ variable is inlined into the browser bundle at build time and is public the moment the deployment goes live. Use a separate key for preview deployments so a leaked preview cannot send as production.
Will a Vercel timeout cut off my send?
Not for a single message. A send is one HTTPS request that completes in well under a second, and with Fluid compute, which is on by default for new projects, the default maximum duration is at least 60 seconds on every plan. Set maxDuration on the route only when a handler loops over many recipients, and prefer a batch send for that anyway.
What happens if the function runs twice?
Without an idempotency key, two sends. A retried invocation, a deploy that lands mid-request or a user who double-clicks the form all look the same to the API. Pass idempotencyKey with a value that names the thing being sent, such as order-4471-receipt; the second call with the same key returns the first message instead of creating another.
Do I need a queue to send email from Vercel?
Not for one message per request. The function waits for the API to accept the message, which is a few hundred milliseconds, and Notix handles delivery, retries and bounces on its side; a webhook tells you the outcome. A queue earns its place when one request must fan out to thousands of recipients, and even then the batch endpoint or a scheduled send usually replaces it.

Password resets are the usual first email from a Vercel app; that use case has the flow and the copy. The free plan does not ask for a card.

Send your first email in five minutes.

Verify a domain, copy an API key, make one call. The free plan does not ask for a card.