# Loops (/docs/v/0.6.1/adapters/loops)



The Loops adapter calls the [Loops transactional API](https://loops.so/docs/api-reference/send-transactional-email) over `fetch` — no extra dependency. It triggers a published transactional email by `transactionalId` for exactly one recipient and hands your message to the template as `dataVariables`.

<ProviderBadge adapter="loops" />

## Configure [#configure]

Create an API key in Loops with permission to send transactional email, then publish the transactional email and copy its `transactionalId`.

```ts title="lib/email.ts"
import { createEmailClient } from "@opencoredev/email-sdk";
import { loops } from "@opencoredev/email-sdk/loops";

export const email = createEmailClient({
  adapters: [
    loops({
      apiKey: process.env.LOOPS_API_KEY!,
      transactionalId: process.env.LOOPS_TRANSACTIONAL_ID!,
    }),
  ],
});
```

<TypeTable
  type="{
  apiKey: {
    description: &#x22;Loops API key.&#x22;,
    type: &#x22;string&#x22;,
    required: true,
  },
  transactionalId: {
    description: &#x22;Published transactional email to render. Must be non-empty — the adapter throws at construction otherwise.&#x22;,
    type: &#x22;string&#x22;,
    required: true,
  },
  baseUrl: {
    description: &#x22;Override the API origin, e.g. for a proxy.&#x22;,
    type: &#x22;string&#x22;,
    default: '&#x22;https://app.loops.so&#x22;',
  },
  fetch: {
    description: &#x22;Custom fetch implementation for tests or special runtimes.&#x22;,
    type: &#x22;typeof fetch&#x22;,
  },
}"
/>

## Send [#send]

The Loops template renders the email. The adapter merges `subject`, `html`, `text`, `from`, and every `metadata` key into `dataVariables` — your template decides which of them appear. Exactly one `to` recipient is allowed per send.

```ts
const result = await email.send({
  from: "Acme <hello@acme.com>",
  to: "user@example.com",
  subject: "Reset your password",
  html: "<p>Use the button below to reset your password.</p>",
  metadata: {
    firstName: "Ada",
    resetUrl: "https://acme.com/reset/abc123",
  },
});

console.log(result.id); // Loops id (or transactionalId) from the response
```

Attachments are supported and sent as `{ filename, contentType, data }` with base64-encoded content.

<Callout type="warn" title="Attachments need account enablement">
  Loops only accepts transactional attachments on accounts where the capability has been enabled —
  without it, sends with attachments fail at the API. Also note `cc`, `bcc`, `replyTo`, `headers`,
  and `tags` throw an `EmailValidationError` before any request; see
  [field support](/docs/v/0.6.1/adapters/field-support).
</Callout>

## Verify from the CLI [#verify-from-the-cli]

```bash
LOOPS_API_KEY="..." LOOPS_TRANSACTIONAL_ID="cm..." npx email-sdk doctor --adapter loops
```

```bash
LOOPS_API_KEY="..." LOOPS_TRANSACTIONAL_ID="cm..." npx email-sdk send \
  --adapter loops \
  --from "Acme <hello@acme.com>" \
  --to user@example.com \
  --subject "Loops smoke test" \
  --text "It works" \
  --dry-run
```

Drop `--dry-run` to trigger one real template send and confirm the API key and published transactional email are live.
