Skip to main content
This guide shows how to send transactional email from Vercel Functions with AhaSend, keep credentials on the server, and test a sandbox send. The AhaSend SDK runs in Vercel Functions. This guide covers deployment; for complete application handlers, see the Next.js and Express guides. Use a send-only key from Credentials → Add → API Key v2 with messages:send:{your-domain}. Use messages:send:all when sending from many domains. For management tasks such as creating a webhook, use a separate full key from Account Settings → API Keys with the needed scopes. See send-only credentials and authentication. Keep credentials on the server. Use sandbox mode for tests. Reuse a stable, business-event idempotency key only with the exact same payload; use different keys for sandbox and live sends. Check every recipient’s status and return a failure when any item is error; HTTP 202 alone does not mean every recipient was accepted. For webhooks, verify the raw request bytes using the full secret as the raw UTF-8 HMAC key, including its aha-whsec- or aha-rsec- prefix. Never base64-decode the secret. The AhaSend SDK verifiers handle this. These examples use a 30,000,000-byte webhook body limit; a hosting platform may impose a smaller limit. Acknowledge verified unknown event types with 2xx. See verification and retry policy.

Prerequisites

Install the SDK

Try a Sandbox Send

Run this short SDK check locally before adding the longer server integration below. Use a verified sender, a send-only API key, and your account ID. It sends to a reserved test address with sandbox: true, so no email is delivered. In the server project directory, add .env to .gitignore, then create .env with AHASEND_API_KEY, AHASEND_ACCOUNT_ID, and AHASEND_FROM (an address on your verified domain). Keep these values out of browser code and AI prompts. Use Node.js 22 or newer. Save this as quick-send.mjs:
quick-send.mjs
Run node --env-file=.env quick-send.mjs from that directory. The output lists each recipient status; queued or scheduled means accepted for sandbox processing. The process exits with a failure if the request or any recipient fails. Continue below for the full integration.

Choose a Runtime

Use Vercel’s default server runtime; no runtime setting is required for the SDK. The SDK also supports Web APIs in edge runtimes. Check your installed Next.js version’s runtime documentation before changing a route’s runtime.
The runtime choice does not change the security boundary. Import the SDK only in server code and never expose an API key to browser code.

Add a Server-Side Send Helper

Create the client at module scope and reuse it. This helper accepts an already authorized and validated business record; do not call it directly with untrusted browser input.
lib/ahasend.ts
Call this helper only after authenticating and authorizing the request. Every Vercel deployment, Preview included, is served from a public generated URL, so a route that reaches this helper without an authorization check is an open mail relay for anyone who finds the URL. For a browser flow, derive signupId, email, and name from your server-side user record. For a backend-to-backend route, require a service credential, validate the request body and bound its size, and apply rate limits. Vercel itself rejects a request body over 4.5 MB with a 413 (FUNCTION_PAYLOAD_TOO_LARGE) before your function runs, which is a platform ceiling and not a substitute for your own limit. The Next.js guide provides a complete route-handler example. An accepted send is multi-status: result.data holds one result per recipient, and an entry can have status: "error" even when the request resolves. Reuse the same stable signup ID for retries of the same payload. A stored non-server-error outcome is replayed for 24 hours; a server error releases the key for re-execution, so the surrounding workflow must tolerate an uncertain duplicate. AhaSend scopes an idempotency key to your account and matches it on the request method, path, and exact body — never on which API key sent it. That is why welcomeKey puts sandbox or live in the key itself: without it, a Preview sandbox send and a Production live send sharing one signup ID would reuse a single key with two different bodies, which the API rejects as a 422 mismatch.

Set Environment Variables

1

Add production variables

In Project → Settings → Environment Variables, add AHASEND_API_KEY and AHASEND_ACCOUNT_ID for Production, plus AHASEND_WEBHOOK_SECRET if you use webhooks.
2

Turn on Sensitive before saving each credential

With Sensitive enabled, Vercel stores the value unreadably: it cannot be retrieved from the dashboard or vercel env ls afterwards, only replaced. Do this while adding the variable — converting an existing one means deleting and re-adding it. Sensitive is available for Production and Preview, not Development.
3

Configure safe previews

For Preview, use a separate least-privilege API key and set AHASEND_SANDBOX=true. Sandbox sends are validated but not delivered. Do not make the production API key available to Preview deployments.
4

Redeploy

Environment variable changes apply only to new deployments. Create a new deployment after adding, changing, or rotating a value.
You can also add values interactively with the CLI. Name the target environment on every command: vercel env add AHASEND_API_KEY with no environment offers every environment at once, which is how a Production key ends up in Preview.
vercel env add stores Production and Preview values as sensitive by default; pass --no-sensitive only if you have a reason to keep a value readable.
Never give these variables a framework’s public prefix — NEXT_PUBLIC_ in Next.js, VITE_ in Vite, NUXT_PUBLIC_ in Nuxt. Any of them inlines the value into the browser bundle. Rotate an API key immediately if it is exposed.

Local Development

vercel dev downloads Development-scoped variables into memory, so a local secret file is not required:
If you deliberately use vercel env pull .env.local instead, ensure .env.local is ignored by version control and never commit it. As of September 2026, Vercel documents a 4.5 MB request and response limit. Check that limit before accepting inbound route events with attachments; the host can reject a request before your verifier runs.

Webhooks on Vercel

Point the webhook created in your AhaSend dashboard at a stable production route such as:
In Next.js, use nextRouteHandler from @ahasend/sdk/webhooks. It reads and caps the exact raw request body, verifies the signature and timestamp, and only then invokes the application handler. The Next.js guide contains the complete handler. Its maxBodyBytes option can only narrow the verifier’s 30 MB ceiling — set it to the size your events actually reach, since Vercel already returns a 413 above 4.5 MB and the default ceiling is far above anything a webhook delivery needs. Keep the webhook route on a domain that Deployment Protection leaves reachable. Standard Protection covers preview and generated deployment URLs but not production domains, which is what a webhook endpoint needs. If you switch the project to All Deployments protection, the production domain requires Vercel Authentication and AhaSend’s deliveries get a 401. A webhook configuration carries no custom headers, so the bypass has to travel in the URL: create a Protection Bypass for Automation secret and append ?x-vercel-protection-bypass=<secret> to the webhook URL. Treat that secret as a credential — it bypasses protection on every deployment in the project until you rotate it, and signature verification, not the bypass, is what authenticates the route. Timestamp validation is not replay deduplication. Before returning a 2xx, atomically claim the verified webhook-id and enqueue durable work (or commit both through an outbox). Acknowledge a duplicate ID without enqueuing it again, and make consumers idempotent. Vercel Queues uses at-least-once delivery, so a queue consumer must also tolerate redelivery. Do not use an untracked promise for required webhook work. Vercel’s post-response helpers — after() from next/server on Next.js 15.1 and later, waitUntil() from @vercel/functions otherwise — do keep the work inside the invocation, but they share the function’s timeout and their promises are cancelled if it expires. Use them for non-critical work, not as a replacement for durable persistence.

Going Further

For substitutions, attachments and scheduled sends, use the sending guide. The Node.js SDK and Go SDK references explain client options, per-recipient results and retries. Use idempotency rules when your application retries a business operation.

Troubleshooting

Vercel recommends its default server runtime for new functions. Remove an unnecessary Edge runtime setting and redeploy. If the route must remain on Edge, confirm that the failure comes from another dependency or from using CommonJS rather than ES module imports.
Confirm that the API key and account ID are set for the environment you deployed to, that the key can send from from.email, and that you created a new deployment after changing the variables.
Give Preview its own restricted key, set AHASEND_SANDBOX=true, and create a new Preview deployment. Rotate the Production key if it was exposed to a Preview environment that should not have it.
Verify and atomically persist the delivery before responding, return a 2xx promptly, and process the durable work idempotently. Do not rely on an untracked promise or timestamp validation to prevent replay.