@wesend/node
v0.2.1
Published
Official Node.js SDK for the WeSend API: SMS, email and one-time code verification for Mongolia.
Maintainers
Readme
WeSend Node.js SDK
The official Node.js library for the WeSend API. Send SMS to Mongolian mobile numbers, send email, and verify users with one-time codes.
- Typed requests and responses, for TypeScript and JavaScript
- No dependencies
- Retries that never send a message twice
Full API reference: wesend.mn/docs
Install
npm install @wesend/nodeRequires Node.js 20 or newer. Works with ESM (import) and CommonJS (require).
Quick start
Create an API key in the WeSend dashboard and keep it in the WESEND_API_KEY environment variable.
import { WeSend } from '@wesend/node'
const wesend = new WeSend() // reads WESEND_API_KEY
const message = await wesend.sms.send({
to: '99112233',
text: 'Tanii zahialga batalgaajlaa. #1042',
})
console.log(message.id, message.status, message.balance)You can also pass the key yourself: new WeSend('ws_...').
Use the SDK only on your server. Never put an API key in browser or mobile app code: anyone can read it there and send with your credits.
SMS
// One number. When this resolves, the message has been handed to the operator.
const message = await wesend.sms.send({ to: '99112233', text: 'Sain baina uu' })
// Up to 1,000 numbers. The messages are queued and sent in the background.
const batch = await wesend.sms.sendBulk({
to: ['99112233', '88112233', '95112233'],
text: 'Shine baraa irlee!',
})const email = await wesend.email.send({
to: '[email protected]',
subject: 'Таны захиалга баталгаажлаа',
html: '<h1>Баярлалаа!</h1><p>Таны #1042 захиалга баталгаажлаа.</p>',
text: 'Баярлалаа! Таны #1042 захиалга баталгаажлаа.',
from_name: 'Shop.mn',
reply_to: '[email protected]',
})
// Up to 1,000 addresses. Each recipient gets a separate email.
const batch = await wesend.email.sendBulk({
to: ['[email protected]', '[email protected]'],
subject: 'Шинэ бараа ирлээ',
html: '<p>Намрын цуглуулга худалдаанд гарлаа.</p>',
})One of html and text is required. An email is queued: read its final status with wesend.messages.get(email.id).
Verify
WeSend creates, sends and checks the one-time code. You never store it.
// 1. Send a code. With an "@" in `to` it goes by email, otherwise by SMS.
const verification = await wesend.verify.create({ to: '99112233', brand: 'Shop.mn' })
// 2. Check what the user typed. Resolves when the code is right, throws when it is not.
try {
await wesend.verify.check(verification.id, { code: '482913' })
// The user is verified.
} catch (error) {
if (error instanceof WeSendError && error.code === 'invalid_code') {
console.log(`Wrong code, ${error.attempts_remaining} tries left`)
} else {
throw error
}
}To resend a code, call verify.create again. The earlier code stops working. Read the current state with wesend.verify.get(id).
Sandbox
Create a sandbox key (ws_test_...) in the dashboard to build and test your integration. It uses the same base URL, the same checks and the same answers as a live key, but nothing is delivered and no credits are charged. It works before DAN verification and after the trial ends.
- Every valid recipient is
sent, except99000001and[email protected], which always fail withprovider_failed. - A verification code is always zeros at its length, for example
000000. - A project may create 1,000 sandbox messages in 24 hours. Sandbox data is deleted after 30 days.
- Every message, batch and verification carries
sandbox: true. A sandbox key cannot read live data, and a live key cannot read sandbox data.
Going live is a key swap: replace WESEND_API_KEY with a live key.
Messages and batches
const message = await wesend.messages.get(id) // status: queued, processing, sent or failed
const batch = await wesend.batches.get(id) // status: processing or completedBalance and pricing
const { balance, next_expiry } = await wesend.balance.get()
const pricing = await wesend.pricing.get()Every send also returns the new balance, so you rarely need to ask separately.
Errors
A failed request throws a WeSendError. Branch on code. Use message only for logs, because its wording can change.
import { WeSend, WeSendError } from '@wesend/node'
try {
await wesend.sms.send({ to: '99112233', text: 'Sain baina uu' })
} catch (error) {
if (!(error instanceof WeSendError)) throw error
switch (error.code) {
case 'insufficient_credits':
console.log(`Need ${error.required} credits, have ${error.balance}`)
break
case 'unsupported_number':
console.log('Not a Mongolian mobile number')
break
default:
console.error(error.status, error.code, error.message)
}
}| Field | Description |
| --- | --- |
| code | The machine-readable code, such as invalid_request or insufficient_credits. See all codes. |
| status | The HTTP status, or null when no response arrived. |
| message | A description in English. |
| retryAfter | Seconds to wait before trying again, when the API says so. Otherwise null. |
| balance, required | On insufficient_credits. |
| attempts_remaining | On invalid_code. |
| invalid | On a bulk send with bad recipients: the entries that were refused, up to 50. |
| id | On provider_failed: the id of the failed message. |
Three codes come from the SDK itself, not from the API: timeout, connection_error (the request never got an answer) and unexpected_response (the answer was not from the WeSend API).
Retries and idempotency
The SDK tries a failed request again, up to 2 more times, when doing so is safe:
| Failure | What the SDK does |
| --- | --- |
| 429 rate_limited | Waits the seconds the API asks for, then sends again. If the wait is longer than 10 seconds, the error is thrown instead. |
| Network failure, timeout or 5xx | Sends again, for reads and for the four send methods. |
| 502 provider_failed | Sends again. The operator did not accept the message and the credits were refunded. |
| Any other 4xx | Thrown at once. Fix the request first. |
After a network failure you cannot know whether a message went out. So sms.send, sms.sendBulk, email.send and email.sendBulk always carry an Idempotency-Key, and a retry with the same key sends nothing new. The SDK makes a key for each call. Pass your own to stay safe across retries in your own code too, for example when a queue job runs twice:
await wesend.sms.send(
{ to: '99112233', text: 'Tanii zahialga batalgaajlaa. #1042' },
{ idempotencyKey: 'order-1042-confirmation' },
)With your own key, a 502 provider_failed is not retried: the key now stands for that failed send, and a repeat would only report the same failure. Send again with a new key.
verify.create and verify.check do not accept an idempotency key, so after a network failure or a 5xx they are not retried.
Options
const wesend = new WeSend(process.env.WESEND_API_KEY, {
timeout: 30_000, // milliseconds one attempt may take
maxRetries: 2, // 0 turns retries off
})| Option | Default | Description |
| --- | --- | --- |
| timeout | 30000 | Milliseconds one attempt may take. |
| maxRetries | 2 | How many times a failed request is tried again. |
| baseUrl | https://api.wesend.mn/v1 | Where requests go. |
| fetch | global fetch | A fetch to use in place of the global one. |
Field names
Request and response fields are named exactly as in the API reference, in snake_case (from_name, batch_id, created_at). The SDK's own options are in camelCase (idempotencyKey, maxRetries).
CommonJS
const { WeSend } = require('@wesend/node')License
MIT
