An API is only as good as the code you write against it. You can always reach Zend with raw HTTP, but then you spend your time serializing bodies, checking status codes, and mapping fields by hand. @usezend/node does that part for you. It is the official Node.js and TypeScript client, and it is built around a few decisions that keep it out of your way. Here is how it works.
Install and authenticate
There is one package for every channel:
npm i @usezend/node
Create a client with your API key, or set ZEND_API_KEY in your environment and construct it with no arguments:
import { Zend } from '@usezend/node';
const zend = new Zend('sent_live_...');
// or, reading ZEND_API_KEY from the environment:
const client = new Zend();
Pointing at a staging environment, or want a different timeout? Pass options, or set ZEND_BASE_URL:
const zend = new Zend('sent_live_...', {
baseUrl: 'https://staging.api.tryzend.com',
timeout: 30_000,
});
It never throws
This is the decision that shapes everything else. Every call resolves to the same small result: either data, or an error, never both. It does not throw for API errors or network failures, which means one bad send can never crash a request handler you forgot to wrap in try/catch.
const { data, error } = await zend.emails.send({
from: 'you@yourdomain.com',
to: 'customer@example.com',
subject: 'Welcome aboard',
html: '<p>Glad to have you.</p>',
});
if (error) {
console.error(error.name, error.statusCode, error.message);
return;
}
console.log('Sent', data.id);
The error is a ZendError, which extends the normal Error and adds a name for the category, such as api_error or timeout, a statusCode when there is one, and a provider code when available. You handle failure as a value you check, right where it happens, instead of somewhere up the stack.
Typed, and idiomatic in both directions
The whole surface is typed, so your editor autocompletes every field and flags a wrong one before you run anything. Just as importantly, you never touch the wire format. You write natural camelCase, and the SDK translates to the API's snake_case on the way out and back:
await zend.messages.send({
to: '+233201234567',
body: 'Your code is 123456',
preferredChannels: ['whatsapp', 'sms'], // becomes preferred_channels on the wire
fallbackEnabled: true,
});
Responses come back normalised too, so an identifier is always id, not the raw _id, and internal bookkeeping fields are stripped. What you get is the shape you would have designed yourself.
One client, every channel
The same client reaches all of Zend. You learn one shape and reuse it:
// Email
await zend.emails.send({ from, to, subject, html });
// SMS or WhatsApp
await zend.messages.send({ to, body, preferredChannels: ['sms'] });
// Voice
await zend.voice.send({ recipients: [to], text });
// Templates, read-only
const { data } = await zend.templates.list({ category: 'transactional' });
Beyond sending, messages also does sendBulk, get, list, cancel, and retry, and voice can upload an audio file and fetch a batch. It is one mental model for the whole platform.
No runtime dependencies
The SDK ships with zero runtime dependencies. It uses the platform's own fetch and FormData, so it needs Node 18 or newer and adds nothing to your bundle. Less to audit, less to update, and nothing pinned between you and the API.
Get started
- Node.js SDK for the full reference
- Quickstart to send your first message
@usezend/nodeon npm
Install it, drop in your key, and send something. The first call is a few lines, and it works the same for every channel after that.
