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.
AhaSend Rules for AI Coding Agents
Add this shared block to your project instructions:@supabase/server to 1.4.1. Its withSupabase helper passes the authenticated Supabase client and user claims to the handler.
Prerequisites
- A Bolt project with Bolt Database or Supabase connected, or a separately deployed Node backend
- An AhaSend account with a verified sending domain
- An API key with a send scope, plus your account ID
messages:send:{your-domain} instead of messages:send:all so a leaked key can only touch one domain’s mail.
Pick Your Backend
Two rules apply whichever shape your project takes. Never call AhaSend from the frontend or the WebContainer preview: that makes your API key available in the browser. Put the key in the secret store for the backend that sends the message, not in a project environment variable used by preview or client code.@ahasend/sdk supports the common Bolt backend choices. What differs is where the key lives:
Bolt and Supabase server functions use the edge-function path below.
Send with the SDK
Install the SDK, which handles retries, idempotency, and typed errors: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 Deno 2.x. Save this as quick-send.mjs:
quick-send.mjs
deno run --env-file=.env --allow-net=api.ahasend.com --allow-env=AHASEND_API_KEY,AHASEND_ACCOUNT_ID,AHASEND_FROM quick-send.mjs. Deno resolves the npm: import. This is a local SDK check; the server function integration follows below. The output lists recipient statuses and the process fails on a rejected recipient or failed request.
In a Bolt or Supabase server function there is nothing to install in the app package: import it with import { AhaSendClient } from "npm:@ahasend/sdk".
If you use Bolt Database, open the database icon, select Secrets, and create AHASEND_API_KEY, AHASEND_ACCOUNT_ID, and AHASEND_SANDBOX (set it to true while testing). Bolt makes database secrets available only to server functions.
If you use your own Supabase project, add the same credentials as Supabase secrets:
“Add a server function calledThe function it generates should look close to this. Create the client once at module scope, authenticate the caller, and load the order before sending:send-receiptthat emails an order confirmation with@ahasend/sdk. Authenticate the caller withwithSupabase({ auth: 'user' })and keep JWT verification enabled. Accept only an order ID, then load the recipient address, name, and order details through the caller’s RLS-scoped Supabase client instead of trusting them from the request body. Read the credentials from server-function secrets withDeno.env.get(), fail at startup ifAHASEND_API_KEYorAHASEND_ACCOUNT_IDis missing, stay in sandbox mode unlessAHASEND_SANDBOXis exactlyfalse, and create oneAhaSendClientat module scope. Use a stable, sandbox-or-production-specific idempotency key for each order. Treat the resolved send as an accepted HTTP 202 response, but inspect every recipient result forstatus: \"error\". Return a generic error to the browser, and never log recipients, message content, secrets, or whole error objects. The frontend must call this function and must never call AhaSend directly or contain the API key.”
supabase/functions/send-receipt/index.ts
message and body carry the provider’s response. The browser gets a generic 502.
On a Node backend the code differs only in how it reads configuration and how it is routed: swap Deno.env.get(...) for process.env, and call the send from your Express, Fastify, or Nitro route after that route has authenticated the caller and loaded the order itself. Full framework setup lives in the Express and Next.js guides.
Handle the 202 Response
Success is 202, not 200: AhaSend has accepted and queued your message for asynchronous delivery. A handler that expects 200 reports failures that did not happen. The 202 body is multi-status: it carries one result per recipient. An individual recipient can come backstatus: "error" with a null id, a suppressed address for example, while the request succeeded and the promise resolves. Inspect every entry, not just the first.
Test in Sandbox Mode
Test through the deployed server function, including when you trigger it from Bolt’s preview; do not put the key in the WebContainer to test locally. KeepAHASEND_SANDBOX=true and AhaSend accepts the message, validates it, triggers the relevant webhooks, and logs it in your dashboard, then stops before delivery. That gives you a real round-trip to the API confirmed by a real 202, with nothing landing in an inbox while you iterate.
Rehearse the failures too. Add sandbox_result: "bounce" to simulate a hard bounce, or "defer", "fail", and "suppress" for the rest, and check your code handles each. The full set is in the sandbox mode guide.
Deploy and Verify the Key
Your deployed backend and frontend live in different homes. Before you call it done:1
Search for the key in frontend code and preview settings
No
aha-sk- string anywhere in the frontend and no AhaSend variable exposed to the WebContainer. The key belongs in Bolt Database Secrets, Supabase Edge Function secrets, or the environment of the Node host that sends the message.2
Confirm the browser calls your backend
The frontend calls your Edge Function or your Node route; only that backend talks to
api.ahasend.com.3
Confirm the function rejects unauthenticated callers
Call the deployed function URL with no session attached. It must answer 401 rather than send anything. Keep JWT verification enabled on it, and check that the recipient address is read from your database rather than from the request body: a function that emails whatever address the caller passes is a relay even when callers have to sign in.
4
Flip sandbox off deliberately
Set
AHASEND_SANDBOX=false in the backend’s secret store, then send one real test to an address you control. Any other value, including a missing value, keeps the example in sandbox mode.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
Error importing @ahasend/sdk in a Supabase Edge Function
Error importing @ahasend/sdk in a Supabase Edge Function
Confirm the import is server-side and uses the Deno npm specifier exactly as shown:
npm:@ahasend/sdk. There is nothing to install in the app’s package.json for this path — the runtime resolves the specifier. If it still fails, redeploy and inspect the function’s logs.undefined API key at runtime
undefined API key at runtime
The key was set in the project preview or frontend host instead of where the send runs. For Bolt Database, use Database > Secrets. For your own Supabase project, use
supabase secrets set; hosted Edge Functions receive updated secrets without a redeploy.The frontend gets 401 from the server function
The frontend gets 401 from the server function
The caller has no session. Sign in and invoke the function through the authenticated Supabase client so the request carries the user’s JWT. Do not disable JWT verification or switch the function to unauthenticated access to make the error go away: that publishes a send endpoint to the internet.
The server function works while testing but not after publish
The server function works while testing but not after publish
Confirm the published frontend calls the same server-function URL and that the function’s own secret store contains the API key, account ID, and intended sandbox setting. Then look at the AhaSend dashboard logs: a message that never reached AhaSend leaves no log line.
400 error mentioning the from address
400 error mentioning the from address
The
from address must belong to a verified sending domain on your account. Check domain status in the dashboard.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.

