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.
npm install @deckle/sdkGrab 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 infrom 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.
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.
// 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.
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.