The official JavaScript SDK is published on npm as node-mailjet and developed at mailjet/mailjet-apiv3-nodejs. It is written in TypeScript and ships full type coverage.
The SDK runs in Node.js and in the browser. In the browser a proxy is currently required because of CORS restrictions — and your private API key must never be shipped in frontend code.
Officially supported Node.js versions: >= 12.x.
Version 7.0.0 contains breaking changes. In short:
- Entry points are restricted.
package.jsonnow declares anexportsmap, so onlyrequire('node-mailjet')andimport ... from 'node-mailjet'resolve. Deep imports such asrequire('node-mailjet/dist/mailjet.node.js')no longer work. .id()must come before.action(). Calling.id()after.action()now throws instead of silently building an incorrect URL.contactdataPUT is validated locally. APUTtocontactdatathrows synchronously ifDatais missing, is not an array, or contains entries withoutName/Valuekeys — instead of failing later with a server-side400.- Requests time out after 30s by default instead of hanging indefinitely. Pass
options.timeoutto change it.
The full list is in the CHANGELOG.
npm install node-mailjetThe Email API and Send API authenticate with your API key and secret. The SMS API uses a bearer token generated in the SMS section of your account.
export MJ_APIKEY_PUBLIC='your API key'
export MJ_APIKEY_PRIVATE='your API secret'
export MJ_API_TOKEN='your API token'CommonJS and native ESM (including named imports) are both supported:
const Mailjet = require('node-mailjet');import Mailjet, { Client } from 'node-mailjet';const mailjet = new Mailjet({
apiKey: process.env.MJ_APIKEY_PUBLIC,
apiSecret: process.env.MJ_APIKEY_PRIVATE
});Or with the apiConnect helper:
const mailjet = Mailjet.apiConnect(
process.env.MJ_APIKEY_PUBLIC,
process.env.MJ_APIKEY_PRIVATE,
{
config: {},
options: {}
}
);const mailjet = Mailjet.smsConnect(process.env.MJ_API_TOKEN);const Mailjet = require('node-mailjet');
const mailjet = Mailjet.apiConnect(
process.env.MJ_APIKEY_PUBLIC,
process.env.MJ_APIKEY_PRIVATE,
);
mailjet
.post('send', { version: 'v3.1' })
.request({
Messages: [
{
From: {
Email: 'pilot@example.com',
Name: 'Mailjet Pilot'
},
To: [
{
Email: 'passenger1@example.com',
Name: 'Passenger 1'
}
],
Subject: 'Your email flight plan!',
TextPart: 'Dear passenger 1, welcome to Mailjet!',
HTMLPart: '<h3>Dear passenger 1, welcome to Mailjet!</h3>'
}
]
})
.then((result) => console.log(result.body))
.catch((err) => console.log(err.statusCode));Requests are composed from a method, a resource, a config object and the request data:
const mailjet = new Mailjet({
apiKey: process.env.MJ_APIKEY_PUBLIC,
apiSecret: process.env.MJ_APIKEY_PRIVATE,
config: CONFIG,
options: OPTIONS
});
const request = mailjet
.METHOD(RESOURCE, CONFIG)
.request(DATA, PARAMS, PERFORM_API_CALL);| Argument | Meaning |
|---|---|
METHOD | post, put, get or delete |
RESOURCE | the API endpoint to call |
CONFIG | connection config — see Config |
OPTIONS | connection options — see Options |
DATA | request body (post, put, delete) |
PARAMS | URL query parameters |
PERFORM_API_CALL | boolean; set false to build the request without sending it |
Config and options can be set once on the client and reused for every request, or passed per request — a per-request config takes precedence.
| Option | Effect |
|---|---|
headers | additional request headers |
timeout | milliseconds before the request is aborted (default 30000 since v7) |
proxy | proxy host, port, protocol and auth (Node only) |
maxBodyLength | maximum request content size in bytes (Node only) |
maxContentLength | maximum response content size in bytes (Node only) |
const mailjet = new Mailjet({
apiKey: process.env.MJ_APIKEY_PUBLIC,
apiSecret: process.env.MJ_APIKEY_PRIVATE,
options: {
timeout: 1000,
maxBodyLength: 1500,
maxContentLength: 100,
headers: {
'X-API-Key': 'foobar',
},
proxy: {
protocol: 'http',
host: 'www.test-proxy.com',
port: 3100,
}
}
});Options are passed through to the underlying HTTP client — see the axios request config for details.
| Key | Effect |
|---|---|
host | base host name (default api.mailjet.com) |
version | API version — v3, v3.1 or v4 |
output | response data type |
| Version | Scope |
|---|---|
v3 | Email API |
v3.1 | Send API v3.1 (latest send version) |
v4 | SMS API, contact delete API |
Most Email API endpoints sit under v3, which is the default and does not need to be specified. For anything else, set version:
const request = mailjet.post('send', { version: 'v3.1' });const request = mailjet.post('send', { version: 'v3.1', host: 'api.us.mailjet.com' });Accounts on Mailjet's US architecture must set api.us.mailjet.com.
The default output is json. Supported values: arraybuffer, document, json, text, stream, and blob (browser only).
const request = mailjet.post('send', { version: 'v3.1', output: 'arraybuffer' });Useful in tests, to build and inspect a request without reaching Mailjet:
const request = mailjet
.post('send', { version: 'v3.1' })
.request({}, {}, false);All Mailjet types are exported from the package entry point:
import {
Contact,
SendEmailV3,
SendEmailV3_1,
Message,
Segmentation,
Template,
SendMessage,
Webhook
} from 'node-mailjet';Request.request<TResult>(data, params, performAPICall) is generic, so responses are typed:
import { Client, SendEmailV3_1, LibraryResponse } from 'node-mailjet';
const mailjet = new Client({
apiKey: process.env.MJ_APIKEY_PUBLIC,
apiSecret: process.env.MJ_APIKEY_PRIVATE
});
const data: SendEmailV3_1.Body = {
Messages: [
{
From: { Email: 'pilot@example.com' },
To: [{ Email: 'passenger@example.com' }],
Subject: 'Your email flight plan!',
HTMLPart: '<h3>Dear passenger, welcome to Mailjet!</h3>',
TextPart: 'Dear passenger, welcome to Mailjet!',
},
],
};
const result: LibraryResponse<SendEmailV3_1.Response> = await mailjet
.post('send', { version: 'v3.1' })
.request(data);
const { Status } = result.body.Messages[0];For library versions 3.*.* and below, use the community-maintained @types/node-mailjet package instead.
Send API v3.1 accepts multiple emails in one request through the Messages array. This is not an atomic operation. Each entry is validated and processed independently, so a single request can return a mix of successes and failures. A 2xx response only means Mailjet accepted the request for processing — it does not mean every message was sent, and one failing message does not stop the others.
Always inspect the Status of each entry in result.body.Messages:
success— the message was accepted and is being delivered.error— the message failed; details are in that entry'sErrorsarray (ErrorCode,ErrorMessage,ErrorIdentifier).
Set AdvanceErrorHandling: true to get one Errors entry per validation problem, with machine-readable codes.
const data: SendEmailV3_1.Body = {
Messages: [
{
From: { Email: 'pilot@example.com' },
To: [{ Email: 'passenger1@example.com' }],
Subject: 'Your flight plan (1)',
TextPart: 'This message is expected to succeed.',
CustomID: 'order-1001',
},
{
From: { Email: 'pilot@example.com' },
To: [{ Email: 'not-a-valid-address' }],
Subject: 'Your flight plan (2)',
TextPart: 'This message is expected to fail.',
CustomID: 'order-1002',
},
],
AdvanceErrorHandling: true,
};
const result: LibraryResponse<SendEmailV3_1.Response> = await mailjet
.post('send', { version: 'v3.1' })
.request(data);
const succeeded = result.body.Messages.filter((message) => message.Status === 'success');
const failed = result.body.Messages.filter((message) => message.Status !== 'success');Note that failed being non-empty does not mean the promise rejected — the request itself succeeded.
Blindly retrying a whole batch resends messages that already succeeded. To retry safely:
- Give every message a unique
CustomIDbefore you send it, and record it against your own order or notification ID. - Collect only the messages whose
Statusiserror— never re-include successes. - Use the failed entry's
Errors[].StatusCode/ErrorCodeto decide whether a retry makes sense. Validation errors (invalid recipient, malformed address, blocked domain) will fail again unless the message itself changes; transient errors (StatusCode >= 500) may succeed. - Resubmit only the failed messages, reusing the same
CustomID, so retries stay correlated in your logs and in Mailjet's. - Treat a request-level rejection (network failure, auth failure, malformed payload) differently: Mailjet never processed the batch, so the entire
Messagesarray is safe to retry as-is.
A runnable example lives in examples/node/src/batchSend.js.
mailjet
.post('contact')
.request({
Email: 'passenger@example.com',
IsExcludedFromCampaigns: true,
Name: 'New Contact'
})
.then((result) => console.log(result.body))
.catch((err) => console.log(err.statusCode));Call .id() before .action():
mailjet
.post('contact')
.id(contactID)
.action('managecontactslists')
.request({
ContactsLists: [
{
ListID: listID,
Action: 'addnoforce'
}
]
})
.then((result) => console.log(result.body))
.catch((err) => console.log(err.statusCode));mailjet.get('contact').request();Query parameters go in the second .request() argument:
mailjet.get('contact').request({}, { IsExcludedFromCampaigns: false });mailjet.get('contact').id(contactID).request();For the contact and contactdata resources, .id() also accepts a contact's email address instead of the numeric ID (.id('user@example.com')). Pass it as-is — the SDK does not URL-encode it, which matches what the REST API expects.
A PUT in the Mailjet API behaves like a PATCH: only the properties you send are updated, and non-mandatory properties can be omitted.
Each entry in Data must be an object with Name and Value keys, where Name matches a property already defined via /contactmetadata. Sending the property directly ({ first_name: 'John' }) instead of { Name: 'first_name', Value: 'John' } returns an Invalid key name error.
mailjet
.put('contactdata')
.id(contactID)
.request({
Data: [
{ Name: 'first_name', Value: 'John' },
{ Name: 'last_name', Value: 'Smith' }
]
});PUT / POST on the template resource only updates metadata (Name, Categories, Purposes, …). It does not touch the template body, so changes made that way appear in a GET template response but never in the Mailjet App editor. To update the actual content, target the template/{id}/detailcontent sub-resource:
mailjet
.put('template')
.id(templateID)
.action('detailcontent')
.request({
'Html-part': '<html><body><p>Hello {{var:name}}</p></body></html>',
'Text-part': 'Hello {{var:name}}'
});A successful DELETE returns 204 No Content with no response body.
mailjet.delete('template').id(templateID).request();A 204 here — and a subsequent 404 on GET template/{id} — confirms the template was removed API-side.
Contact deletion uses a dedicated v4 endpoint, so the resource name is the plural contacts and the version must be set explicitly:
await mailjet
.delete('contacts', { version: 'v4' })
.id(contactID)
.request();See Delete a contact for details.
const Mailjet = require('node-mailjet');
const mailjet = Mailjet.smsConnect(process.env.MJ_API_TOKEN, {
config: { version: 'v4' }
});
mailjet
.post('sms-send')
.request({
Text: 'Have a nice SMS flight with Mailjet!',
To: '+33600000000',
From: 'MJPilot'
})
.then((result) => console.log(result.body))
.catch((err) => console.log(err.statusCode));The repository includes runnable examples for several environments:
- Browser — RequireJS-based request playground
- Node — simple request scripts
- Sendmail — Express app that lists contacts and sends email
- React — React request playground
- Firebase — Firebase Functions for static and query-driven sends
Browser-side examples still require a proxy because of CORS.
The SDK is open source. Fork the repository, branch, implement your fix or feature, document it, and open a pull request at mailjet/mailjet-apiv3-nodejs. Documentation improvements belong in the API documentation repo.