Skip to main content
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.

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.

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

  • Templating: pass substitutions per recipient and use {{ variable }} in the subject or body.
  • Batch sends: recipients accepts up to 100 entries, each of which gets its own message.
  • Webhooks: AhaSend signs every webhook with the Standard Webhooks scheme, and WebhookVerifier from @ahasend/sdk/webhooks verifies it on Deno. A receiver arrives with no user session, so it needs auth: "none" plus verify_jwt = false under [functions.<name>] in supabase/config.toml — the one place in this guide where a public function is correct, because the signature becomes the only thing authenticating the caller. That also makes its body attacker-controlled, so read it through the module’s bounded adapter rather than buffering whatever arrives: const handle = nextRouteHandler(verifier, callback, { maxBodyBytes: 1_000_000 }) takes a web-standard Request despite the name, so withSupabase({ auth: "none" }, (req) => handle(req)) wires it up. It streams the signed bytes, stops above your limit, and calls your callback only once the signature checks out. Verification covers those exact bytes, so re-serializing await req.json() changes them and the check will never pass. A valid signature still does not make a delivery unique: before the handler does anything durable, commit the verified webhook-id in the same database transaction as that work, await that commit before you return, and answer repeats with a 2xx. The Cloudflare Workers guide shows the same pattern on another edge runtime.
  • Attachments, scheduling, tags: all available on the same endpoint. See the API reference.
  • Moving the send to a Node server: the Express guide covers the Node imports, environment access, and route integration.
  • Building in a different AI tool? See v0 and Bolt.new.

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.