# Node.js with Express

> Send email with Couryo from a Node.js Express API, with idempotency, error handling and a signed webhook read from the raw body.

Source: https://couryo.com/en/docs/express

An Express server that sends the welcome email on sign-up and receives delivery events by webhook, with the [Node SDK](https://couryo.com/en/docs/sdks.md).

## 1. Install

```bash title="Terminal"
npm install express @couryo/node
```

```ini title=".env"
COURYO_API_KEY=ck_test_...
COURYO_WEBHOOK_SECRET=whsec_...
```

Run it with `node --env-file=.env server.js` (Node 20.6 or newer reads `.env` on its own).

## 2. Send on sign-up

```js title="server.js"
import express from "express";
import { Couryo, CouryoError, verifyWebhook, WebhookVerificationError } from "@couryo/node";

const app = express();
const couryo = new Couryo({ language: "en" }); // reads COURYO_API_KEY

app.post("/signup", express.json(), async (req, res) => {
  const { email, name } = req.body;
  const user = await createUser({ email, name }); // your function

  try {
    const sent = await couryo.emails.send(
      {
        from: "Example <hello@example.com>",
        to: email,
        subject: `Welcome, ${name}!`,
        html: `<p>Hi ${name}, your account is ready.</p>`,
        text: `Hi ${name}, your account is ready.`,
        tags: { kind: "welcome" },
      },
      // If this route runs twice for the same user, the email goes out once.
      { idempotencyKey: `welcome-${user.id}` },
    );
    res.status(201).json({ user: user.id, email_id: sent.id });
  } catch (e) {
    if (e instanceof CouryoError) {
      // e.code is stable: domain_not_verified, daily_limit_reached, recipient_suppressed...
      console.error(e.code, e.message, e.requestId);
      return res.status(201).json({ user: user.id, warning: "account created, email not sent" });
    }
    throw e;
  }
});
```

The SDK already retries network errors, `5xx` and `rate_limited`, always with the same `Idempotency-Key`. What reaches the `catch` is what retrying will not fix.

## 3. Receive events by webhook

The signature covers the exact body. Use `express.raw` on this route, never `express.json`:

```js title="server.js"
app.post("/webhooks/couryo", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    event = verifyWebhook({ secret: process.env.COURYO_WEBHOOK_SECRET, headers: req.headers, body: req.body });
  } catch (e) {
    if (e instanceof WebhookVerificationError) return res.status(400).send("invalid signature");
    throw e;
  }

  switch (event.type) {
    case "delivered":
      break;
    case "bounced":
    case "complained":
      // Couryo already suppressed the address when it should. Flag it in your database if you like:
      // event.data.recipient, event.data.explanation
      break;
  }
  res.sendStatus(204);
});

app.listen(3000);
```

If you have a global `express.json()`, register it **after** this route, or keep it only on the routes that need it (as above). Answer fast: any `2xx` confirms; slow work goes to your own queue.

## 4. Batch, templates and scheduling

```js title="server.js"
// Up to 100 emails in one call, one result per item, in the same order.
const { data } = await couryo.emails.batch(
  customers.map((c) => ({ from: "Store <hello@example.com>", to: c.email, template: "delivery-notice", variables: { name: c.name } })),
);

// Schedule for one hour from now (up to 30 days).
await couryo.emails.send({ from: "Store <hello@example.com>", to: "ana@example.com", subject: "Reminder", text: "...", scheduled_at: new Date(Date.now() + 3600_000).toISOString() });
```

## Over SMTP (Nodemailer)

If the project already uses Nodemailer, change only the transport:

```js title="mailer.js"
import nodemailer from "nodemailer";

export const transport = nodemailer.createTransport({
  host: "smtp.couryo.com",
  port: 587, // STARTTLS; or 465 with secure: true
  secure: false,
  requireTLS: true,
  auth: { user: "couryo", pass: process.env.COURYO_API_KEY },
});
```

Both paths produce the same timeline and the same webhooks. See [SMTP](https://couryo.com/en/docs/smtp.md) for the reply codes.
