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
- A Vercel account and a project to deploy
- An AhaSend account with a verified sending domain
- An API key permitted to send from that domain, and your account ID
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 withsandbox: 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
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.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
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.
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.
Local Development
vercel dev downloads Development-scoped variables into memory, so a local
secret file is not required:
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: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
SDK import fails in an Edge route
SDK import fails in an Edge route
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.
401 from AhaSend after deploying
401 from AhaSend after deploying
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.Preview deployment sends real mail
Preview deployment sends real mail
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.Webhook deliveries time out or repeat
Webhook deliveries time out or repeat
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.
Related Guides
- Before sending: verify a domain and create a send-only key.
- Other ways to send: REST API, SMTP, CLI quickstart, Node.js SDK and Go SDK.
- Request rules: API authentication, scopes, idempotency, errors and rate limits.
- Testing and events: sandbox mode, CLI webhook testing, event payloads, signature verification and delivery retries.
- Data and limits: retention, tracking and plans and feature availability.

