> ## Documentation Index
> Fetch the complete documentation index at: https://ahasend.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> AhaSend stores all message data in the EU. See /facts.
> The API base URL is https://api.ahasend.com. Send the API key in the Authorization: Bearer header. See /api-reference/authentication.
> Use send.ahasend.com for SMTP. Ports 25, 587 and 2525 use STARTTLS. Port 465 is not supported. See /smtp.
> For send-only integrations, create an API Key v2 under Credentials → Add and scope it to messages:send:{your-domain}. See /send-api/credentials and /api-reference/scopes.

# How to Add Email to Your App with Cursor

> Add transactional email with Cursor: an AhaSend project rule in .cursor/rules, a precise Agent prompt, a diff review checklist, and sandbox tests.

This guide shows how to send transactional email from an app built with Cursor with AhaSend, keep credentials on the server, and test a sandbox send.

Cursor Project Rules are MDC files under `.cursor/rules/`. Mark an AhaSend rule as always applied and Cursor includes it as context for every Agent (Chat) request, reducing the chance that the model reaches for a package that has never been published.

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](https://ahasend.com/docs/api-reference/scopes.md). See [send-only credentials](https://ahasend.com/docs/send-api/credentials.md) and [authentication](https://ahasend.com/docs/api-reference/authentication.md).

Keep credentials on the server. Use [sandbox mode](https://ahasend.com/docs/send-api/sandbox.md) 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](https://ahasend.com/docs/api-reference/webhooks/security.md) and [retry policy](https://ahasend.com/docs/api-reference/webhooks/retry-policy.md).

## Prerequisites

Cursor writes the code, but it can't create your account or verify your domain. Do these two things in the [AhaSend dashboard](https://dash.ahasend.com) first:

1. **Verify a sending domain.** Add the SPF, DKIM, and DMARC records from the [domain setup guide](https://ahasend.com/docs/domains.md). Unverified domains cannot send and produce an API error.
2. **Create an API key.** Follow the [API credentials guide](https://ahasend.com/docs/send-api/credentials.md). Scope it to `messages:send:{your-domain}` instead of `messages:send:all`: if the key ever leaks, the damage is limited to one domain's outbound mail. You'll also need your **account ID**.

You'll also need Node.js 22 or newer, the range the SDK supports. Put the key and account ID in your local environment. If you use a `.env` file, create it yourself, ensure it is ignored by git before adding values, and never paste its contents into Cursor or ask the agent to read or print it. Cursor routes AI requests through its backend, so choose the Privacy Mode appropriate for your project. For a Cursor Cloud Agent, add the key in the dashboard as a **Runtime Secret** rather than an Environment Variable: both load as environment variables, but only a Runtime Secret is redacted from the agent's tool call results, chat transcript, and commits. Never commit credentials or bake them into an environment snapshot.

## Step 1: Install the SDK

Do this before you prompt anything. An agent that finds `@ahasend/sdk` already in `package.json` has far less room to invent an alternative.

```bash npm theme={null}
npm install @ahasend/sdk
```

```bash pnpm theme={null}
pnpm add @ahasend/sdk
```

```bash yarn theme={null}
yarn add @ahasend/sdk
```

```bash bun theme={null}
bun add @ahasend/sdk
```

## Try a Sandbox Send

Run this short SDK check locally before adding the longer server integration below. Use a [verified sender](https://ahasend.com/docs/domains.md), a [send-only API key](https://ahasend.com/docs/send-api/credentials.md), 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`:

```js quick-send.mjs theme={null}
import { AhaSendClient } from "@ahasend/sdk";
try {
  const client = AhaSendClient.fromEnv();
  const result = await client.messages.send({
    from: { email: process.env.AHASEND_FROM },
    recipients: [{ email: "recipient@example.com" }],
    subject: "My first sandbox email",
    text_content: "Hello from AhaSend",
    sandbox: true,
  });
  const statuses = result.data.map((entry) => entry.status);
  console.log({ statuses });
  if (!statuses.length || statuses.some((s) => !["queued", "scheduled"].includes(s)))
    throw new Error("A recipient was rejected");
} catch {
  console.error("Sandbox send failed; check your credentials, sender and account.");
  process.exitCode = 1;
}
```

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.

## Step 2: Write a Cursor Rule File

Cursor reads Project Rules from `.mdc` files in a `.cursor/rules/` directory; the older single `.cursorrules` file is deprecated. Create an **Always** rule that pins down the facts the model is most likely to get wrong:

Use this frontmatter, followed by the shared rules below:

```yaml theme={null}
---
description: AhaSend email integration rules
alwaysApply: true
---
```

```markdown theme={null}
## AhaSend rules for AI coding agents

- Use @ahasend/sdk on the server (Node.js 22+, Deno, Bun, or a supported edge runtime).
- Create a send-only API Key v2 under Credentials → Add, scoped to messages:send:{your-domain}.
  Use messages:send:all only for sending from many domains. Management calls need a separate
  full key from Account Settings → API Keys with the needed scopes.
- Never put API keys in browser code, logs, prompts, or source control. Use server secrets.
- Use AhaSendClient with apiKey and accountId, then await client.messages.send(request, options).
  The v2 path is POST /v2/accounts/{account_id}/messages. from is an object, recipients is an array,
  and content fields are text_content and html_content. Use Create Conversation for CC/BCC.
- Authenticate send routes and take recipients and business-event IDs from trusted server data.
  Add request-size and rate limits. Await sending or commit a durable job before returning success.
- Require AHASEND_DELIVERY_MODE to be sandbox or live; reject missing or other values.
  Use a sandbox-mode credential for agent tests. Never decide delivery mode from NODE_ENV.
- Set sandbox from that explicit mode. Prefix stable business-event idempotency keys with the mode.
  Reuse a key only with the same request. Ordinary results last 24 hours; 5xx may re-execute.
- Inspect every result.data item: queued and scheduled were accepted; error was rejected and has
  a null id. Return a non-2xx result or fail the job when any recipient is rejected.
- Verify webhooks with @ahasend/sdk/webhooks before parsing or acting on untrusted data.
  Await verification. Use the exact raw body and the literal UTF-8 bytes of the full secret,
  prefix included; never base64-decode it. Bound the body to 30,000,000 bytes before reading it.
- Return 2xx for verified unknown events. Commit webhook-id and durable work atomically,
  acknowledge committed duplicates, and retry storage failures. The default clock tolerance
  is five minutes. Never log payloads, recipient addresses, signatures, secrets, or idempotency keys.
- Use a verified sender domain. Schedule the first attempt in the future and within 7 days.
  Encode binary attachment bytes as base64 yourself before setting base64: true.
- Check the API and SDK docs instead of inventing packages, fields, event names, or flags.
  Docs index: https://ahasend.com/docs/llms.txt
```

The rule is now included in model context for every Agent request. It does not reach Tab completion or Inline Edit (Cmd/Ctrl+K), so write the sending code through Agent rather than an inline edit, where none of this grounding applies. Rules guide model behavior rather than enforcing it, so confirm that **ahasend-email** is listed under Customize → Rules and review the resulting code against it.

> [!TIP]
>
> For a one-off, you can paste a documentation URL into the chat instead. AhaSend publishes a machine-readable index at `https://ahasend.com/docs/llms.txt` that lists every doc page. The rule file is better because it persists; the URL is fine when you just want one correct call.

## Step 3: Write a Specific Prompt

With context in place the prompt can be short, but name the file, the service, and the pattern to follow. Cursor's own guidance is that "add tests for auth.ts" produces worse results than a specific brief. Compare:

* Weak: "add email to my app"
* Better: "When `routes/signup.ts` creates a user, enqueue a durable welcome-email job with an immutable snapshot of the send payload in the same database transaction. Send that stored payload from a worker with the AhaSend SDK following the AhaSend rule, reuse the client from `lib/ahasend.ts`, and pass a stable idempotency key derived from the job ID. Do not detach a promise from the request."

For a multi-file change, a reusable email module plus the call site, switch the agent into **Plan mode** (Shift+Tab) first. It lets Agent research the codebase and propose an editable plan, so you can review the approach before asking it to build.

## Step 4: Review the Diff

Don't accept the diff just because it's green. First confirm that the signup transaction writes both the user and one uniquely keyed email job/outbox record; the request must not report success if that durable handoff fails. The sending side then has two parts: a single shared client:

```ts lib/ahasend.ts theme={null}
import { AhaSendClient } from "@ahasend/sdk";

export const ahasend = new AhaSendClient({
  apiKey: process.env.AHASEND_API_KEY!,
  accountId: process.env.AHASEND_ACCOUNT_ID!,
});
```

Then the send itself, with every recipient result inspected:

```ts workers/send-welcome.ts theme={null}
import { ahasend } from "../lib/ahasend.js";

const result = await ahasend.messages.send(
  {
    sandbox: true,
    from: { email: "hello@yourdomain.com", name: "Your App" },
    recipients: [{ email: job.payload.email, name: job.payload.name }],
    subject: "Welcome aboard",
    html_content: "<p>Thanks for signing up.</p>",
    text_content: "Thanks for signing up.",
  },
  { idempotencyKey: `sandbox-welcome-${job.id}` },
);

const rejected = result.data.filter((r) => r.status === "error");
if (rejected.length > 0) {
  // Retrying changes nothing: within the 24-hour window the same idempotency
  // key replays this same result. Record the rejection for follow-up instead
  // of completing the job as delivered.
  await markJobRejected(job.id, rejected.length);
}
```

Six things to confirm in whatever the agent wrote:

* **The handoff is durable.** The user and uniquely keyed welcome-email job are committed together, and a worker retries pending work. There must be no detached send promise in the request handler.
* **Retries reuse an immutable request.** The job stores the exact send payload, and its stable idempotency key is reused only for that payload. A retry beyond AhaSend's 24-hour result-retention window must reconcile rather than assume deduplication.
* **The real package.** The import must be `@ahasend/sdk`. If you see `import ahasend from "ahasend"` or a hand-rolled `fetch` to the REST API in a plain Node service, the model ignored your rule.
* **Every recipient checked.** Code that reads `result.data[0].status` and moves on will miss a suppressed address in a batch. The promise resolving means the request was accepted, not that every recipient was.
* **The key comes from the environment.** If the agent hardcoded the key or dropped it in a config file that git tracks, fix that before you commit.
* **No open relay.** If the agent added a route that sends to a recipient, subject, or body taken from the request, you have shipped a spam relay under your verified domain. Recipients and content must come from server-side state the authenticated caller owns, and the route must be authorized, size-capped, and rate limited.

> [!WARNING]
>
> If the agent scaffolds any frontend code, make sure the key never lands in a client-exposed variable (`NEXT_PUBLIC_`, `VITE_`, `PUBLIC_`, `NUXT_PUBLIC_`). Anything with those prefixes ships to the browser.

## Step 5: Test It in Sandbox Mode

Cursor can run the code it just wrote, but its test runs should not send real email to real inboxes. With [sandbox mode](https://ahasend.com/docs/send-api/sandbox.md), one field makes the message go through the full validation and queuing pipeline at no cost and with no delivery. Give the agent's environment a dedicated sandbox credential as well, so a generated test that forgets the field still can't deliver.

```ts theme={null}
const result = await ahasend.messages.send({
  from: { email: "hello@yourdomain.com", name: "Your App" },
  recipients: [{ email: "test@example.com", name: "Ada" }],
  subject: "Welcome aboard",
  text_content: "Thanks for signing up.",
  sandbox: true,
  sandbox_result: "bounce", // rehearse a failure path
});
```

Ask Cursor to "add an integration test that sends in sandbox mode and asserts every recipient came back queued" to exercise the real API path without delivering mail. To verify a webhook handler too, first configure the matching event subscription at a URL AhaSend can reach; expose a local handler through a tunnel, copy the dashboard secret to `AHASEND_WEBHOOK_SECRET` exactly including its prefix, and do not paste it into Cursor. Then swap `sandbox_result` to `"bounce"`, `"defer"`, `"fail"`, or `"suppress"` and assert on the durable result after the webhook arrives. Add this to your rule file: default `sandbox` to `true` everywhere except production, read the flag from an environment variable so going live is a config change rather than a code edit, and namespace idempotency keys by environment so a sandbox key is never reused for a live send.

## Going Further

For substitutions, attachments and scheduled sends, use the [sending guide](https://ahasend.com/docs/send-api/send-email.md). The [Node.js SDK](https://ahasend.com/docs/guides/nodejs-sdk.md) and [Go SDK](https://ahasend.com/docs/guides/go-sdk.md) references explain client options, per-recipient results and retries. Use [idempotency rules](https://ahasend.com/docs/api-reference/idempotency.md) when your application retries a business operation.

## Troubleshooting

> [accordion] Cursor keeps importing a package that doesn't exist
>
> The rule file isn't being applied, or it isn't specific enough. Confirm that it has a `.mdc` extension and `alwaysApply: true`, then check that **ahasend-email** appears with the right status under Customize → Rules. Install `@ahasend/sdk` before prompting; naming the package in the prompt itself also helps for a single request.

> [accordion] The generated code throws AhaSendConfigurationError on startup
>
> Common causes are a missing or malformed API key, a non-UUID account ID, or construction in a browser-like runtime. If Cursor put the client in a client component, move the send to a server route. The constructor check is only a backstop; keeping credentials out of browser bundles is what protects them.

> [accordion] 401 AhaSendAuthenticationError in the agent's test run
>
> The API key is missing, malformed, or revoked. Check that your `.env` is loaded in the terminal Cursor is using (`node --env-file=.env`), and that the key exists in your [dashboard](https://dash.ahasend.com/account/-/transactional/credentials) with the `messages:send:all` or domain-scoped send permission.

> [accordion] 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, and see [the quickstart](https://ahasend.com/docs/quickstart.md) if you haven't added one yet.

Every endpoint the SDK exposes is in the [API reference](https://ahasend.com/docs/api-reference.md).

## Related Guides

* Before sending: [verify a domain](https://ahasend.com/docs/domains.md) and [create a send-only key](https://ahasend.com/docs/send-api/credentials.md).
* Other ways to send: [REST API](https://ahasend.com/docs/send-api/send-email.md), [SMTP](https://ahasend.com/docs/smtp.md), [CLI quickstart](https://ahasend.com/docs/cli/quickstart.md), [Node.js SDK](https://ahasend.com/docs/guides/nodejs-sdk.md) and [Go SDK](https://ahasend.com/docs/guides/go-sdk.md).
* Request rules: [API authentication](https://ahasend.com/docs/api-reference/authentication.md), [scopes](https://ahasend.com/docs/api-reference/scopes.md), [idempotency](https://ahasend.com/docs/api-reference/idempotency.md), [errors](https://ahasend.com/docs/api-reference/errors.md) and [rate limits](https://ahasend.com/docs/api-reference/rate-limits.md).
* Testing and events: [sandbox mode](https://ahasend.com/docs/send-api/sandbox.md), [CLI webhook testing](https://ahasend.com/docs/cli/webhook-testing.md), [event payloads](https://ahasend.com/docs/api-reference/webhooks.md), [signature verification](https://ahasend.com/docs/api-reference/webhooks/security.md) and [delivery retries](https://ahasend.com/docs/api-reference/webhooks/retry-policy.md).
* Data and limits: [retention](https://ahasend.com/docs/retention.md), [tracking](https://ahasend.com/docs/tracking/open-tracking.md) and [plans and feature availability](https://ahasend.com/docs/facts.md#plans-and-features).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.