Skip to main content
This guide shows how to send transactional email from a Lovable server function with AhaSend, keep credentials on the server, and test a sandbox send. Ask Lovable for email and it will build something that shows a success toast in the preview. Whether that toast means a delivered message or a published API key is the part you cannot see from there. 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.

AhaSend Rules for AI Coding Agents

Add this shared block to your project instructions:
This example pins @supabase/server to 1.4.1. Its withSupabase helper passes the authenticated Supabase client and user claims to the handler.

Prerequisites

Lovable cannot verify your sending domain or create your credentials. Those steps happen in the dashboard and are quick: the quickstart has both. Scope the key to messages:send:{your-domain} rather than messages:send:all, so if the key ever leaks the blast radius is one domain’s outbound mail instead of your whole account.

Install the SDK for a Local Check

Install Deno 2.x on your own computer. The npm: import in the example below downloads the SDK; no browser package installation is needed.

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 Deno 2.x. Save this as quick-send.mjs:
quick-send.mjs
Run 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.

Store the Key as a Secret

Anything the browser can read, a visitor can read, so the key never goes in frontend code or in a VITE_-prefixed variable, which ships to the browser by design.
If you ever find yourself typing your API key into a variable with a VITE_ prefix, stop. That one is public. Rotate the key before continuing.
Lovable Cloud is on by default for most workspaces, and it switches itself on the first time you ask for a feature that needs a backend. That gives your app server-side functions and a place to keep secrets. Open Cloud → Secrets and add three values:
Lovable encrypts these and injects them into your Edge Functions at runtime. They are not part of your frontend bundle, they are not in your project’s .env, and they do not show up in the published site. That is the whole point.

Prompt Lovable for a Backend Send

Lovable Cloud runs backend integrations in Edge Functions. Import @ahasend/sdk with a Deno npm: specifier, read secrets with Deno.env.get(), and pass them to the constructor. Ask precisely. The words that matter most are Edge Function: without them, Lovable may call the email API from the client. Spell out the architecture:
“When a signed-in user requests their welcome email, send it through AhaSend from an Edge Function, never from the frontend. Authenticate the caller with withSupabase({ auth: 'user' }), keep JWT verification enabled, and derive the recipient email and stable idempotency key from the verified user claims rather than request JSON. Import AhaSendClient from npm:@ahasend/sdk, read AHASEND_API_KEY, AHASEND_ACCOUNT_ID, and AHASEND_SANDBOX with Deno.env.get(), reject missing configuration at startup, check every returned recipient status, and never log recipients, content, secrets, request bodies, or whole errors. The frontend should invoke the Edge Function with the signed-in user’s session, not call AhaSend directly.”
The function it generates should look close to this:
supabase/functions/send-welcome-email/index.ts
Check four details in review: secrets are read only in the Edge Function; withSupabase authenticates the caller; the recipient and idempotency key come from verified user claims; and supabase/config.toml does not disable JWT verification for send-welcome-email. The wrapper also handles browser CORS and preflight requests. AhaSend answers a send with 202 because delivery is asynchronous: the API has accepted and queued your message rather than finished delivering it. The body is multi-status, so result.data carries one entry per recipient, and an individual recipient can come back status: "error" with a null id, a suppressed address for example, while the promise resolves. Check every entry, not just the first. The Edge Function returns its own 202 only after every recipient was accepted. The stable key protects retries of the same welcome-email operation. Reuse a key only with the exact same payload: AhaSend matches a key against a hash of the request body, so the same key with a changed body is answered 422 rather than replayed. That is why the key carries the environment — the sandbox flag is part of the body, and a key already stored against a sandbox send would reject the first live one. That key is also the send-rate ceiling on this endpoint. Deriving it from the user id means one account gets one welcome email per idempotency window no matter how many times the button is clicked; the calls after the first replay the stored response instead of mailing again. If Lovable rewrites the key to a fresh UUID per request — a reasonable default in other contexts, and what AhaSend’s own idempotency guide suggests for one-off retries — that ceiling disappears and anyone who can sign up can make your account send on demand. For durable exactly-once behavior beyond the API’s idempotency window, atomically claim a welcome-email job in your database and process it from a retryable outbox or worker.

Verify Where the Key Landed

Lovable’s preview will not catch this mistake for you. Use Lovable’s code view to confirm these checks.
1

The key is referenced only inside the Edge Function

Via Deno.env.get(), and nowhere else. If you see your key, anything starting with aha-sk-, or a VITE_AHASEND... variable anywhere in the frontend, send Lovable back: “Move the AhaSend call into the Edge Function and remove the key from the frontend entirely.” Then rotate the key.
2

The frontend calls your Edge Function

It should invoke the function through the authenticated Supabase client, which sends the user’s session. The browser should be talking to your own backend, never directly to api.ahasend.com.
3

The function does not trust recipient data from the browser

It must derive the recipient from verified user claims and keep JWT verification enabled. A caller-controlled email field turns the function into an email relay even when callers must sign in.
If you are not comfortable reading the code, ask Lovable directly: “Is my AhaSend API key exposed anywhere in the frontend bundle?” Then verify its answer against the checks above rather than taking the success toast at face value.

Test in Sandbox Mode

Keep AHASEND_SANDBOX=true while you build. In sandbox mode AhaSend runs your message through validation and processing, fires the relevant webhooks, shows it in your dashboard logs, and then stops before delivery. It costs nothing and it cannot email a real customer by accident. Sandbox also lets you rehearse the unhappy paths. Add sandbox_result: "bounce" to the send request and AhaSend simulates a hard bounce so you can see how your app reacts. "defer", "fail", and "suppress" cover the other outcomes, and the full list is in the sandbox mode guide. Run through them once, confirm you get a clean 202 on the happy path, and only then set AHASEND_SANDBOX=false. Know what that flip costs you. A Lovable project has one Cloud backend and one Secrets store, so the editor preview and the published app read the same AHASEND_SANDBOX: turning it off turns real sending on for both at once, including the next time you click the button in the preview. If you want to keep rehearsing after launch, do it on the AhaSend side instead of with this flag — a credential created in sandbox mode simulates every send made with it regardless of the request, so a separate development project holding a sandbox-mode key cannot mail a real customer even if the flag is wrong. The sandbox mode guide covers creating one.

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

Confirm the import is server-side and uses the Deno npm specifier exactly as shown: npm:@ahasend/sdk. Then ask Lovable to call the function directly and inspect its Edge Function logs.
Re-check Cloud → Secrets, confirm the key has a send scope for the from domain, and rotate it if it was ever exposed. Secret changes are available to functions without redeploying them.
Make sure the user is signed in and the frontend invokes the function through the authenticated Supabase client. Keep JWT verification enabled; do not make the send function public to work around authentication errors.
The from address must belong to a verified sending domain on your account. Check domain status in the dashboard.