DeckleDocs
Dashboard

SDKs

Node.js SDK

The official @deckle/sdk package — a thin, fully typed wrapper over the Deckle REST API for sending email and managing your audience.

@deckle/sdk gives you a typed client for every Deckle resource. It has zero dependencies, ships with TypeScript types out of the box, and runs anywhere modern fetch is available.

Runtime support

The SDK targets Node 18+ and also runs on Deno, Bun, and the browser. It relies only on the built-in fetch, so there is nothing else to install.

Install#

Add the package to your project with your package manager of choice.

bash
npm install @deckle/sdk

Initialize#

Create a client with a secret API key. Keep the key in an environment variable — never commit it to source control. Generate one in the dashboard under Developers → API keys.

deckle.ts
import { Deckle } from "@deckle/sdk";

const deckle = new Deckle(process.env.DECKLE_API_KEY!);

const { id } = 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("Sent", id);

Test vs. live

A sk_test_ key only delivers to the SES simulator or your own verified domains, so you can wire up the SDK safely before going live. Swap in a sk_live_ key to send to real inboxes.

Resources#

Every REST resource is exposed as a namespace on the client. Method names map directly to the underlying API, and all inputs and responses are fully typed.

ResourceMethods
deckle.emailssend, get, list
deckle.contactscreate, list, get, update, delete
deckle.eventscreate
deckle.suppressionslist, create, delete
deckle.campaignscreate, list, get, send
deckle.templatescreate, list, get
deckle.automationscreate, list, get, update, delete

Here are a few of them in context — send an email, upsert a contact, and record an event.

usage.ts
// Send a transactional email
await deckle.emails.send({
  from: "Acme <hello@acme.com>",
  to: "jane@example.com",
  subject: "Your receipt",
  template: "tmpl_receipt",
  variables: { order_id: "order_1234", total: "$49.00" },
});

// Upsert a contact by email
await deckle.contacts.create({
  email: "jane@example.com",
  name: "Jane Doe",
  data: { plan: "pro" },
});

// Record an event that can trigger an automation
await deckle.events.create({
  name: "order.completed",
  email: "jane@example.com",
  data: { order_id: "order_1234" },
});

List endpoints are cursor-based and return { data, has_more, next_cursor }. Pass limit and cursor to page through results.

ts
let cursor: string | null = null;

do {
  const page = await deckle.emails.list({ limit: 100, cursor });
  for (const email of page.data) {
    console.log(email.id, email.status);
  }
  cursor = page.next_cursor;
} while (cursor);

Handling errors#

Any non-2xx response throws a DeckleError. It exposes the HTTP status, a human-readable message, and the parsed response body (the { error } payload) so you can branch on the exact failure.

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: "suppressed@example.com",
    subject: "Hello",
    html: "<p>Hi</p>",
  });
} catch (err) {
  if (err instanceof DeckleError) {
    console.error(err.status);   // e.g. 422
    console.error(err.message);  // "Recipient is suppressed"
    console.error(err.body);     // { error: "Recipient is suppressed" }
  } else {
    throw err;
  }
}

Rate limits

A burst past 120 requests / minute throws a DeckleError with status 429. Read the Retry-After header (in seconds) from the response and back off before retrying.

Verify webhooks#

Deckle signs every webhook delivery with HMAC-SHA256 over the raw request body. Use the verifyWebhookSignature helper to confirm a payload really came from Deckle before you trust it. Pass the raw body string — not a re-serialized object — along with the X-Deckle-Signature header and your endpoint's whsec_ secret.

webhook.ts
import { verifyWebhookSignature } from "@deckle/sdk";

// e.g. an Express route mounted with a raw body parser
app.post("/webhooks/deckle", (req, res) => {
  const signature = req.header("X-Deckle-Signature")!;
  const valid = verifyWebhookSignature(
    req.body, // the raw request body
    signature,
    process.env.DECKLE_WEBHOOK_SECRET!,
  );

  if (!valid) {
    return res.status(400).send("Invalid signature");
  }

  const event = JSON.parse(req.body);
  console.log(event.type); // "email.delivered", "email.opened", ...
  res.sendStatus(200);
});

Raw body required

The signature is computed over the exact bytes Deckle sent. Verify against the raw body before any JSON parsing or middleware re-encodes it. See Verify webhooks for framework-specific setup.

Options#

The constructor accepts an optional second argument. Override baseUrl to point at a different host, or pass a custom fetch implementation to add retries, logging, or proxying.

ts
import { Deckle } from "@deckle/sdk";

const deckle = new Deckle(process.env.DECKLE_API_KEY!, {
  baseUrl: "https://app.getdeckle.com/api/v1",
  fetch: customFetch, // your own fetch-compatible function
});

Next steps#