Skip to content

About

Official Node.js / TypeScript SDK for the Zalify API (email sending via Zalify Reach)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

@zalify/sdk

The official Node.js / TypeScript SDK for the Zalify API.

It sends transactional and marketing email from your own domains through Zalify, with retries, idempotency, and typed errors. It has no runtime dependencies and needs Node 18+ (or any runtime with fetch).

npm install @zalify/sdk

Before you start

  1. In your Zalify workspace, go to Settings → Email and add a sending domain, then publish the DNS records it shows (DKIM TXT + MAIL FROM CNAME). Add a _dmarc TXT too (v=DMARC1; p=none to start).
  2. In Settings → Developer, create an API key and tick Send email (and Manage contacts to add contacts). A key can only do what it was created with.
  3. Keep the key on your server, e.g. in ZALIFY_API_KEY. Never ship it to a browser.

Send an email

import { Zalify } from "@zalify/sdk";

const zalify = new Zalify(process.env.ZALIFY_API_KEY);

const { id } = await zalify.emails.send(
  {
    from: "Acme <notifications@acme.com>",
    to: "user@example.com",
    replyTo: "support@acme.com",
    subject: "Reset your password",
    html: "<p>…</p>",
    text: "…",
    tags: { kind: "password_reset" },
  },
  { idempotencyKey: `reset:${tokenId}` }
);
  • type defaults to "transactional": delivered at once, no unsubscribe link. Use type: "marketing" for newsletters and announcements. They get an unsubscribe link and headers, and only reach recipients who are subscribed contacts in your workspace.
  • to is one address. Send one message per recipient, or use a batch.
  • A recipient on your suppression list (an earlier hard bounce or spam complaint) still gets an id, but the message is not delivered.
  • Each day your workspace can email a limited number of addresses it has never emailed before. New workspaces start at 1,000 a day for transactional mail and 500 for marketing. The limit doubles after each day with low bounce and complaint rates, and goes away once it reaches 50,000. Over the limit, sends fail with new_recipient_limit (marketing waits until the next day). People you've emailed before are never limited.

Send from a sender set up in Zalify

Instead of from, pass senderEmailId: a sender email from Settings → Email. Its name, address and Reply-To are used (your replyTo still wins), so they're managed in Zalify rather than in your code.

const { data: senders } = await zalify.emails.listSenders();
// [{ id, name, email, replyTo, verified }]

await zalify.emails.send(
  { senderEmailId: process.env.ZALIFY_SENDER_EMAIL_ID, to, subject, html },
  { idempotencyKey: `reset:${tokenId}` }
);

A message takes from or senderEmailId, not both. A sender that isn't in your workspace or isn't verified yet fails with unverified_sender.

Send a batch

Send up to 100 messages in one request. A refused message doesn't fail the others.

const { data } = await zalify.emails.sendBatch(messages);
for (const [i, result] of data.entries()) {
  if ("error" in result) console.warn(messages[i].to, result.error.code);
}

Contacts

Create or update a contact by email, e.g. when someone signs up. The key needs the Manage contacts permission.

const contact = await zalify.contacts.upsert({
  email: "user@example.com",
  firstName: "Ada",
  properties: { plan: "pro" },
  subscribed: true, // they ticked "send me product updates"
  lists: [PRODUCT_UPDATES_LIST_ID],
});
// { id, email, subscribed: true, lists: [{ id, status: "joined" }] }
  • subscribed: true subscribes the contact to marketing email, false unsubscribes, and leaving it out keeps what's there. It's taken as given, so only send true for people who opted in. New senders start with a small daily volume that grows while bounces and complaints stay low.
  • Marketing email (type: "marketing") only reaches subscribed contacts.
  • Contacts are matched by email. A phone that already belongs to another contact isn't attached (phoneSkipped: true).
  • Adding a contact to a list can start that list's automations, such as a welcome series.
  • Calls are safe to retry; the same email updates the same contact.

Errors, retries and idempotency

Every failure is a ZalifyError with a stable code, the HTTP status, and details for validation errors.

import { ZalifyError } from "@zalify/sdk";

try {
  await zalify.emails.send(message);
} catch (error) {
  if (error instanceof ZalifyError && error.code === "unverified_sender") {
    // The from address isn't on a verified domain.
  }
  throw error;
}
code status meaning
unauthorized 401 missing or invalid API key
missing_scope 403 the key lacks the permission: Send email or Manage contacts
email_not_enabled 403 the workspace has no sending set up yet
contacts_not_enabled 403 the workspace has no contacts set up yet
list_not_found 404 a list in lists isn't in your workspace (nothing was written)
usage_paused 403 email sending is paused for the workspace (plan or balance)
invalid_request 400 the body doesn't match the schema (see details)
unverified_sender 422 the from address isn't on a verified domain
not_subscribed 422 a marketing recipient isn't a subscribed contact
idempotency_conflict 409 the idempotency key was used for a different request
rate_limited 429 over 120 requests a minute for this key (a batch counts once)
new_recipient_limit 429 the recipient is new to your workspace and today's allowance of new recipients is used; it resets at 00:00 UTC (retryAfter)
unavailable 503 temporary; safe to retry with the same idempotency key

The SDK retries rate_limited, unavailable, idempotency_in_progress, timeouts and network errors on its own, up to maxRetries (default 3). It honors Retry-After, and won't wait longer than maxRetryDelayMs (default 10 s). Retries always reuse the request's idempotency key, so they never send twice.

Pass your own idempotencyKey per logical email, such as reset:${tokenId} or invite:${inviteId}. Then even retrying the whole call after a crash or deploy sends once. Without one, the SDK makes a key per call, which only covers its own retries.

Options

new Zalify({
  apiKey: "zalify_api_…",                   // default: process.env.ZALIFY_API_KEY
  baseUrl: "https://app.zalify.com/api/v1", // default
  maxRetries: 3,
  timeoutMs: 30_000,                        // per attempt
  maxRetryDelayMs: 10_000,
  fetch: customFetch,                       // optional
});

Webhooks

Add an endpoint in Settings → Developer → Webhooks to get events for email sent through the API:

  • email.sent
  • email.delivered
  • email.bounced, with bounce_type hard or soft
  • email.complained
  • email.opened
  • email.clicked, with url
  • contact.unsubscribed

The signing secret (whsec_…) is shown once, when you create the endpoint.

Verify every request against the raw body before trusting it:

import express from "express";
import { verifyWebhook, WebhookVerificationError } from "@zalify/sdk";

app.post("/webhooks/zalify", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    event = verifyWebhook(req.body, req.headers, process.env.ZALIFY_WEBHOOK_SECRET!);
  } catch (error) {
    if (error instanceof WebhookVerificationError) return res.status(400).end();
    throw error;
  }
  if (event.type === "email.bounced" && event.data.bounce_type === "hard") {
    // stop emailing event.data.to
  }
  res.status(204).end();
});

In a Next.js route handler: verifyWebhook(await req.text(), req.headers, secret). Or construct the client with new Zalify({ webhookSecret }) and call zalify.webhooks.verify(body, headers).

  • Requests follow the Standard Webhooks spec.
  • webhook-id stays the same across retries, so use it to drop duplicates.
  • Answer 2xx within 10 s. Failed deliveries are retried for about 13 hours.
  • Events can arrive out of order.

Coming from Resend or Postmark

The send call takes the same shape as Resend's emails.send. The differences:

  • to is a single address.
  • replyTo is a single address.
  • tags is an object, not an array.

For Postmark, map From/To/Subject/HtmlBody/TextBody/ReplyTo to from/to/subject/html/text/replyTo. Message streams become type: transactional or marketing.

Roadmap

  • Listing contacts and lists; batch upsert
  • A sandbox key that renders without delivering

API reference: https://app.zalify.com/api/v1/reference

License

MIT

About

Official Node.js / TypeScript SDK for the Zalify API (email sending via Zalify Reach)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages