# Deckle documentation — full text > Every documentation page, concatenated. Index: https://app.getdeckle.com/llms.txt ======================================================================== URL: https://app.getdeckle.com/docs/introduction Section: Get started ======================================================================== Get started # Deckle documentation Deckle is a design-first email platform — the developer experience of a modern email API with the audience, templates, and automations of a marketing tool. Send transactional and marketing email from your own domain. Everything you send goes out from a domain you own and verify, so your reputation stays yours. Start with a single API call, then grow into contacts, campaigns, and event-driven automations without changing tools. ``` import { Deckle } from "@deckle/sdk"; const deckle = new Deckle(process.env.DECKLE_API_KEY!); await deckle.emails.send({ from: "Acme ", to: "jane@example.com", subject: "Welcome to Acme 👋", html: "

Hey Jane

Thanks for joining.

", }); ``` New to Deckle? Follow the Quickstart to send your first email in under five minutes, then verify a domain to start sending to real inboxes.Built for LLMs. For all documentation in an index, see llms.txt. To read the full text of the documentation, see llms-full.txt. ## Start building Jump straight to what you need. QuickstartCreate a key and send your first email with the SDK, HTTP, or SMTP. Send emailTransactional sends, templates, variables, attachments, and idempotency. AudienceManage contacts, track events, build segments, and handle suppressions. AutomationsTrigger multi-step email flows from events and subscriptions. Domains & deliverabilityVerify a domain with DKIM, SPF, and DMARC to protect your reputation. API referenceEvery endpoint, parameter, and response for the Deckle REST API. ## What you can build Deckle covers the full lifecycle of the email you send — from a one-off password reset to a recurring newsletter to a lifecycle automation that reacts to what your users do. Transactional emailReceipts, magic links, and alerts over the API, SMTP, or SDK. Marketing campaignsBroadcast to your whole list or a segment, with A/B subject lines. Beautiful templatesA gallery of clean, deliverability-safe templates you can brand and reuse. ## How it fits together Under the hood, Deckle runs on Amazon SES and layers an event-driven core on top: contacts are keyed by email, events record what they do, and automations react to those events. That single primitive is what turns a send API into a marketing platform. Read How Deckle works for the full picture. Base URL The API is served at https://app.getdeckle.com/api/v1. All requests are authenticated with a secret API key — see API keys. ======================================================================== URL: https://app.getdeckle.com/docs/quickstart Section: Get started ======================================================================== Get started # Quickstart Create an API key and send your first email in a few minutes — with the SDK, plain HTTP, or SMTP. This guide gets a single email out the door. In test mode you can start immediately; to reach real inboxes you’ll verify a sending domain at the end. ### Create an API key In the dashboard, open Developers → API keys and create a key. Pick the Test environment to experiment safely. Copy the key — it starts with sk_test_ and is shown only once. Store it safely Deckle stores only a hash of your key. If you lose it, revoke the key and create a new one. Never commit keys — load them from an environment variable. ### Install the SDK (optional) The official Node.js SDK is zero-dependency and works on Node 18+, Deno, Bun, and the browser. Prefer raw HTTP? Skip ahead to the next step. bash ``` npm install @deckle/sdk ``` ### Send an email Set your key as an environment variable, then send: ``` import { Deckle } from "@deckle/sdk"; const deckle = new Deckle(process.env.DECKLE_API_KEY!); const { id } = await deckle.emails.send({ from: "Acme ", to: "delivered@resend-like.simulator", // use success@simulator.amazonses.com in test mode subject: "Hello from Deckle", html: "

It works!

Your first Deckle email.

", }); console.log("Queued:", id); ``` Test mode A sk_test_ key can only send to the AWS SES simulator (for example success@simulator.amazonses.com) or to your own verified domains — never a real inbox. That makes it safe to run in CI. ### Verify a domain to go live To send to real recipients, add and verify a sending domain, then switch to a sk_live_ key. Deckle generates the DKIM, SPF, and DMARC records for you to publish at your DNS provider. - Open Domains and add your domain. - Publish the DKIM, SPF & DMARC records. - Wait for verification (auto-polled), then send from an address on that domain. ## What’s next You’ve sent an email. Here’s where to go from here. Send your first emailA deeper walkthrough — templates, variables, and inspecting the result. API keysTest vs live keys, rotation, and keeping secrets safe. DomainsVerify a domain so your mail lands in the inbox, from your own reputation. API referenceEvery endpoint, parameter, and response. ======================================================================== URL: https://app.getdeckle.com/docs/send-your-first-email Section: Get started ======================================================================== Get started # Send your first email A hands-on walkthrough: send raw HTML, then a branded template with variables, and inspect what happened. If you followed the Quickstart, you already have a key. Here we go a little deeper — the same send call powers everything from a password reset to a receipt. ## Prerequisites - An API key (create one) exported as DECKLE_API_KEY. - A verified sending domain for live sends — or use test mode against the SES simulator. ## Send a raw HTML email The only required fields are from, to, subject, and one of html or template. The from address must be on a domain you’ve verified. ``` import { Deckle } from "@deckle/sdk"; const deckle = new Deckle(process.env.DECKLE_API_KEY!); const result = await deckle.emails.send({ from: "Acme ", to: "jane@example.com", reply_to: "support@acme.com", subject: "Welcome to Acme 👋", html: `

Welcome, Jane

Thanks for signing up. Your account is ready.

`, text: "Welcome, Jane. Thanks for signing up.", }); console.log(result.id, result.status); ``` Always include plain text Pass a text alternative alongside html. Multipart emails look trustworthy to spam filters and render everywhere. When you send a stored template, Deckle generates the text part for you. ## Send a template with variables Instead of inline HTML, reference a stored template by id and pass variables. Deckle renders the template, fills {{placeholders}}, and injects open + click tracking automatically. template.ts ``` await deckle.emails.send({ from: "Acme ", to: "jane@example.com", subject: "Welcome, {{first_name}}", template: "tmpl_welcome_01", variables: { first_name: "Jane", plan: "Pro" }, }); ``` See Templates and Variables & personalization for the full syntax, including fallbacks like {{first_name | "there"}}. ## Inspect the result Every send returns an id. Use it to fetch the current status and engagement counts, or list recent sends. ts ``` const email = await deckle.emails.get(result.id); // { id, to, subject, status: "delivered", opens: 1, clicks: 0, ... } const recent = await deckle.emails.list({ status: "sent", limit: 20 }); ``` You’ll also see every send in the dashboard under Developers → Email logs, with delivery, open, click, bounce, and complaint status as it arrives. ## Next steps TemplatesReuse designed emails and brand them per project. ContactsStore your audience and personalize from their data. AutomationsTrigger flows from events like sign-ups and purchases. ======================================================================== URL: https://app.getdeckle.com/docs/concepts/how-deckle-works Section: Core concepts ======================================================================== Concepts # How Deckle works A tour of Deckle's architecture — sending from your own domain on top of AWS SES, with an event-driven core that turns a send API into a full marketing platform. Deckle is a thin, opinionated layer over Amazon SES plus an event-driven core. You send from a domain you own and verify, Deckle handles rendering and delivery, and every delivery signal flows back to update status, protect your reputation, and drive automations. ## The big picture Deckle runs on Model A: you send from your own verified domain on top of AWS SES. There is no shared sending pool. Every email leaves under your domain’s DKIM signature, so deliverability rides on your reputation and yours alone. That is the key difference from shared-pool providers. When many senders share the same IPs and envelope domain, one bad actor’s spam complaints can quietly drag down everyone else’s inbox placement. With Model A, your reputation is isolated: nobody else’s list hygiene can poison your delivery, and you carry the full, portable value of the domain reputation you build. On top of delivery, Deckle layers an event-driven core. Contacts, named events, segments, automations, and campaigns all sit above the same send pipeline, so the same infrastructure that delivers a password reset can also power a lifecycle automation. You own the domain, Deckle owns the plumbing Because you send from your own domain, you keep your reputation even if you ever leave. Deckle creates the SES identity and shows you the DNS records to publish — see Domains overview. ## The send pipeline Every email — whether it comes from the API, the SDK, the SMTP relay, a campaign, or an automation — travels the same path. Here is what happens between your request and the inbox. ### Validate the request Deckle checks the required fields (from, to, subject, and one of html or template) and confirms the from address belongs to a verified domain in the project. Invalid requests are rejected before anything is sent. ### Check the suppression list The recipient is checked against the project’s suppression list. If the address was hard-bounced, complained, unsubscribed, or added manually, the send is rejected with a 422 so you never re-mail an address that should stay quiet. ### Render to clean HTML and plain text If you passed a template, Deckle renders its React Email source with your variables into deliverability-safe, inline-CSS HTML, and generates a plain-text alternative automatically. Template sends also get open-pixel and click tracking injected; raw html sends are delivered as-is, uninstrumented. ### Deliver through AWS SES The finished message is handed to AWS SES and sent from your verified domain under its DKIM signature. Deckle records an email log with an id (email_…) and an initial status. ### Receive delivery signals over SNS SES reports what happened — delivery, bounce, or complaint — back to Deckle through an SNS notification. This is where the async half of the pipeline begins: the outcome arrives moments (or, for some bounces, longer) after the accept. ### Update status, auto-suppress, and fire webhooks Deckle updates the email’s status from each notification, auto-suppresses hard bounces and complaints so they are never mailed again, and fires the matching webhooks to your endpoints (email.delivered, email.bounced, email.complained, and, for tracked template sends, email.opened and email.clicked). Not configured yet? You still get a clean accept Before delivery is fully wired up, a valid send returns 202 — accepted and queued — instead of failing. Your integration can be built and tested end to end while DNS and SES finish verifying. ## The event-driven core Delivery is only half of Deckle. The other half is a small set of primitives that turn raw sends into a marketing platform. They all revolve around one idea: a contact is keyed by email, and things happen to it over time. ### Contacts A contact is an email address plus optional name, subscription state, and custom data. Email is the identity, so the same person is one contact whether they arrived from a signup form, a CSV import, or an API call. Contacts are unlimited — Deckle bills per send, not per contact. ### Events Events are named actions tied to a contact by email — user.signed_up, order.completed, anything meaningful in your product. You record them once and they do double duty: they build the contact’s activity timeline and they drive everything reactive downstream. ### Automations Automations are workflows that react to events (or to a contact subscribing). A trigger starts an enrollment, and ordered steps — send an email, wait for a delay, wait for another event, branch on a condition, or run an action like adding a tag — carry the contact through the flow. This is the primitive that connects “what a user did” to “what email they get next.” ### Segments Segments are live, rule-based groups of contacts. Instead of a static list, a segment is a set of AND/OR rules over attributes, events, and engagement — membership recalculates as contacts change. Use one as a campaign target. ### Campaigns Campaigns broadcast a single message to your whole audience or a segment, one-off or scheduled, with optional A/B subject lines. Under the hood, a campaign fans out into the very same send pipeline above — every recipient is validated, suppression-checked, rendered, delivered, and tracked exactly like a one-off send. One pipeline, many entry points Transactional API calls, SMTP, campaigns, and automation steps all converge on the same validate → render → SES → SNS path. Learn it once and it explains every kind of send Deckle makes. ## What this means for you Because these pieces share one foundation, you can start small and grow without switching tools: - Send a single transactional email today with nothing but an API key and a verified domain. - Start recording events, and those same signals populate timelines and wake automations later — no re-instrumentation. - Layer on segments and campaigns when you are ready to broadcast, reusing the reputation and deliverability you have already built. ## Next steps Dig into the primitives that make the pipeline and the event-driven core concrete. Projects & environmentsHow projects isolate your data and how Test and Live environments keep sends safe. Send with the SDKPut the send pipeline to work from Node with @deckle/sdk. EventsRecord named actions that power timelines, segments, and automation triggers. ======================================================================== URL: https://app.getdeckle.com/docs/concepts/projects-and-environments Section: Core concepts ======================================================================== Concepts # Projects & environments A project is an isolated workspace, and every project has a Test and a Live environment. Which one you send from is decided entirely by the key you use. Everything in Deckle lives inside a project, and every project ships with two environments. You don't configure environments separately — you pick one by using a sk_test_ or sk_live_ key, which keeps testing safely away from real inboxes. ## Projects A project is a self-contained workspace. Each one owns its own data and never shares it with another project, so you can keep separate apps, brands, or clients fully isolated under a single account. Everything below is scoped to the project you're in: - Contacts — your audience, keyed by email, with events and activity timelines. - Templates — the React Email templates you send and brand. - Domains — the verified domains you're allowed to send from. - API keys — the secret keys that authenticate every request. - Events — the named actions that power segments and automations.Because keys are scoped to a project, a key from one project can never read or send on behalf of another. Create and switch between projects with the project switcher in the dashboard top bar. ## Test and Live Every project has two environments, Test and Live. You don't select an environment in a settings page — you select it by choosing which key you send with. A sk_test_ key targets the Test environment; a sk_live_ key targets Live. ``` import { Deckle } from "@deckle/sdk"; // A test key sends into the Test environment. const test = new Deckle("sk_test_..."); await test.emails.send({ from: "Acme ", to: "success@simulator.amazonses.com", subject: "Sandbox check", html: "

This never reaches a real inbox.

", }); // A live key sends into the Live environment. const live = new Deckle("sk_live_..."); await live.emails.send({ from: "Acme ", to: "jane@example.com", subject: "Welcome to Acme 👋", html: "

Hey Jane

", }); ``` In the dashboard, the Test/Live toggle sits in the top bar. It filters what you see — emails, logs, and reports — to the environment you're viewing, so a test send never clutters your live analytics. ### What Test can do The Test environment is a sandbox. A sk_test_ key can only deliver to two kinds of recipient: - AWS SES simulator mailboxes, such as success@simulator.amazonses.com, bounce@simulator.amazonses.com, and complaint@simulator.amazonses.com. - Addresses on the project's own verified domains.This lets you exercise the full pipeline — templates, variables, webhooks, and bounce or complaint handling — without any risk of reaching a real person. Test keys are sandboxed A sk_test_ key can never send to arbitrary real inboxes. It reaches only the SES simulator mailboxes or your project's verified domains. Any other recipient is rejected. To email real people — and to run campaign or broadcast sends — you must use a sk_live_ key. ### What requires Live ActionTestLive Transactional send to the SES simulatorYesYes Send to your own verified domainYesYes Send to arbitrary real inboxesNoYes Campaign / broadcast sendNoYes ## Switching projects Use the project switcher in the dashboard top bar to move between projects or create a new one. Switching changes everything the dashboard shows — contacts, templates, domains, keys, and logs all belong to the active project. Over the API there's nothing to switch: the project (and its environment) is determined by the key on the request. Point your app at a different project by swapping the key, typically through an environment variable such as DECKLE_API_KEY. One key per environment per deploy Give each deploy its own key: a sk_test_ key in local and staging, a sk_live_ key in production. Store both in API key secrets, never in source control. ## Next steps API keysCreate, scope, and rotate the test and live keys that pick your environment. Send with the SDKSend your first email from a project using the Node.js SDK. ======================================================================== URL: https://app.getdeckle.com/docs/concepts/api-keys Section: Core concepts ======================================================================== Concepts # API keys Secret keys authenticate every request to the Deckle API, SDK, and SMTP relay. Create one per environment, keep it secret, and rotate it when you need to. Deckle uses secret API keys to authenticate requests. Each key is scoped to a single project and a single environment (Test or Live), so the key you send decides which sandbox — and which data — you touch. Secret keys only Deckle does not issue public or publishable keys. Every key starts with sk_live_ or sk_test_ and must stay on your server. ## Creating a key Keys are managed in the dashboard. You'll see the full key exactly once at creation, so have somewhere safe to paste it before you start. ### Open Developers In the dashboard top-nav, go to Developers. ### Go to API keys Open the API keys tab and choose Create key. ### Choose an environment and name Pick Test or Live, then give the key a descriptive name like production-api or staging. The name is just for you — it helps you tell keys apart later when you rotate or revoke them. ### Copy the key The full key (sk_live_…) is shown once. Copy it and store it in your secrets manager or environment variables now — you cannot view it again. ## Using a key Pass the key as a bearer token in the Authorization header on every request, or hand it to the SDK when you construct the client. ``` curl https://app.getdeckle.com/api/v1/emails \ -H "Authorization: Bearer sk_live_..." ``` Store the key in an environment variable such as DECKLE_API_KEY and read it at runtime. The same key also works as your SMTP password — see Keep keys secret below. ## Test vs live keys Every project has two environments, and the key prefix is what selects between them. A sk_test_ key runs in the sandbox; a sk_live_ key sends to real inboxes. Read Projects & environments for the full model. PrefixEnvironmentCan send to sk_test_Test (sandbox)SES simulator mailboxes and your own verified domains only sk_live_LiveAny recipient, from a verified domain Test keys are safe to experiment with A test key can only deliver to AWS SES simulator addresses like success@simulator.amazonses.com or to domains you've verified — it can never reach a real inbox. Campaign and broadcast sends always require a live key. ## Rotating and revoking Keys don't expire on their own. When one is compromised, when a teammate leaves, or on a routine schedule, rotate it. Because a key is shown only once, rotation is a create-then-swap, not an in-place reset. ### Rotate a key ### Create a replacement Create a new key in the same environment with a fresh name. ### Deploy the new key Update DECKLE_API_KEY everywhere it's used and roll out the change. ### Revoke the old key Once traffic is flowing on the new key, revoke the old one from Developers → API keys. ### Revoke a key Revoking a key is immediate and permanent — every request using it (API, SDK, or SMTP) then fails with a 401. Revoke any key you no longer need or suspect has leaked. json ``` { "error": "Invalid API key" } ``` ## Keep keys secret Treat a secret key like a password: it can send email from your domain, read your contacts, and trigger campaigns. Keep it on your server and out of anything a browser or a repository can see. Your key is shown only once Deckle displays the full key a single time at creation and never stores it in a form we can read back — we keep only a SHA-256 hash of the key plus a masked prefix (like sk_live_…a1b2) so you can recognize it in the dashboard. If you lose the key, you can't recover it — create a new one and revoke the old. This same secret key doubles as your SMTP password, so protect it accordingly.A few rules that keep keys where they belong: Never ship keys to the clientKeys are server-side only. Don't put them in browser code, mobile apps, or public repos — there are no publishable keys for a reason. - - Use environment variablesLoad the key from DECKLE_API_KEY (or your secrets manager) at runtime instead of hardcoding it in source. ## Next steps - - Send over HTTPUse your key to send your first email with a raw HTTP request. Projects & environmentsSee how Test and Live environments and per-project isolation fit together. ======================================================================== URL: https://app.getdeckle.com/docs/concepts/dashboard Section: Core concepts ======================================================================== Concepts # The dashboard A quick tour of the Deckle dashboard — the top-nav, the environment and project switchers, and the shape every list page shares. The dashboard lives at app.getdeckle.com and is where you verify domains, build templates, manage your audience, and watch what you send. It’s dark-only by design, so every screen is tuned for long sessions in the editor and the campaign report. ## Navigation The top navigation bar is the same on every page. Six primary destinations sit in the nav itself; account-level settings live behind the account menu on the far right. Nav itemRouteWhat’s there Dashboard/Sending overview and KPIs for the current project and environment. Audience/audienceContacts, events, segments, and suppressions. Templates/templatesThe template gallery and your saved, branded templates. Campaigns/campaignsOne-off and scheduled broadcasts, plus their reports. Automations/automationsVisual, event-driven workflows and their run history. Developers/developersAPI keys, webhooks, SMTP credentials, and logs. Account menu Domains, Settings, Team, and Billing live in the account menu at the top-right, not in the main nav. That’s where you verify a sending domain and invite teammates. ## Environments & projects Two switchers in the top bar control the scope of everything you see and do. Together they answer a single question: which project, and Test or Live? ### Test / Live toggle The environment toggle flips the whole dashboard between your Test and Live data. Test is fully sandboxed — sends only reach the AWS SES simulator or your own verified domains — while Live sends to real inboxes. The toggle mirrors the key you use in code: sk_test_ keys read and write Test, sk_live_ keys read and write Live. See Projects & environments for the full model. ### Project switcher A project is an isolated workspace with its own contacts, templates, domains, API keys, and events. Use the project switcher — right next to the environment toggle — to jump between workspaces or create a new one. Nothing crosses the boundary: switching projects swaps out every list on every page. Read the top bar first Before you send a test broadcast or debug a “missing” contact, glance at the top bar. Most surprises come down to being in the wrong project or the wrong environment. ## Anatomy of a page Every list page in Deckle — Contacts, Templates, Campaigns, Automations — follows the same three-part layout, so once you know one, you know them all. - Header & KPI cards. The page title sits above a row of KPI cards that summarize the current view — total contacts, delivery rate, active automations — scoped to your selected project and environment. - Search, filter & action. A control row below the header holds search, tabs or filters, and the primary action button (for example, New campaign or Import contacts) on the right. - Table. The main content is a table of records. Rows link to a detail view — a contact’s activity timeline, a campaign’s report, or a template in the editor.Because the pattern is consistent, the primary action is always where you expect it and the KPIs always reflect the same scope as the table beneath them. ## Next steps API keysCreate Test and Live keys in Developers, and learn how they map to environments. ContactsBuild your audience — the records behind the Audience section and its timelines. ======================================================================== URL: https://app.getdeckle.com/docs/mcp Section: Build with AI ======================================================================== Build with AI # Model Context Protocol (MCP) Connect Claude, Cursor, and other AI tools to Deckle so they can manage your email — send messages, build templates and campaigns, edit automations, and manage contacts — straight from chat. Deckle ships a remote MCP server. Point any MCP-capable AI tool at it with your secret key and it can drive your project through the same v1 API — with the same auth, validation, scoping, and rate limits. ## The server POSThttps://app.getdeckle.com/mcpIt speaks JSON-RPC over Streamable HTTP and authenticates with a Deckle secret API key in the Authorization header — exactly like the REST API. Every tool call is scoped to the key’s project and environment. Your config holds a secret The key in your MCP config can send email and manage your data. Keep the file private, and use a sk_test_ key while you experiment. ## Connect your tool Get a key from Developers → API keys, replace YOUR_SECRET_KEY below, and add the config for your client. They all point at the same URL and differ only in file format. ``` claude mcp add --transport http deckle https://app.getdeckle.com/mcp \ --header "Authorization: Bearer YOUR_SECRET_KEY" ``` You can also connect from the dashboard: Developers → Connect AI has a copy-paste recipe for each tool. No remote-MCP support? If your client can’t send an Authorization header on a remote server, bridge through the mcp-remote stdio proxy:bash ``` npx -y mcp-remote https://app.getdeckle.com/mcp \ --header "Authorization: Bearer YOUR_SECRET_KEY" ``` ## What the AI can do The server exposes tools across every core resource: AreaTools EmailsSend transactional email, list sent emails, check a message’s status. TemplatesList, read, create, and edit templates (blocks or raw HTML). CampaignsList, read, create, update, and send campaigns. AutomationsList, read, create, update, and delete automations. ContactsList, read, upsert, update, and delete contacts. EventsRecord custom events for a contact. SuppressionsList, add, and remove suppressed addresses. Test it After connecting, try asking your assistant: “Using Deckle, create a welcome template and send a test to me@example.com.” It will call the tools for you. ## Set it up in three steps ### Create a secret key In Developers → API keys, create a key (start with Test) and copy it. ### Add the server to your tool Paste the config above for your client, swapping in your key, and restart the tool. ### Ask it to do something Deckle’s tools now appear to your assistant. Ask it to draft a campaign or send an email and confirm the result in the dashboard. ======================================================================== URL: https://app.getdeckle.com/docs/sending/sdk Section: Sending email ======================================================================== Sending # Send with the SDK Send transactional and marketing email from your own verified domain with the official @deckle/sdk Node client. The Deckle SDK is a thin, zero-dependency wrapper over the REST API. Create a client with your secret key, then call deckle.emails.send() — no request-signing or boilerplate to write. It runs on Node 18+, Deno, Bun, and the browser. ## Install Add the SDK to your project. It ships with TypeScript types and has no runtime dependencies. bash ``` npm install @deckle/sdk ``` Grab a secret key from the dashboard under Developers → API keys and store it in your environment as DECKLE_API_KEY. Use an sk_test_ key while you build and an sk_live_ key in production — see API keys for the difference. ## Send an email Instantiate the client once and reuse it. The from address must belong to a domain you have verified, and either html or text (or a template) is required. The call returns the email's id and current status. ``` import { Deckle } from "@deckle/sdk"; const deckle = new Deckle(process.env.DECKLE_API_KEY!); const email = await deckle.emails.send({ from: "Acme ", to: "jane@example.com", subject: "Welcome to Acme 👋", html: "

Hey Jane

Thanks for joining.

", }); console.log(email.id); // "email_9f2c…" ``` You can pass an array to to for multiple recipients, and add cc, bcc, reply_to, and attachments the same way. Raw html sends go out as-is; for open and click tracking, send with a template instead. Your from-domain must be verified Deckle only delivers from domains you own and have verified (Model A). If the domain in from is not verified, the send is rejected. Add and verify a domain under Domains before sending to real inboxes. Test keys can only send to your verified domains or the AWS SES simulator mailboxes. ## Send with a template Pass a template id instead of html to render one of your stored templates. Fill its {{variable}} placeholders with the variables object — values also merge with the contact's stored data — and Deckle injects open-pixel and click tracking automatically. You can override the subject per send. send-template.ts ``` import { Deckle } from "@deckle/sdk"; const deckle = new Deckle(process.env.DECKLE_API_KEY!); const email = await deckle.emails.send({ from: "Acme ", to: "jane@example.com", subject: "Your Acme receipt", // optional override template: "tmpl_receipt", variables: { first_name: "Jane", order_id: "ord_1234", total: "$49.00", }, }); ``` See Variables & personalization for placeholder and fallback syntax like {{first_name | "there"}}. ## Inspect sent email Look up a single email by id, or page through everything you've sent. List results are cursor-based — pass next_cursor back as cursor while has_more is true. inspect.ts ``` // Fetch one email const email = await deckle.emails.get("email_9f2c…"); console.log(email.status); // "delivered" // List recent emails, filtered by status const page = await deckle.emails.list({ status: "bounced", limit: 50, }); for (const item of page.data) { console.log(item.id, item.status); } if (page.has_more) { const next = await deckle.emails.list({ status: "bounced", cursor: page.next_cursor!, }); } ``` For richer delivery signals — bounces, complaints, opens, and clicks in real time — subscribe to webhooks rather than polling list. ## Handling errors Every failed request throws a DeckleError. It carries the HTTP status, a human-readable message, and the parsed response body. Wrap sends in a try/catch and branch on the status. errors.ts ``` import { Deckle, DeckleError } from "@deckle/sdk"; const deckle = new Deckle(process.env.DECKLE_API_KEY!); try { await deckle.emails.send({ from: "Acme ", to: "jane@example.com", subject: "Welcome to Acme 👋", html: "

Hey Jane

", }); } catch (err) { if (err instanceof DeckleError) { // err.status → HTTP status code (e.g. 422, 429) // err.message → "message" from { "error": "..." } // err.body → the parsed error response if (err.status === 429) { console.warn("Rate limited — retry after a moment"); } else if (err.status === 422) { console.error("Rejected:", err.message); } else { throw err; } } else { throw err; } } ``` Common statuses: 422 for a rejected send (unverified domain, suppressed recipient, or missing field), 401 for an invalid key, and 429 when you exceed 120 requests / minute — the response includes a Retry-After header. See Errors for the full list. ## Next steps Send with templatesRender branded, tracked emails from your stored templates. IdempotencySafely retry sends without delivering the same email twice. Emails API referenceEvery field and response for send, get, and list. ======================================================================== URL: https://app.getdeckle.com/docs/sending/http Section: Sending email ======================================================================== Sending # Send over HTTP Send email with a single authenticated POST request — no SDK required. Works from any language or runtime that can make an HTTP call. The @deckle/sdk is just a thin wrapper around one endpoint. If you’d rather call the API directly — from a language we don’t ship an SDK for, or from a shell script — everything you need is a Bearer token and a JSON body. ## The endpoint All sends go through a single endpoint. Send a POST request with your JSON body and authenticate with your secret API key in the Authorization header. POST/api/v1/sendhttp ``` POST https://app.getdeckle.com/api/v1/send Authorization: Bearer sk_live_... Content-Type: application/json ``` The base URL is https://app.getdeckle.com/api/v1. Create a key in the dashboard under Developers → API keys — the full sk_live_... value is shown once at creation, so store it somewhere safe. See API keys for details. Test vs. live keys A sk_test_ key can only send to the AWS SES simulator (for example success@simulator.amazonses.com) or your own verified domains. Swap in a sk_live_ key to reach real inboxes. ## Send a request The body is JSON with snake_case fields. to, from, and subject are required, plus either html or template. The from address must be on a domain you’ve verified. ``` curl -X POST https://app.getdeckle.com/api/v1/send \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "from": "Acme ", "to": "jane@example.com", "subject": "Welcome to Acme 👋", "html": "

Hey Jane

Thanks for joining.

" }' ``` You can pass cc, bcc, reply_to (a string or array), a plain-text text alternative, and attachments in the same body. For the full field list, see the emails API reference. ## The response A successful send returns 200 with a JSON object describing the queued email. The id is Deckle’s email log id (prefixed email_) — use it to look up status later. json ``` { "id": "email_2Nc8Kx1mQvRz", "status": "queued", "messageId": "0100019200000000-11111111-2222-3333-4444-555555555555-000000@eu-west-1.amazonses.com" } ``` Not sending yet? If your project doesn’t have delivery configured, the request returns 202 and the email is accepted and queued rather than sent immediately. Verify a domain to start delivering. ## Using a template Instead of raw html, pass a template id and a variables object. Deckle renders the stored template with your values, injects a plain-text alternative, and — unlike raw HTML sends — automatically adds open and click tracking. json ``` { "from": "Acme ", "to": "jane@example.com", "subject": "Your Acme receipt", "template": "tmpl_welcome", "variables": { "first_name": "Jane", "plan": "Pro" } } ``` Placeholders in the template use {{variable}} syntax, with an optional fallback like {{first_name | "there"}}. The subject can be overridden per send, as shown above. See Templates and Variables & personalization for more. ## Errors Errors return the appropriate HTTP status and a JSON body of the shape { "error": "message" }. The most common ones: StatusMeaning 401Missing or invalid API key. Check the Authorization header and that the key hasn’t been revoked. 422The request was understood but couldn’t be processed — a missing required field, an unverified from domain, or a suppressed recipient. 429Rate limited. You’ve exceeded 120 requests per minute for this project; retry after the number of seconds in the Retry-After header. Idempotent sends Add an Idempotency-Key header to safely retry a send. A repeat with the same key returns the first result with idempotent: true instead of sending again. See Idempotency. ## Next steps Use the Node SDKSkip the raw HTTP and send with typed methods and helpers. Emails API referenceEvery field, response, and status value for the send endpoint. ======================================================================== URL: https://app.getdeckle.com/docs/sending/smtp Section: Sending email ======================================================================== Sending # Send over SMTP Point any SMTP client at Deckle to send email through your verified domain — no SDK or HTTP integration required. If your framework or platform already speaks SMTP, you can send through Deckle without touching the API. Use your secret API key as the SMTP password and every message routes through your verified domain on AWS SES, just like an API send. ## Credentials Connect your client with the settings below. The username is always the literal string apikey — the value that matters is the password, which is one of your secret API keys. SettingValue Hostsmtp.getdeckle.com Port587 (STARTTLS) or 465 (TLS) Usernameapikey PasswordYour secret API key (sk_live_… or sk_test_…) Which port? Prefer 587 with STARTTLS. Use 465 (implicit TLS) if your client or network blocks 587. Both are encrypted.Create a key in the dashboard under Developers → API keys. The same secret key doubles as your SMTP password, so a key with the right environment is all you need. See API keys for how keys map to Test and Live. ## Send with Nodemailer Here’s a complete example using Nodemailer in Node.js. Store your key in an environment variable rather than hard-coding it. send-smtp.ts ``` import nodemailer from "nodemailer"; const transporter = nodemailer.createTransport({ host: "smtp.getdeckle.com", port: 587, secure: false, // true for port 465 auth: { user: "apikey", pass: process.env.DECKLE_API_KEY!, }, }); await transporter.sendMail({ from: "Acme ", to: "jane@example.com", subject: "Welcome to Acme 👋", html: "

Hey Jane

Thanks for joining.

", text: "Hey Jane — thanks for joining.", }); ``` Match the port to the transport Set secure: false for port 587 (STARTTLS) and secure: true for port 465 (implicit TLS). Mismatching them is the most common connection failure. ## Other frameworks Any tool that can send over SMTP works with Deckle — point it at the credentials above and send. A few common setups: ### WordPress Install an SMTP plugin (for example WP Mail SMTP), then set the host to smtp.getdeckle.com, port 587 with encryption/STARTTLS, username apikey, and paste your secret key as the password. Set the “From” address to an address on your verified domain. ### Laravel Configure the SMTP mailer in your .env: .env ``` MAIL_MAILER=smtp MAIL_HOST=smtp.getdeckle.com MAIL_PORT=587 MAIL_USERNAME=apikey MAIL_PASSWORD=sk_live_... MAIL_ENCRYPTION=tls MAIL_FROM_ADDRESS=hello@acme.com MAIL_FROM_NAME="Acme" ``` ### Any other SMTP client Django, Rails Action Mailer, Postfix, a printer, a CI job — whatever sends mail, give it the host smtp.getdeckle.com, port 587 or 465, username apikey, and your secret API key as the password. There’s nothing Deckle-specific beyond these settings. ## Requirements - Your from-domain must be verified. The address in the From header has to belong to a domain you’ve added and verified in Domains. Sends from an unverified domain are rejected. - A live key sends to real inboxes. Use an sk_live_… key for production traffic. A test key (sk_test_…) is sandboxed — it can only reach the AWS SES simulator mailboxes or your own verified domains, never arbitrary recipients.Keep your key secret Your SMTP password is a full-access API key. Load it from an environment variable or secret store, never commit it, and rotate it by creating a new key and revoking the old one. ## Next steps API keysCreate, scope, and rotate the secret keys you use as your SMTP password. Domains overviewAdd and verify the domain you'll send from with DKIM, SPF, and DMARC. ======================================================================== URL: https://app.getdeckle.com/docs/sending/templates Section: Sending email ======================================================================== Sending # Templates Store a design once, then send it by id with per-send variables instead of shipping raw HTML on every call. A template bundles a reusable subject and design into a single object you reference by id. Send with template instead of html, pass in the values that change, and Deckle renders and instruments the email for you. ## What is a template A template is a stored, reusable email design — a subject and a body — that lives in your project and is identified by a tmpl_ id. Instead of building the same HTML in your code for every send, you author the design once (in the dashboard or the gallery) and reference it by id at send time. The stored source uses {{variable}} placeholders, so a single template covers every recipient. You supply the values that differ per send, and Deckle renders the final HTML and a plain-text alternative before delivering through your verified domain. Templates vs. raw HTML Reach for a template when the same design is sent repeatedly (receipts, welcome emails, password resets). Send raw html for one-off or fully dynamic content you assemble in code. ## Send with a template Pass the template id as template and a variables object with the values to fill in. You can override the stored subject per send with the subject field, or omit it to use the template's own subject. ``` import { Deckle } from "@deckle/sdk"; const deckle = new Deckle(process.env.DECKLE_API_KEY!); await deckle.emails.send({ from: "Acme ", to: "jane@example.com", template: "tmpl_welcome", variables: { first_name: "Jane", company: "Acme", }, }); ``` Exactly one of html or template is required. Everything else about the send — cc, bcc, reply_to, and attachments — works the same as a raw HTTP or SDK send. ## Variables Templates and subjects use {{variable}} placeholders, filled from the variables object on the send and from the contact's stored data. Missing values can fall back with {{first_name | "there"}}. See Variables & personalization for the full syntax, fallbacks, and conditional blocks. ## Automatic tracking Template sends are instrumented When you send with a template, Deckle automatically injects an open-tracking pixel and rewrites links for click tracking, so opens and clicks flow into the contact timeline and your webhooks. Raw html sends are delivered as-is and are not instrumented.If you need open and click analytics on content you build yourself, send it through a stored template rather than inline html. The tracked events surface on the contact timeline and as email.opened and email.clicked webhook events. ## Managing templates Create, edit, and preview templates in the dashboard under Templates. The editor gives you desktop and mobile previews, light and dark inbox modes, sample variable data, and checks for spam score, broken links, and missing variables. You can also manage templates programmatically. See the Templates API reference for creating and listing templates, or start from a ready-made design in the template gallery. ## Next steps - - VariablesPersonalize subjects and templates with placeholders, fallbacks, and conditionals. Template galleryDozens of clean, brandable React Email templates to start from. Templates APICreate, list, and fetch templates over the REST API. ======================================================================== URL: https://app.getdeckle.com/docs/sending/variables Section: Sending email ======================================================================== Sending # Variables & personalization Insert per-recipient values into subjects and templates with {{variable}} placeholders, with fallbacks and conditional blocks for the tricky cases. Personalization in Deckle is a single primitive: {{variable}} placeholders. Drop them into a subject line or a template, provide values on send or from the contact, and Deckle fills them in as it renders each email. ## Placeholders A placeholder is a name wrapped in double curly braces, like {{first_name}}. You can use placeholders in the subject and anywhere in a template body. When Deckle renders the email, each placeholder is replaced with the matching value for that recipient. template.json ``` { "subject": "{{first_name}}, your order is on its way", "body": "Hi {{first_name}}, order #{{order_id}} shipped to {{city}} today." } ``` Placeholder names are case-sensitive and should be lowercase with underscores (first_name, order_id). A placeholder with no matching value renders as empty unless you give it a fallback. Where variables are applied Placeholders are resolved for template sends and for the subject line. Raw html sends are delivered as-is and are not instrumented or interpolated — pass a template when you want personalization. ## Providing values Values come from two places, merged together for each recipient: Value sources variablesobjectOptionalA key/value map you pass on the send call. Great for one-off values that only exist at send time, like order_id or a magic link.contact.dataobjectOptionalThe custom data stored on a contact (keyed by email). Anything you saved on the contact is available as a placeholder without repeating it on every send.Pass a variables map alongside the template when you send. Keys match placeholder names; values are strings (or numbers that render as text). ``` import { Deckle } from "@deckle/sdk"; const deckle = new Deckle(process.env.DECKLE_API_KEY!); await deckle.emails.send({ from: "Acme ", to: "jane@example.com", template: "tmpl_shipping_update", variables: { first_name: "Jane", order_id: "1234", city: "Portland", }, }); ``` When you send to a known contact, Deckle also pulls from that contact's stored data. If a placeholder appears in both places, the variables you pass on the send win — so you can store defaults on the contact and override them per send. contact.ts ``` // Store data on the contact once... await deckle.contacts.create({ email: "jane@example.com", name: "Jane Doe", data: { first_name: "Jane", plan: "Pro" }, }); // ...then {{first_name}} and {{plan}} resolve automatically, // even when you don't pass them in variables. await deckle.emails.send({ from: "Acme ", to: "jane@example.com", template: "tmpl_welcome", }); ``` Precedence at a glance Send-time variables override contact.data, which overrides the placeholder’s fallback, which falls back to empty. Most specific wins. ## Fallback values Real data is never complete. Give any placeholder a fallback with the pipe syntax so a missing value degrades gracefully instead of leaving a hole: text ``` Hi {{first_name | "there"}}, welcome aboard. ``` If first_name resolves to a value, that value is used; otherwise the quoted fallback is rendered. Fallbacks work everywhere placeholders do — subject and body alike. PlaceholderValue presentValue missing {{first_name}}Jane(empty) {{first_name | "there"}}Janethere Default your greetings A fallback on the first placeholder in a greeting is the single highest-leverage habit for personalization — Hi {{first_name | "there"}} never renders an awkward “Hi ,”. ## Conditional blocks For advanced templates you can render a chunk of content only when a value is present. This is useful when a section only makes sense for some recipients — a plan name, a referral code, a shipping note. text ``` {{#if plan}} You're on the {{plan}} plan — thanks for being a customer. {{else}} Upgrade any time to unlock more sends. {{/if}} ``` Conditional blocks are an advanced feature; for most emails, placeholders with fallbacks cover what you need. Build and preview conditionals in the template editor before you rely on them in a send. ## Previewing The template editor renders your placeholders against sample data so you can see exactly how a personalized email will look before it goes out. ### Open the template In the dashboard, go to Templates and open the template you're editing. ### Fill in sample data Set sample values for each placeholder in the editor's variable panel. Try both a filled value and an empty one to confirm your fallbacks behave. ### Preview across inboxes Toggle desktop/mobile and light/dark inbox preview. The editor also flags missing variables and broken links so nothing ships half-personalized.Preview is not a send Sample data lives in the editor only. Actual sends resolve placeholders from your send-time variables and the recipient contact’s stored data — not from the preview panel. ## Next steps TemplatesCreate reusable templates, then send them with a template id and variables. ContactsStore per-contact data so placeholders resolve automatically on every send. ======================================================================== URL: https://app.getdeckle.com/docs/sending/attachments Section: Sending email ======================================================================== Sending # Attachments Attach files — PDFs, images, CSVs, or anything else — to a transactional email by passing them as base64-encoded content. Add an attachments array to any send. Each item carries a filename and the file’s bytes as a base64-encoded string, and Deckle delivers it alongside your email from your verified domain. ## The shape Each entry in the attachments array is an object with the following fields. Attachment fields filenamestringRequiredThe name the file is delivered as, including its extension — for example invoice.pdf.contentstring (base64)RequiredThe file’s bytes, base64-encoded. Items without content are dropped.content_typestringOptionalThe MIME type, such as application/pdf or image/png. Optional — Deckle infers a sensible type from the filename when omitted. ## Example Read a file, base64-encode it, and pass it in attachments. The SDK takes the same fields as the raw JSON body. ``` import { readFileSync } from "node:fs"; import { Deckle } from "@deckle/sdk"; const deckle = new Deckle(process.env.DECKLE_API_KEY!); const invoice = readFileSync("./invoice.pdf"); await deckle.emails.send({ from: "Acme ", to: "jane@example.com", subject: "Your invoice", html: "

Thanks for your order — your invoice is attached.

", attachments: [ { filename: "invoice.pdf", content: invoice.toString("base64"), content_type: "application/pdf", }, ], }); ``` ## Notes A few things to keep in mind when attaching files: Behavior Base64-encode contentrequiredOptionalcontent must be a base64 string, not raw bytes or a file path. In Node, buffer.toString("base64") does the job.Empty items are droppedbehaviorOptionalAny attachment whose content is missing or empty is silently skipped, so the email still sends without it.Keep sizes reasonableguidanceOptionalLarge attachments hurt deliverability and can be rejected by receiving servers. Prefer a link to a hosted file for anything sizable, and reserve attachments for small documents.Link instead of attach when you can For big files or ones a recipient may not expect, sending a link keeps your message light and your deliverability healthy. Reserve attachments for small, expected documents like receipts and invoices. ## Next steps Send with the SDKEvery send option — templates, variables, cc/bcc, and idempotency. Emails API referenceThe full request and response schema for sending email over HTTP. ======================================================================== URL: https://app.getdeckle.com/docs/sending/idempotency Section: Sending email ======================================================================== Sending # Idempotency Retry a send safely. An idempotency key guarantees a given request runs at most once, so a dropped connection or a retry loop never turns into a double-send. Networks fail in the worst places — right after your request reaches Deckle but before the response reaches you. Instead of guessing whether the email went out, attach an idempotency key and retry with confidence: the second call returns the first result rather than sending again. ## Why idempotency A POST /v1/send that times out is ambiguous. Maybe the email sent and you lost the response; maybe it never arrived. Without a safeguard, retrying risks sending the same message twice, and skipping the retry risks not sending at all. An idempotency key removes the ambiguity. You generate one stable key per logical send and pass it on every attempt. Deckle records the first request under that key and, for any repeat, replays the original response. That gives you: - Safe retries. Retry a timed-out or failed request as many times as you need — only the first one actually sends. - No double-sends. A duplicated webhook, a double-clicked button, or an at-least-once job runner can all fire the same send twice without reaching your recipient twice. ## The Idempotency-Key Send the key in the Idempotency-Key request header on POST /v1/send. With the SDK, pass idempotencyKey on the send call and the header is set for you. ``` curl -X POST https://app.getdeckle.com/api/v1/send \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order_1234" \ -d '{ "from": "Acme ", "to": "jane@example.com", "subject": "Your receipt", "html": "

Thanks for your order

" }' ``` Where it applies The Idempotency-Key header is honored on POST /v1/send. Scope it to the transactional sends you retry — receipts, magic links, and alerts. ## Behavior The first request with a given key runs normally and Deckle stores its result. Any later request that reuses the same key skips sending entirely and returns that stored result, with an extra idempotent: true field so you can tell a replay from an original. ``` { "id": "email_9f2c...", "status": "queued", "messageId": "0100018e..." } ``` The repeat returns the same id and messageId as the original — it points at the one email that was already created. Nothing new is sent. ScenarioWhat happens First request with a keySends normally; result is stored under the key. Repeat with the same keyReturns the first result with idempotent: true; does not resend. Different key (or no key)Treated as a brand-new send. Same key, different body A key identifies one logical send. If you reuse a key but change the payload, you still get the original result back — Deckle does not re-evaluate the new body. Use a fresh key when you genuinely mean to send something different. ## Choosing a key Derive the key from a stable business identifier that maps one-to-one to the email you intend to send — the order id, the invoice number, the password-reset token. That way every retry of the same logical event naturally produces the same key. - Do use a deterministic id like order_1234, invoice_2026_0042, or reset_{userId}_{tokenId}. - Don't use a random value generated fresh on each attempt — a new UUID per retry defeats the purpose, since every call looks unique.If a single business event legitimately triggers several distinct emails, give each one its own key by adding a suffix — for example order_1234_receipt and order_1234_shipping. Keys are per project Idempotency keys are scoped to your project, so keys from different projects never collide. A short, human-readable key also makes retries easy to trace in your own logs. ## Next steps Idempotency (API reference)The header, response fields, and semantics in the REST reference. Send with the SDKSend transactional email from Node with the @deckle/sdk client. ======================================================================== URL: https://app.getdeckle.com/docs/audience/contacts Section: Audience ======================================================================== Audience # Contacts A contact is a person on your list, keyed by email. Store names and arbitrary custom data, then use contacts to power segments, campaigns, and automations. Everything in your audience is built around the contact. It's just an email plus whatever you want to know about the person behind it — and it's the anchor that ties events, sends, and automations together. ## The contact model Contacts are keyed by their email address, which is unique within a project. A contact carries a small set of built-in fields plus a free-form data object for anything specific to your product. Contact fields emailstringRequiredThe contact's email address and unique key within the project.namestringOptionalOptional display name, used for {{first_name}} and other personalization.subscribedbooleanOptionalWhether the contact is subscribed to marketing email. Defaults to true. Unsubscribing (or a complaint) flips this to false.sourcestringOptionalWhere the contact came from, e.g. api, import, or a value you set yourself.dataobjectOptionalArbitrary JSON — any custom fields you want to store on the contact.Unlimited contacts There's no cap on how many contacts you store, and no per-contact fee. Deckle bills on what you send, not on the size of your list — so you can keep every address, subscribed or not. ## Create or update Contacts are upserted by email: create a contact that already exists and Deckle updates it instead of erroring. The response status tells you which happened — 201 when a new contact was created, 200 when an existing one was updated. ``` import { Deckle } from "@deckle/sdk"; const deckle = new Deckle(process.env.DECKLE_API_KEY!); const contact = await deckle.contacts.create({ email: "jane@example.com", name: "Jane Doe", subscribed: true, source: "signup-form", data: { plan: "pro", company: "Example Inc.", }, }); // contact.id -> "contact_..." ``` Upsert semantics Because create is an upsert on email, it's safe to call on every signup or sync — you won't create duplicates. Only the fields you pass are changed; omitted fields keep their previous values. ## List, get, update, delete The full set of contact operations is available from the SDK and the REST API. Listing is cursor-based — pass limit and cursor to page through results. contacts.ts ``` import { Deckle } from "@deckle/sdk"; const deckle = new Deckle(process.env.DECKLE_API_KEY!); // List (cursor-based pagination) const { data, has_more, next_cursor } = await deckle.contacts.list({ limit: 50, }); // Get a single contact by id or email const contact = await deckle.contacts.get("jane@example.com"); // Update — merge in new fields await deckle.contacts.update("jane@example.com", { data: { plan: "enterprise" }, }); // Delete await deckle.contacts.delete("jane@example.com"); ``` Prefer to work over HTTP directly? Each operation maps to a REST endpoint — see the Contacts API reference for request and response shapes. ## Custom data The data object holds arbitrary JSON. Store plan tiers, company names, feature flags, lifecycle stages — whatever your product cares about. There's no schema to define up front; just send the keys you want. json ``` { "plan": "pro", "company": "Example Inc.", "signup_date": "2026-05-01", "trial_ends": "2026-05-15", "seats": 12, "beta_features": ["reports", "automations"] } ``` Custom data is what makes the rest of the platform expressive. It feeds {{variable}} personalization in templates and subject lines, and it's available as attributes when you build segments and branch inside automations. Personalize with data Any key on data can be referenced in a template with a fallback, e.g. {{plan | "free"}}. See Variables & personalization for the full syntax. ## Import & export Already have a list elsewhere? Import it from the dashboard, or add contacts one at a time through the API. You can export at any time. ### Open Audience → Contacts In the dashboard top-nav, go to Audience and open the Contacts table. Choose Import. ### Upload your CSV Drop in a CSV file. The first row should be your column headers. ### Map your fields Match each CSV column to a contact field — email, name, subscribed — or map it into data as a custom field. Rows are upserted by email, so re-importing updates existing contacts rather than duplicating them. ### Export when you need to Use Export on the Contacts table to download your list as a CSV, including custom data fields. ## Automations Contacts don't just sit in a table — they trigger workflows. When a new contact is created as subscribed, Deckle looks for live automations that fire on the contact.subscribed trigger and enrolls the contact automatically. Auto-enroll on subscribe Newly-created subscribed contacts are automatically enrolled in any live welcome automation triggered by contact.subscribed. Draft automations don't enroll anyone — set an automation to live to start welcoming new contacts.The contact detail page also shows an activity timeline — sends, opens, clicks, and events — so you can see a contact's full history in one place. ### Reasons a contact might not be subscribed A contact's subscribed flag flips to false in a few cases: CauseEffect Unsubscribe (one-click or hosted page)Contact is unsubscribed and added to suppressions Complaint (marked as spam)Auto-suppressed via SES/SNS; no further marketing email Imported with subscribed: falseStored, but excluded from marketing sends and auto-enroll ## Next steps EventsRecord what contacts do and use those actions to trigger automations. SegmentsBuild live, rule-based groups over contact attributes and events. Contacts APIEvery contact endpoint, parameter, and response for the REST API. ======================================================================== URL: https://app.getdeckle.com/docs/audience/events Section: Audience ======================================================================== Audience # Events Record what your users do as named events, tie them to a contact by email, and use them to trigger automations. Events are the heartbeat of Deckle’s event-driven core. Every time something meaningful happens in your product, send an event — Deckle attaches it to the matching contact, adds it to their timeline, and lets your automations react in real time. ## What are events An event is a named action tied to a contact by their email address. The name describes what happened, and you can attach an optional data payload with details about the action. Deckle doesn’t prescribe a taxonomy — you choose names that map to your product. A common convention is object.action, lowercase and dot-separated: Event nameFires when user.signed_upA new user creates an account. order.completedA customer finishes a purchase. trial.expiredA free trial reaches its end date. Events show up on the contact’s activity timeline alongside sends, opens, and clicks, so you get one view of everything a person has done. ## Track an event Send an event with POST /v1/events or the SDK’s events.create. A name is required; email links the event to a contact, and data carries any structured details you want on the timeline and available to automations. ``` curl -X POST https://app.getdeckle.com/api/v1/events \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "name": "order.completed", "email": "jane@example.com", "data": { "order_id": "ord_1234", "total": 4200, "currency": "usd" } }' ``` Body parameters namestringRequiredThe event name, e.g. user.signed_up. Use a stable, lowercase convention so your automation triggers stay consistent.emailstringOptionalThe contact this event belongs to. Used to match an existing contact; if omitted or unknown, the event is still recorded with contact_id set to null.dataobjectOptionalOptional structured payload with details about the action. Stored with the event and available to automation conditions. ## Linking to contacts Deckle matches an event to a contact by its email. When the address belongs to an existing contact, the event is attached to them and appears on their timeline immediately. Unknown emails are never dropped. If no contact matches — or you send an event without an email at all — the event is still recorded with contact_id set to null. This means you can start emitting events before a person becomes a contact, and the history is already there once they are. Emails are the key Contacts are keyed by email across all of Deckle. Send events with the same address you use when you create a contact, and everything lines up on one timeline. ## Powering automations Events are what make automations event-driven. An automation can use a custom event as its trigger, so posting order.completed can start a post-purchase flow the moment it happens. Events also wake automations that are waiting. A wait step pauses an enrollment until a specific event arrives (or a timeout elapses) — for example, wait for order.completed after a “complete your setup” nudge, and continue the moment the user follows through. Data drives conditions Automation condition steps can branch on the event’s data, so a single order.completed event can route high-value orders differently from the rest. ## Next steps AutomationsTrigger multi-step flows from events and wake waits when they arrive. Events API referenceThe full request and response schema for tracking events. ======================================================================== URL: https://app.getdeckle.com/docs/audience/segments Section: Audience ======================================================================== Audience # Segments Live, rule-based groups of contacts that recalculate as your audience changes — target them from campaigns and reference them by id in the API. A segment is a saved set of rules over your contacts. Instead of exporting a static list, you describe who belongs — subscribed contacts who fired order.completed in the last 30 days, say — and Deckle keeps the membership up to date for you. ## What is a segment A segment is a live, rule-based group of contacts. You define the rules once; Deckle evaluates them continuously against your audience. A contact is a member whenever they match the rules and drops out the moment they stop matching — you never maintain the list by hand. Segments are the marketing counterpart to your event-driven core: contacts are keyed by email, events record what they do, and a segment turns those attributes and events into a targetable audience for a campaign. Managed in the dashboard Segments are created and edited in the dashboard under Audience → Segments. There is no public write endpoint — you can’t create or update a segment over the API. You reference an existing segment by its id where a campaign accepts a segment. ## Rules A segment is built from rules, combined into AND / OR groups. Within a group you choose whether a contact must match every rule (AND) or any rule (OR), and you can nest groups to express more precise audiences. Each rule targets one of four categories. Rule typeMatches onExample attributeA contact field or custom key in the contact’s stored datadata.plan is pro eventWhether a named event was recorded for the contactfired order.completed at least once engagementSend activity — opens and clicks on your emailopened an email in the last 30 days subscribedThe contact’s subscription statesubscribed is true A typical segment mixes categories. To reach engaged pro customers, you might combine a subscribed rule, an attribute rule, and an engagement rule inside a single AND group. segment-rules.json ``` { "match": "and", "rules": [ { "type": "subscribed", "value": true }, { "type": "attribute", "field": "data.plan", "op": "eq", "value": "pro" }, { "match": "or", "rules": [ { "type": "event", "name": "order.completed", "op": "occurred" }, { "type": "engagement", "metric": "opened", "within_days": 30 } ] } ] } ``` Preview before you send The segment editor shows an estimated member count and a sample of matching contacts as you add rules, so you can sanity-check the audience before you target it from a campaign. ## Live membership Segment membership is not a snapshot. Deckle recalculates it as your data changes: when a contact is created or updated, when an event is recorded, and when engagement (an open or click) comes in. A contact enters or leaves the segment automatically based on whether they currently match the rules. ### How a contact moves in and out Because membership follows your data, ordinary API calls change who belongs — no separate segment call is needed. What triggers a recalculation contact changeattribute rulesOptionalUpserting a contact with POST /v1/contacts or updating custom data re-evaluates attribute and subscribed rules for that contact.event recordedevent rulesOptionalPosting an event with POST /v1/events can pull a contact into a segment whose rules require that event.engagementengagement rulesOptionalOpens and clicks reported back from your sends keep engagement-based rules current.For example, recording order.completed for a subscribed pro customer adds them to the segment above on the next evaluation — you only had to send the event. ``` import { Deckle } from "@deckle/sdk"; const deckle = new Deckle(process.env.DECKLE_API_KEY!); // Recording this event may move the contact into a segment // whose rules include order.completed. await deckle.events.create({ name: "order.completed", email: "jane@example.com", data: { order_id: "order_1234", amount: 4900 }, }); ``` ## Use in campaigns The main job of a segment is to target a campaign. When you create a campaign you set its target to segment and pass the segment’s id in the segment field. Membership is resolved when the campaign sends, so you reach whoever matches the rules at send time. ### Build the segment in the dashboard Open Audience → Segments, add your AND/OR rules, and save. Copy the segment’s id (it looks like seg_...) from the segment detail page. ### Target it from a campaign Create a draft campaign with target: "segment" and segment: "seg_...". The campaign will broadcast only to contacts in that segment. ### Send with a live key Send the campaign with POST /v1/campaigns/{id}/send using a sk_live_ key. Deckle resolves current membership and delivers to it. ``` import { Deckle } from "@deckle/sdk"; const deckle = new Deckle(process.env.DECKLE_API_KEY!); // Reference the segment by id — there is no segment write API. const campaign = await deckle.campaigns.create({ name: "Pro customer update", subject: "What's new for Pro", from_name: "Acme", from_email: "hello@acme.com", target: "segment", segment: "seg_engaged_pro", }); await deckle.campaigns.send(campaign.id); // live key only ``` Reference by id only The API reads segments but never writes them. Wherever a campaign accepts a segment, pass the id of a segment you built in the dashboard — you can’t define rules inline. ## Next steps CampaignsBroadcast to a segment or your whole list, with A/B subject lines and reports. ContactsCreate and update the contacts and custom data your segment rules match on. ======================================================================== URL: https://app.getdeckle.com/docs/audience/suppressions Section: Audience ======================================================================== Audience # Suppressions The suppression list is a per-project set of email addresses Deckle will never send to. It protects your sender reputation by keeping known-bad and opted-out recipients out of every send. Every send is checked against the suppression list first. If a recipient is on it, the send is rejected before it ever reaches AWS SES — so a hard bounce or complaint you’ve already seen can’t happen twice. ## What is the suppression list The suppression list is a project-scoped set of addresses that Deckle refuses to deliver to. It exists to protect your deliverability: repeatedly emailing an address that hard-bounced, or one that marked you as spam, is exactly what damages a sender’s reputation. Because Deckle uses Model A — you send from your own verified domain — that reputation is yours to protect. Suppression is enforced on every send path: transactional API sends, SMTP, campaigns, and automations. There is no way to opt an individual send out of the check. ## Automatic vs manual Addresses land on the list two ways. Most entries are added for you, but you can also curate the list by hand. ### Added automatically When AWS SES reports a hard bounce or a complaint, the notification flows back to Deckle over SNS and the recipient is suppressed automatically. You don’t have to do anything — the address is on the list before you could send to it again. Contacts who unsubscribe are suppressed the same way. ### Added manually You can add and remove addresses yourself, from the dashboard (Audience → Suppressions) or through the API. Manual entries are useful for addresses you already know are bad, or for honoring an opt-out request that came in over another channel. Removing an address clears it from the list so future sends go through again. Removing is not always safe Deleting a hard_bounce or complaint entry lets you send to that address again, but the underlying reason usually still holds. Only remove entries you know are safe to re-engage. ## Manage the list The SDK exposes list, create, and delete on deckle.suppressions. Listing is cursor-paginated like every other list endpoint. ``` import { Deckle } from "@deckle/sdk"; const deckle = new Deckle(process.env.DECKLE_API_KEY!); const { data, has_more, next_cursor } = await deckle.suppressions.list({ limit: 50, }); for (const entry of data) { console.log(entry.email, entry.reason); } ``` Full endpoint reference For request bodies, response shapes, and status codes, see the Suppressions API reference. ## Reasons Every entry carries a reason that records how the address got onto the list. ReasonAdded byWhat it means hard_bounceAutomaticSES reported a permanent bounce — the mailbox doesn’t exist or rejected mail outright. complaintAutomaticThe recipient marked a message as spam, reported via an SES feedback loop. unsubscribeAutomaticThe contact opted out through a one-click unsubscribe or the hosted preference page. manualYouAdded by hand in the dashboard or through the API. ## Sending is blocked Any send targeting a suppressed address is rejected with HTTP 422 and the standard error body. The message is never queued and never reaches SES. ``` { "error": "Recipient jane@example.com is suppressed (hard_bounce)" } ``` The reasons a send can be blocked at this stage are: - The recipient is on the suppression list for any reason (hard_bounce, complaint, unsubscribe, or manual). - To resume sending, remove the address with deckle.suppressions.delete(email) — but only once you know the underlying issue is resolved.Test the check The AWS SES simulator address bounce@simulator.amazonses.com triggers a hard bounce, which auto-suppresses it. Send to it once with a test key to watch the list update. ## Next steps DeliverabilityKeep complaint rates low and your sender reputation clean. Suppressions APIEndpoints, parameters, and responses for managing the list. ======================================================================== URL: https://app.getdeckle.com/docs/marketing/campaigns Section: Marketing ======================================================================== Marketing # Campaigns Broadcast a one-off or scheduled email to your whole list or a segment, with optional A/B subject lines and a full post-send report. A campaign is a broadcast: a single email you compose once and send to many contacts at once. Unlike a transactional send, a campaign has a target audience, respects the suppression list and one-click unsubscribe, and reports back on how it performed. ## What is a campaign Campaigns are for the email you send to people, not machines — a newsletter, a product announcement, a promotion. You pick an audience (everyone, or a segment), choose a template and subject, and send it as a one-off or on a schedule. Every campaign starts life as a draft. Drafts are the only editable state — once a campaign is sent it becomes an immutable record you can report on. Because campaigns are broadcasts to real inboxes, they always go out from a verified domain, include a visible unsubscribe link, and skip any address on your suppression list. Transactional vs. broadcast Sending a single receipt or magic link? Use emails.send instead. Campaigns are for reaching a list. ## Create a draft Create a campaign with campaigns.create. It returns a draft — nothing is sent until you call campaigns.send. Give it a name (for the dashboard), the subject and from identity your recipients will see, the template to render, and a target audience. ``` import { Deckle } from "@deckle/sdk"; const deckle = new Deckle(process.env.DECKLE_API_KEY!); const campaign = await deckle.campaigns.create({ name: "July product update", subject: "What's new in Acme this month", from_name: "Acme", from_email: "hello@acme.com", template: "tmpl_newsletter", target: "all", }); console.log(campaign.id); // camp_... ``` Campaign parameters namestringRequiredInternal label for the campaign, shown in the dashboard. Not visible to recipients.subjectstringRequiredThe subject line recipients see. Supports {{variable}} personalization.from_namestringRequiredDisplay name in the From header, e.g. Acme.from_emailstringRequiredSending address. Its domain must be verified — see /docs/domains/overview.templatestringRequiredThe tmpl_... id to render as the email body.targetstringRequiredWho receives the campaign: "all" or "segment".segmentstringOptionalThe segment id to target. Required when target is "segment".preview_textstringOptionalThe preheader shown after the subject in most inbox previews.variantsarrayOptionalAlternate subject lines for an A/B test — see below. ## Targeting A campaign’s target decides who receives it. Send to your entire audience, or narrow to a live segment — a rule-based group that recalculates as contacts change. Either way, unsubscribed and suppressed contacts are excluded automatically. ### Send to everyone ts ``` await deckle.campaigns.create({ name: "Launch announcement", subject: "We shipped something big", from_name: "Acme", from_email: "hello@acme.com", template: "tmpl_announcement", target: "all", }); ``` ### Send to a segment Set target to "segment" and pass the segment’s id. Segments have no public write API — build them in the dashboard under Audience → Segments, then reference the id here. ts ``` await deckle.campaigns.create({ name: "Win-back for lapsed users", subject: "We miss you", from_name: "Acme", from_email: "hello@acme.com", template: "tmpl_winback", target: "segment", segment: "seg_lapsed_30d", }); ``` Membership is live A segment recalculates as contacts and events change, so the recipient set is resolved at send time — not when you create the draft. ## A/B subject lines Test which subject line lands better by passing a variants array. The subject you set on the campaign is variant A (the base); each entry in variants is an alternate. You can add up to three alternates — B, C, and D — for four subject lines in total. ts ``` await deckle.campaigns.create({ name: "July product update", subject: "What's new in Acme this month", // variant A (base) from_name: "Acme", from_email: "hello@acme.com", template: "tmpl_newsletter", target: "all", variants: [ "Your July Acme roundup", // variant B "3 new things in Acme", // variant C ], }); ``` Only the subject line changes between variants — the from identity, template, and target stay the same. Open and click rates are reported per variant so you can see which subject won. ## Send it Sending is a separate, deliberate step. Call campaigns.send with the campaign’s id to broadcast the current draft to its target audience. send-campaign.ts ``` import { Deckle } from "@deckle/sdk"; const deckle = new Deckle(process.env.DECKLE_API_KEY!); await deckle.campaigns.send("camp_8f2a1c"); ``` ### Deckle resolves the audience The target — all contacts or the segment — is expanded at send time, then filtered against unsubscribes and the suppression list. ### Emails are rendered and delivered Your template is rendered per recipient (with any {{variable}} personalization), an unsubscribe link and one-click headers are added, and each message is delivered through your verified domain. ### Results stream into the report As SES reports delivery, opens, clicks, bounces, and complaints, the campaign report fills in.Sending requires a LIVE key Campaign sends only work with a sk_live_ key. With Redis configured, delivery runs asynchronously on the worker so large lists don’t block your request. Without a worker, sends run synchronously and are capped at 200 recipients. ## Reports Once a campaign is sent, its report tracks how recipients engaged. Fetch it with campaigns.get, or open the campaign in the dashboard. Metrics update as SES and SNS deliver notifications back to Deckle. MetricWhat it counts sendsRecipients the campaign was dispatched to after filtering. deliveredMessages SES confirmed as delivered to the inbox provider. opensRecipients who opened the email (tracked via the open pixel). clicksRecipients who clicked a tracked link in the email. bouncesMessages that hard- or soft-bounced. Hard bounces are auto-suppressed. complaintsRecipients who marked the email as spam. These are auto-suppressed too. unsubscribesRecipients who opted out via the unsubscribe link or one-click header. Keep complaints low Bounces and complaints suppress the address automatically. Watch your complaint rate — the deliverability guide covers the thresholds bulk senders need to stay under. ## Next steps AutomationsGo beyond one-off broadcasts with event-driven, multi-step email flows. Campaigns API referenceEvery parameter and response for creating, listing, and sending campaigns. ======================================================================== URL: https://app.getdeckle.com/docs/marketing/automations Section: Marketing ======================================================================== Marketing # Automations Build multi-step email workflows that react to what your contacts do — welcome series, onboarding drips, and lifecycle flows that run themselves. An automation is a trigger plus an ordered set of steps. When something happens — a contact subscribes, or you fire a named event — Deckle enrolls the contact and walks them forward through the flow: sending email, waiting, branching, and updating their record along the way. ## What is an automation Each automation has a single trigger and a list of steps that form a forward flow (a DAG — steps always point onward, never back). A contact enters at the trigger and moves from one step to the next until the flow ends or a branch sends them down a different path. Steps run in order. A delay or wait step parks the contact until its time or event arrives, then the flow resumes exactly where it left off. Every enrolled contact keeps its own position, so thousands of people can be at different points in the same automation at once. Contacts are the unit of enrollment Automations operate on contacts, keyed by email. A step can send a template, wait, branch on a field, or change the contact’s record — always for the one contact moving through the flow. ## Triggers A trigger decides who enters the automation and when. There are two kinds. TriggerFires when contact.subscribedA contact becomes subscribed — for example a newly created subscribed contact, which auto-enrolls in your live welcome flows. eventA custom event you fire by name (such as user.signed_up or order.completed) matches the automation’s configured event. For an event trigger, name the event the automation should listen for. Any event you send with that name enrolls the matching contact. The same event stream can also wake a wait step mid-flow — see below. ## Step types Steps are the building blocks of the flow. Each step has a type that determines what it does and where the contact goes next. TypeWhat it does emailSends a template to the contact. Optionally override the subject line for this step. delayWaits a fixed amount of time before continuing — delayMinutes from 1 to 525600 (one year). waitPauses until a named event arrives (waitEvent) or a timeout elapses (waitTimeoutMinutes, 1 to 36000). Whichever comes first resumes the flow. condition / branchAn if/else test on a field, op, and value. A match routes to nextTrue; otherwise to nextFalse. actionChanges the contact record: unsubscribe, update-field, add-tag, or remove-tag. Branches keep the flow moving forward A condition step splits the path in two but never loops back. Point nextTrue and nextFalse at the steps each side should continue to, and leave one unset to end that branch. ## Build it visually Most automations start in the dashboard. Open Automations in the top nav to design a flow on a canvas — no code required. ### Create an automation In the dashboard, open Automations and create a new flow. Give it a name so you can find it later. ### Pick a trigger Choose contact.subscribed or an event trigger and, for events, type the event name to listen for. ### Add steps Drop in email, delay, wait, condition, and action steps and connect them into the order you want. Branch lanes show the true and false paths side by side. ### Go live Switch the status from draft to live to start enrolling contacts. Draft flows never enroll anyone. ## Via the API You can also define an automation programmatically. Pass a trigger and an ordered array of steps to automations.create. The example below sends a welcome email, waits three days, branches on whether the contact has opened, and tags them accordingly. ``` import { Deckle } from "@deckle/sdk"; const deckle = new Deckle(process.env.DECKLE_API_KEY!); const automation = await deckle.automations.create({ name: "Welcome series", trigger: { type: "contact.subscribed" }, steps: [ { id: "welcome", type: "email", template: "tmpl_welcome", subjectOverride: "Welcome to Acme 👋", }, { id: "wait_3d", type: "delay", delayMinutes: 4320 }, { id: "check_opened", type: "condition", field: "opened", op: "eq", value: true, nextTrue: "tag_engaged", nextFalse: "nudge", }, { id: "tag_engaged", type: "action", action: "add-tag", value: "engaged" }, { id: "nudge", type: "email", template: "tmpl_nudge" }, ], }); ``` A new automation is created as a draft. Manage it with automations.list, automations.get, automations.update, and automations.delete, or see the full API reference. ## Going live An automation is either draft or live. Draft flows are editable but inert — no one is enrolled and no steps run. Setting the status to live starts enrolling matching contacts. - Draft — the build state. Change the trigger and steps freely; nothing fires. - Live — the automation enrolls contacts and, on going live, resumes any enrollments that were paused while it was a draft, so contacts pick up where they left off.Test before you flip it live Once live, real contacts start moving through the flow and receiving email. Preview each template and double-check delays and branches before changing the status. ## Next steps EventsFire named events to trigger automations and wake steps waiting on a signal. Automations APIEvery field for creating, updating, and managing automations over the API. CampaignsBroadcast one-off and scheduled sends to your whole list or a segment. ======================================================================== URL: https://app.getdeckle.com/docs/marketing/template-gallery Section: Marketing ======================================================================== Marketing # Template gallery A built-in library of high-quality, deliverability-safe email templates you can brand, preview, and export — or send by id straight from the API. Every Deckle project ships with a gallery of ready-made templates. Pick one, let your brand kit apply automatically, and either send it by id or export the markup. No template starts from a blank canvas. ## A gallery, in the product Open Templates in the dashboard top-nav to browse dozens of embedded templates authored with React Email. Each one renders to clean, inline-CSS HTML with a plain-text alternative, so it lands well in real inboxes — including the clients that strip