> ## 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.

# How to Add Email to Your App with GitHub Copilot

> Delegate an AhaSend transactional email integration to GitHub's Copilot cloud agent, then review the pull request for correctness and credential safety.

GitHub Copilot cloud agent works asynchronously and can open a pull request for your review. It is limited to its working branch and a human must review and merge its pull request. It cannot access Actions, Codespaces, or Dependabot secrets, but it can access any **Agents** secrets that an administrator explicitly gives it.

## Prerequisites

Ensure Copilot cloud agent is enabled for the target repository and that you have write access to it. Without both, Copilot cannot be assigned the issue.

The agent can write the integration, but it cannot verify your domain or create your credentials. Do that 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](/docs/domains).
2. **Create an API key.** Follow the [API credentials guide](/docs/send-api/credentials) and scope it to `messages:send:{your-domain}` instead of `messages:send:all`. Note your **account ID**.

Keep the deployed application's API key in your hosting platform's secret store. You do not need to expose it to Copilot for the agent to implement and test the integration.

## Step 1: Install the SDK and Commit It

Install the official SDK and commit the manifest and lockfile before assigning the task.

<CodeGroup>
  ```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
  ```
</CodeGroup>

This gives the agent an unambiguous dependency and lets its normal package install reproduce your dependency graph.

## Step 2: Write the Issue

Give the agent the API contract, security boundaries, and acceptance tests instead of leaving it to infer them. Open an issue, use a prompt like this, and assign it to Copilot:

> **Title:** Add order-confirmation email via AhaSend
>
> **Body:** When an order is created, send a confirmation email using the official AhaSend TypeScript SDK, `@ahasend/sdk`, which is already a dependency. Do not hand-roll HTTP calls or add another email package.
>
> * Validate `AHASEND_API_KEY`, `AHASEND_ACCOUNT_ID`, and the sender address at startup. Construct one `AhaSendClient` at module scope with `apiKey` and `accountId`.
> * Send with `client.messages.send({ from, recipients, subject, html_content, text_content })`. Body fields are snake\_case.
> * Pass a stable, server-derived business identifier as `options.idempotencyKey` so an application-level retry does not create a second email. Reuse a key only for the same exact payload, and include the delivery mode in the key: keys live in one account-wide namespace shared by sandbox and live traffic, and the API matches them against a hash of the request body.
> * Treat the response as multi-status: inspect every entry in `result.data`. A recipient can have `status: "error"` and a null `id` even though the promise resolved.
> * Catch `AhaSendAPIError` from `@ahasend/sdk` and branch on structured fields, not message text. Logs may include only aggregate counts, HTTP status, SDK error code, and request ID. Never log addresses, content, bodies, headers, idempotency keys, or whole request, response, event, client, or error objects.
> * Keep API keys and all AhaSend calls in server-only code. Authenticate and authorize the caller, validate input, and load the order and recipient from server-authoritative storage; do not trust a client-supplied email address or order ID. Bound the request body and rate-limit the triggering route per authenticated caller so a looping or hostile client cannot mail-bomb a customer. Escape any untrusted value before inserting it into HTML.
> * Unit tests must mock the API boundary. Any explicitly approved live integration test must set `sandbox: true` and assert every recipient result.
> * If the task involves webhooks, import `WebhookVerifier` from `@ahasend/sdk/webhooks`, verify the exact raw body, and `await verifier.parse(headers, rawBody)`. Enforce a request-size limit, and commit the verified `webhook-id` and the durable work it guards in one transaction before any side effect, then run that work from a worker outside the request. A bare unique-ID insert loses the event if the process dies after the insert but before processing, and timestamp verification alone does not prevent replays.

Copilot cloud agent also supports repository-wide instructions in `.github/copilot-instructions.md`. Put durable rules such as server-only credentials, stable idempotency keys, multi-status checks, and safe logging there so later email tasks inherit them.

## Step 3: Review the Pull Request

When the agent finishes, it requests your review. For a new task, GitHub limits it to a newly created `copilot/` branch, and Copilot cannot approve or merge the pull request. Review the diff and the agent's session log before allowing workflows or merging.

### Did the key stay server-side?

Scan the entire diff for hardcoded keys and for client-exposed variables such as `NEXT_PUBLIC_` or `VITE_`. Verify that the call is reachable only after your application's normal authentication and authorization checks, that the recipient comes from trusted server-side data, and that the route bounds its request body and rate-limits each authenticated caller. A send path that any caller can reach, or reach without a ceiling, is a mail relay regardless of how correct the SDK usage is. If any of those checks fail, request a correction with an `@copilot` comment on the pull request.

### Is it using the SDK safely?

A representative server-side service looks like this:

```ts lib/send-order-confirmation.ts theme={null}
import { AhaSendAPIError, AhaSendClient } from "@ahasend/sdk";

function requiredEnv(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing required environment variable: ${name}`);
  return value;
}

function deliveryMode(): "sandbox" | "live" {
  const value = requiredEnv("AHASEND_DELIVERY_MODE");
  if (value !== "sandbox" && value !== "live") {
    throw new Error("AHASEND_DELIVERY_MODE must be sandbox or live");
  }
  return value;
}

const client = new AhaSendClient({
  apiKey: requiredEnv("AHASEND_API_KEY"),
  accountId: requiredEnv("AHASEND_ACCOUNT_ID"),
});

const mode = deliveryMode();
const sender = requiredEnv("AHASEND_FROM_ADDRESS");

type ConfirmedOrder = Readonly<{
  id: string;
  customerEmail: string;
}>;

export async function sendOrderConfirmation(order: ConfirmedOrder): Promise<void> {
  try {
    const result = await client.messages.send(
      {
        from: { email: sender, name: "Your Store" },
        recipients: [{ email: order.customerEmail }],
        subject: "Your order is confirmed",
        text_content: "Thanks—your order is confirmed.",
        html_content: "<p>Thanks—your order is confirmed.</p>",
        sandbox: mode === "sandbox",
      },
      { idempotencyKey: `order-confirmation-${mode}-${order.id}` },
    );

    const rejected = result.data.filter((entry) => entry.status === "error");
    if (rejected.length > 0) {
      console.warn("AhaSend rejected recipients", { count: rejected.length });
      throw new Error("Order confirmation was not accepted");
    }
  } catch (error) {
    if (error instanceof AhaSendAPIError) {
      console.error("AhaSend request failed", {
        status: error.status,
        code: error.code,
        requestId: error.requestId,
      });
      throw new Error("Email provider request failed");
    }
    throw error;
  }
}
```

The route or controller calling this function still has to authenticate the request, authorize access to the order, and load the order from your database. Keeping the client at module scope also preserves its shared configuration and any enabled local rate pacing across calls.

### Does it handle retries and multi-status results?

The SDK generates an idempotency key for its own retry loop, but a stable caller-provided key is needed when your application may invoke the business operation again. Confirm the PR derives that key from a trusted immutable ID and never logs it.

Keys are scoped to the account, not to a delivery mode, and the API matches a reused key against a hash of the request body. A key built from the order ID alone therefore collides between the sandbox run of Step 4 and the first live send for the same order: the bodies differ only in the `sandbox` field, so the live request is rejected as an idempotency mismatch instead of being sent. Including the mode in the key, as above, keeps the two namespaces apart.

A resolved send returns one entry per recipient. Do not accept code that checks only `result.data[0]`, treats resolution as unconditional success, or exposes recipient details in a response or log.

## Step 4: Test Without Giving the Agent a Production Credential

Prefer mocked tests: Copilot can validate request construction, error handling, multi-status behavior, and idempotency without any AhaSend credential.

If you deliberately want the agent to call the real AhaSend API in sandbox mode, create a dedicated, domain-scoped API key that you can revoke immediately afterward. In the repository, go to **Settings → Secrets and variables → Agents**, store it as `AHASEND_API_KEY`, store the account ID as an Agents secret or variable, and add Agents variables for `AHASEND_FROM_ADDRESS` and `AHASEND_DELIVERY_MODE`. The sender must use the verified domain, and the delivery mode must be `sandbox`. A sandbox send validates the request without delivering email; use `sandbox_result` values such as `deliver`, `bounce`, `defer`, `fail`, and `suppress` to exercise different outcomes.

This is an ordinary API key, not a sandbox-restricted credential. Agents secrets are exposed as environment variables to scripts and tools Copilot runs, and `sandbox: true` is only a request field. Only provide the key if you accept that risk. Keep the agent firewall enabled and explicitly allow `api.ahasend.com` if the request is blocked; do not disable the firewall just to make the test pass.

<Warning>
  Never give the agent a production credential. GitHub masks Agents secret values in session logs, but masking is not an authorization boundary and does not prevent a process from using the credential.
</Warning>

Remove the temporary Agents credential after the test. Your deployed application should use a separate key from its hosting platform's secret store.

## Step 5: Merge and Send a Real Email

By default, GitHub Actions workflows do not run automatically when Copilot pushes. First inspect the diff, especially changes under `.github/workflows/`, then use **Approve and run workflows** and confirm the required checks actually completed. Your own approval does not count toward required approvals on a Copilot pull request, so obtain another approval if the repository requires one.

After review and CI pass, merge through your normal protected-branch process. In the deployed environment:

* Set `AHASEND_API_KEY`, `AHASEND_ACCOUNT_ID`, and `AHASEND_FROM_ADDRESS` in the hosting platform's secret store.
* Set `AHASEND_DELIVERY_MODE` explicitly to `live`; fail closed when it is absent or invalid.
* Send one confirmation to an address you control and verify the result and delivery.

## Going Further

* **Exercise sandbox outcomes:** integration tests can set `sandbox_result` to `bounce` or `suppress` to exercise the corresponding delivery and webhook paths. Assert multi-status send results separately.
* **Templating and webhooks:** per-recipient `substitutions` support personalized batches, and `@ahasend/sdk/webhooks` verifies delivery-event signatures. Preserve the raw body, and commit `webhook-id` together with the durable work in one transaction before any side effect.
* Same grounding technique, different instruction file: [Cursor](/docs/guides/cursor), [Claude](/docs/guides/claude), and [Windsurf](/docs/guides/windsurf).
* For complete framework integrations, see [Next.js](/docs/guides/nextjs), [Express](/docs/guides/express), or [Remix](/docs/guides/remix).

## Troubleshooting

<AccordionGroup>
  <Accordion title="The PR hand-rolls HTTP calls instead of using the SDK">
    Confirm the dependency and lockfile were committed before the task started. Comment `@copilot replace the raw HTTP calls with the @ahasend/sdk client`, then add the constraint to `.github/copilot-instructions.md`.
  </Accordion>

  <Accordion title="The integration test gets an authentication error">
    Confirm that any deliberately provided credential is stored under **Secrets and variables → Agents**, not under Actions, Codespaces, or Dependabot. Check its scope and revocation state without printing it. Prefer a mocked test if external access is unnecessary.
  </Accordion>

  <Accordion title="The sandbox request is blocked">
    Copilot cloud agent's firewall may block the AhaSend API host. Keep the firewall enabled and add `api.ahasend.com` to its allowlist only if you have accepted the credential risk. Otherwise, use a mocked test.
  </Accordion>

  <Accordion title="The generated code throws in a browser">
    The API client belongs in server-only code. Move the AhaSend client and send call behind an authenticated server route or controller, and ensure no client component imports that module.
  </Accordion>
</AccordionGroup>

Every SDK endpoint is documented in the [API reference](/docs/api-reference).
