DeckleDocs
Dashboard

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 <hello@acme.com>",
  to: "jane@example.com",
  subject: "Welcome to Acme 👋",
  html: "<h1>Hey Jane</h1><p>Thanks for joining.</p>",
});

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 <hello@acme.com>",
  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 <hello@acme.com>",
    to: "jane@example.com",
    subject: "Welcome to Acme 👋",
    html: "<h1>Hey Jane</h1>",
  });
} 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#