Cookbook
Copy-pasteable code recipes for common patterns.
Copy-pasteable recipes for common email, SMS, and workflow patterns using the Wraps TypeScript SDKs.
Basic send with plain HTML or a React Email component. Uses the WrapsEmail client with automatic AWS credential resolution.
import { WrapsEmail } from "@wraps.dev/email";const email = new WrapsEmail({ region: "us-east-1" });// Plain HTMLawait email.send({ from: { email: "hello@yourapp.com", name: "YourApp" }, to: user.email, subject: "Welcome to YourApp!", html: ` <h1>Welcome, ${user.name}!</h1> <p>Your account is ready. Here is what to do next:</p> <a href="https://yourapp.com/getting-started">Get started</a> `,});// Or use a React Email componentimport WelcomeEmail from "./emails/welcome";await email.send({ from: { email: "hello@yourapp.com", name: "YourApp" }, to: user.email, subject: "Welcome to YourApp!", react: <WelcomeEmail name={user.name} />,});Send unique content to up to 100 recipients in a single API call using sendBatch. Each entry can have its own subject and body.
import { WrapsEmail } from "@wraps.dev/email";const email = new WrapsEmail({ region: "us-east-1" });// Send unique content to each recipient (max 100 per batch)const result = await email.sendBatch({ from: { email: "newsletter@yourapp.com", name: "YourApp" }, entries: subscribers.map((sub) => ({ to: sub.email, subject: `${sub.name}, here is your weekly digest`, html: renderNewsletter({ name: sub.name, articles }), })), tags: { campaign: "weekly-digest" },});console.log(`Sent: ${result.successCount}, Failed: ${result.failureCount}`);// Check individual resultsfor (const entry of result.results) { if (entry.status === "failure") { console.error(`Failed for index ${entry.index}: ${entry.error}`); }}Create SES templates with {{variable}} placeholders, then send personalized emails without rebuilding HTML each time. Use sendBulkTemplate for up to 50 recipients.
import { WrapsEmail } from "@wraps.dev/email";const email = new WrapsEmail({ region: "us-east-1" });// 1. Create a template with {{variable}} placeholdersawait email.templates.create({ name: "order-confirmation", subject: "Order #{{orderId}} confirmed", html: ` <h1>Thanks for your order, {{name}}!</h1> <p>Order #{{orderId}} has been confirmed.</p> <p>Total: ${{total}}</p> `,});// 2. Send using the templateawait email.sendTemplate({ from: "orders@yourapp.com", to: customer.email, template: "order-confirmation", templateData: { name: customer.name, orderId: order.id, total: order.total.toFixed(2), },});// 3. Bulk send to up to 50 recipients with per-recipient dataawait email.sendBulkTemplate({ from: "orders@yourapp.com", template: "order-confirmation", destinations: customers.map((c) => ({ to: c.email, templateData: { name: c.name, orderId: c.orderId, total: c.total.toFixed(2), }, })),});Next.js route handler that verifies the Wraps webhook signature and processes bounce, complaint, and delivery events.
import crypto from "crypto";import { type NextRequest, NextResponse } from "next/server";const WEBHOOK_SECRET = process.env.WRAPS_WEBHOOK_SECRET!;export async function POST(request: NextRequest) { // Verify the webhook signature const signature = request.headers.get("x-wraps-signature"); if ( !signature || !crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(WEBHOOK_SECRET), ) ) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { event, detail, messageId } = await request.json(); switch (event) { case "Bounce": { const { bounceType, bouncedRecipients } = detail.bounce; if (bounceType === "Permanent") { // Hard bounce: remove these addresses from your mailing list for (const r of bouncedRecipients) { await db.contacts.update({ where: { email: r.emailAddress }, data: { bounced: true, bouncedAt: new Date() }, }); } } break; } case "Complaint": { // ISP complaint: unsubscribe immediately for (const r of detail.complaint.complainedRecipients) { await db.contacts.update({ where: { email: r.emailAddress }, data: { unsubscribed: true, unsubscribedAt: new Date() }, }); } break; } case "Delivery": console.log(`Email ${messageId} delivered successfully`); break; } return NextResponse.json({ received: true });}Query delivery, open, click, and bounce events from DynamoDB using the events API. Requires the historyTableName config option.
import { WrapsEmail } from "@wraps.dev/email";const email = new WrapsEmail({ region: "us-east-1", historyTableName: "wraps-email-events", // DynamoDB table deployed by CLI});// Get events for a specific email by messageIdconst status = await email.events!.get("01234567-89ab-cdef-0123-456789abcdef");if (status) { console.log(`Status: ${status.status}`); // "sent" | "delivered" | "opened" | "clicked" | "bounced" | "complained" | "suppressed" for (const event of status.events) { console.log(` ${event.type} at ${new Date(event.timestamp).toISOString()}`); }}// List recent emails with filteringconst { emails, nextToken } = await email.events!.list({ accountId: "123456789012", startTime: new Date(Date.now() - 24 * 60 * 60 * 1000), // last 24 hours maxResults: 20,});for (const e of emails) { console.log(`${e.subject} -> ${e.to.join(", ")} [${e.status}]`);}Check, add, remove, and list suppressed email addresses on the SES account-level suppression list.
import { WrapsEmail } from "@wraps.dev/email";const email = new WrapsEmail({ region: "us-east-1" });// Check if an address is suppressedconst entry = await email.suppression.get("bounced@example.com");if (entry) { console.log(`Suppressed: ${entry.reason} on ${entry.lastUpdated}`);}// Manually suppress an addressawait email.suppression.add("bad-actor@example.com", "COMPLAINT");// Remove from suppression list (e.g. after user re-confirms)await email.suppression.remove("reactivated@example.com");// List all suppressed addresses with filtersconst { entries, nextToken } = await email.suppression.list({ reason: "BOUNCE", startDate: new Date("2024-01-01"), maxResults: 100,});for (const e of entries) { console.log(`${e.email} - ${e.reason} - ${e.lastUpdated}`);}Declarative workflow definition with delays, conditions, and branching. Define in wraps/workflows/*.ts for full TypeScript intellisense.
import { defineWorkflow, sendEmail, delay, condition, exit, updateContact, waitForEmailEngagement,} from "@wraps.dev/client";export default defineWorkflow({ name: "User Onboarding", description: "3-email drip sequence for new signups", trigger: { type: "contact_created" }, defaults: { from: "hello@yourapp.com", fromName: "YourApp", }, steps: [ // Day 0: Welcome email sendEmail("welcome", { template: "welcome" }), updateContact("mark-welcomed", { updates: [{ field: "welcomeEmailSent", operation: "set", value: true }], }), // Day 1: Check if they activated delay("wait-1-day", { days: 1 }), condition("check-activation", { field: "contact.hasActivated", operator: "equals", value: true, branches: { yes: [exit("activated")], no: [ sendEmail("tips", { template: "getting-started-tips" }), // Day 3: Final nudge if still not activated delay("wait-2-days", { days: 2 }), condition("check-activation-again", { field: "contact.hasActivated", operator: "equals", value: true, branches: { yes: [exit("activated-late")], no: [sendEmail("last-chance", { template: "activation-reminder" })], }, }), ], }, }), ],});List, read, reply to, and forward inbound emails stored in S3. Requires the inboxBucketName config option.
import { WrapsEmail } from "@wraps.dev/email";const email = new WrapsEmail({ region: "us-east-1", inboxBucketName: "wraps-email-inbox-123456789012", // S3 bucket deployed by CLI});// List recent inbound emailsconst { emails: summaries, nextToken } = await email.inbox!.list({ maxResults: 10,});for (const summary of summaries) { console.log(`${summary.emailId} - ${summary.lastModified}`);}// Get full email content by IDconst message = await email.inbox!.get(summaries[0].emailId);console.log(`From: ${message.from.name} <${message.from.address}>`);console.log(`Subject: ${message.subject}`);console.log(`Date: ${message.date}`);console.log(`Body: ${message.text ?? message.html}`);// Access attachmentsfor (const att of message.attachments) { const url = await email.inbox!.getAttachment(message.emailId, att.id); console.log(`Attachment: ${att.filename} (${att.contentType}) -> ${url}`);}// Reply to the email (preserves threading headers)await email.inbox!.reply(message.emailId, { from: "support@yourapp.com", html: "<p>Thanks for reaching out! We will get back to you shortly.</p>",});// Forward to another addressawait email.inbox!.forward(message.emailId, { from: "support@yourapp.com", to: "team@yourapp.com",});Check opt-out status before sending, handle OptedOutError gracefully, and manage the opt-out list programmatically.
import { WrapsSMS, OptedOutError } from "@wraps.dev/sms";const sms = new WrapsSMS({ region: "us-east-1" });// Check opt-out status before sendingconst isOptedOut = await sms.optOuts.check("+14155551234");if (isOptedOut) { console.log("User has opted out, skipping send");} else { const result = await sms.send({ to: "+14155551234", message: "Your order has shipped! Track it here: https://yourapp.com/track/123", messageType: "TRANSACTIONAL", }); console.log(`Sent: ${result.messageId} (${result.segments} segments)`);}// Handle opt-out errors during sendtry { await sms.send({ to: "+14155559999", message: "Weekly sale: 20% off everything!", messageType: "PROMOTIONAL", });} catch (error) { if (error instanceof OptedOutError) { console.log(`${error.phoneNumber} has opted out`); // Mark as opted out in your database } throw error;}// Manage opt-out list directlyawait sms.optOuts.add("+14155550000"); // Manually opt out a numberawait sms.optOuts.remove("+14155550000"); // Remove opt-out (re-consent received)const optedOut = await sms.optOuts.list();for (const entry of optedOut) { console.log(`${entry.phoneNumber} opted out at ${entry.optedOutAt}`);}