.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.
Prerequisites
Cursor writes the code, but it can’t create your account or verify your domain. Do these two things in the AhaSend dashboard first:- Verify a sending domain. Add the SPF, DKIM, and DMARC records from the domain setup guide. Unverified domains cannot send and produce an API error.
- Create an API key. Follow the API credentials guide. Scope it to
messages:send:{your-domain}instead ofmessages:send:all: if the key ever leaks, the damage is limited to one domain’s outbound mail. You’ll also need your account ID.
.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.
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:
.cursor/rules/ahasend-email.mdc
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.tscreates 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 fromlib/ahasend.ts, and pass a stable idempotency key derived from the job ID. Do not detach a promise from the request.”
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:lib/ahasend.ts
workers/send-welcome.ts
- 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 seeimport ahasend from "ahasend"or a hand-rolledfetchto the REST API in a plain Node service, the model ignored your rule. - Every recipient checked. Code that reads
result.data[0].statusand 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.
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, 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.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
- Apply the pattern to other services: any external API the model has only half-learned benefits from the same three steps, a rule file with verified facts, a specific prompt, and verification against the real API.
- Rehearse the failure cases: with a configured, reachable webhook, ask Cursor to write tests that set
sandbox_resulttobounceandsuppress, so you see how your code handles a rejection before a real recipient does. - Templating: per-recipient
substitutionswith{{ variable }}in the subject or body turns one send call into a personalised batch. Add it to your rules file and the agent will reach for it. - Webhooks:
@ahasend/sdk/webhooksverifies signatures and parses events, but your application still owns bounded raw-body routing, atomicwebhook-iddeduplication, durable processing, and idempotent side effects. - The same grounding trick applies in every AI coding tool, with a different file name: Claude uses
CLAUDE.md, GitHub Copilot uses.github/copilot-instructions.md, and Windsurf uses.windsurf/rules/. - Once the agent has written the code, the framework guides show what the finished integration looks like end to end: Express, Next.js, and Fastify.
Troubleshooting
Cursor keeps importing a package that doesn't exist
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.The generated code throws AhaSendConfigurationError on startup
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.
401 AhaSendAuthenticationError in the agent's test run
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 with the messages:send:all or domain-scoped send permission.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, and see the quickstart if you haven’t added one yet.
