Skip to content
Back to skills

Transactional Email Integration

ASecurity

Integrate an Email Service Provider (SES, SendGrid, Postmark, Resend, Mailgun) for transactional sending with idempotent sends, signature-verified and idempotent webhook event handling (delivered/bounce/complaint/open), and retry/rate-limit discipline. Reach for this when the user says "wire up <ESP>", "send receipts/password resets", or "handle delivery webhooks". Used by `email-sending-engineer` (primary).

  • 7 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 23, 2026
ai-agentsrustgonoderailsapi

Works with

  • api

Security analysis

A100/100

Scanned September 23, 2026

npx -y skills add mcorbett51090/RavenClaude --skill transactional-email-integration --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Transactional Email Integration?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Transactional Email Integration
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mcorbett51090-transactional-email-integration/badge)](https://www.skillsdirectory.com/skills/mcorbett51090-transactional-email-integration)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: transactional-email-integration
description: Integrate an Email Service Provider (SES, SendGrid, Postmark, Resend, Mailgun) for transactional sending with idempotent sends, signature-verified and idempotent webhook event handling (delivered/bounce/complaint/open), and retry/rate-limit discipline. Reach for this when the user says "wire up <ESP>", "send receipts/password resets", or "handle delivery webhooks". Used by `email-sending-engineer` (primary).
---

# Skill: transactional-email-integration

> **Invoked by:** `email-sending-engineer` (primary).
>
> **When to invoke:** "integrate <ESP>"; "send a transactional email"; "handle the bounce/complaint webhook"; "our sends double under retry".
>
> **Output:** a send path with idempotency + a verified, idempotent webhook handler that closes the feedback loop into suppression.

## Procedure

1. **Pick the ESP by the seam, not the logo.** Transactional-first (Postmark, Resend, SES) vs marketing/blended (SendGrid, Mailgun). Map against [`../../knowledge/esp-capability-map-2026.md`](../../knowledge/esp-capability-map-2026.md). Wrap whichever you pick behind a small `EmailProvider` interface so it's swappable and testable.
2. **Make the send idempotent.** Derive a stable idempotency key from the business event (`order-123-receipt`), not a UUID generated at call time. Use the ESP's idempotency header where it exists; otherwise record "sent <key>" in your store and check before sending. A retried call must **not** produce a second email.
3. **Send with timeouts + bounded retries.** Treat the ESP call like any external dependency: short timeout, retry on 429/5xx with exponential backoff + jitter, and a dead-letter path. Do **not** retry a 4xx validation error.
4. **Verify the webhook, then process.** Verify the provider's signature (SES SNS signature, SendGrid/Postmark signing secret) **before** trusting the payload — an unverified webhook endpoint is an open suppression-injection hole.
5. **Make the handler idempotent and order-independent.** Providers deliver events **at least once** and out of order. Key on the event id; an already-seen event is a no-op. A `bounce` arriving before `delivered`, or twice, must not corrupt state.
6. **Close the loop into suppression.** A `bounce` (hard) or `complaint` event updates the suppression list ([`../bounce-complaint-suppression/SKILL.md`](../bounce-complaint-suppression/SKILL.md)) that the next send checks. This is the whole reason to handle webhooks.

## Worked example (language-agnostic; Node shown)

```js
// send — idempotent on a business key, behind a provider interface
async function sendReceipt(orderId, to) {
  const key = `order-${orderId}-receipt`;
  if (await sends.has(key)) return; // already sent — idempotent
  await provider.send({
    to,
    template: "receipt",
    idempotencyKey: key, // provider-side guard where supported
    headers: { "List-Unsubscribe": "<mailto:unsub@example.com>" },
  });
  await sends.put(key);
}

// webhook — verify, then idempotent process
function handleEvent(req) {
  if (!provider.verifySignature(req)) return res.status(401);
  for (const e of provider.parse(req.body)) {
    if (events.seen(e.id)) continue; // at-least-once delivery — dedupe
    events.mark(e.id);
    if (e.type === "bounce" && e.hard) suppression.add(e.email, "hard_bounce");
    if (e.type === "complaint") suppression.add(e.email, "complaint");
  }
  return res.status(200); // ack so the provider stops retrying
}
```

## Guardrails

- Never `await provider.send()` without an idempotency guard — retries and double-submits will double-send.
- Never trust a webhook body before verifying its signature; that endpoint can poison your suppression list.
- Return `2xx` only after you've durably recorded the event — a `200` tells the provider to stop retrying.
- Keep the API key in the secret store, never in the template repo (the hook flags a leaked key).

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…