Skip to content

Repository files navigation

sendly-client

TypeScript client for the Sendly SMS API (https://api.sendly.link). Works on Node.js 18+ and in any modern runtime with a built-in fetch.

Installation

npm install sendly-client

Quick start

The API token is generated in the Sendly customer panel (or provided by customer support). The conventional environment variables are SENDLY_TOKEN, SENDLY_BASE_URL (optional URL override) and SENDLY_FROM (optional default sender); the older names ACTIO_TOKEN, ACTIO_BASE_URL and ACTIO_FROM remain supported as a fallback (when both are set, SENDLY_* takes precedence). SendlyClient reads SENDLY_TOKEN/ACTIO_TOKEN and SENDLY_BASE_URL/ACTIO_BASE_URL itself when not passed explicitly to the constructor; SENDLY_FROM/ACTIO_FROM is not read by this client and must be passed as from on each call.

import { SendlyClient } from 'sendly-client';

const client = new SendlyClient({
  token: process.env.SENDLY_TOKEN!,
  // baseUrl: process.env.SENDLY_BASE_URL, // optional, default https://api.sendly.link
  // timeoutMs: 30000,                     // optional, default 30 s
});

const { messageId } = await client.sendSms({
  from: '48732129000',
  to: '48732129001',
  body: 'Test Sendly',
});
console.log(messageId); // unique message id, e.g. "a906cff7719bd889"

The message_id field (snake_case in the API) is exposed throughout the client as messageId (single send, sendSmsMulti result entries, and NOTIFICATION webhook events).

Equivalent curl request:

curl -X POST https://api.sendly.link/api/sms \
  -H "Authorization: Bearer $SENDLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"from": "48732129000", "to": "48732129001", "body": "Test Sendly"}'

Sending to multiple numbers

sendSmsMulti accepts 1–100 unique destination numbers. Server-side validation is all-or-nothing: every number must be a valid Polish mobile number and the numbers must be unique – otherwise the whole request is rejected. This API method must additionally be activated for your token by Sendly customer support.

const results = await client.sendSmsMulti({
  from: '48732129000',
  to: ['48732129001', '48732129002'],
  body: 'Test Sendly',
});
// [{ number: '48732129001', messageId: '...' }, { number: '48732129002', messageId: '...' }]

Equivalent curl request:

curl -X POST https://api.sendly.link/api/sms-multi \
  -H "Authorization: Bearer $SENDLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"from": "48732129000", "to": ["48732129001", "48732129002"], "body": "Test Sendly"}'

Parsing webhooks

Sendly POSTs incoming messages and (once activated) delivery notifications to the webhook URL configured in the customer panel. A single delivery attempt is made, redirects are not followed, and no authentication is performed against the webhook; the request originates from the current IP address of api.sendly.link.

parseWebhook turns a parsed JSON payload into a typed event discriminated on the type field (IncomingMessage | DeliveryNotification):

import { createServer } from 'node:http';
import { parseWebhook } from 'sendly-client';

createServer((req, res) => {
  const chunks = [];
  req.on('data', (chunk) => chunks.push(chunk));
  req.on('error', (err) => {
    console.error('Webhook request error:', err);
  });
  req.on('end', () => {
    const raw = Buffer.concat(chunks).toString('utf8');
    try {
      const event = parseWebhook(JSON.parse(raw));
      if (event.type === 'MESSAGE') {
        console.log(`SMS from ${event.from} to ${event.to}: ${event.body}`);
      } else {
        console.log(`Message ${event.messageId} status: ${event.status}`); // DELIVERED | ERROR
      }
      res.writeHead(200).end();
    } catch (err) {
      console.error('Invalid webhook payload:', err);
      res.writeHead(400).end();
    }
  });
}).listen(3000);

Example payloads:

{"type": "MESSAGE", "from": "48732129000", "to": "48732129001", "body": "Test sms"}
{"type": "NOTIFICATION", "message_id": "a906cff7719bd889", "status": "DELIVERED"}

parseWebhook throws SendlyValidationError for payloads that don't match any known shape.

Error handling

All errors thrown by this library inherit from SendlyError:

  • SendlyValidationError – client-side validation failed before any HTTP request was made (from/to must be numeric strings of 9–11 digits, body non-empty, multi-send to must contain 1–100 unique numbers).
  • SendlyApiError – the API responded with a non-2xx status (403 authorization problem, 422 validation problem). Carries statusCode, a parsed errors map (optional keys token, from, to, body, each an array of strings), and rawBody with the verbatim response body as a fallback for non-JSON or unexpectedly shaped responses.
import { SendlyApiError, SendlyValidationError } from 'sendly-client';

try {
  await client.sendSms({ from: '48732129000', to: '48732129001', body: 'Hi' });
} catch (error) {
  if (error instanceof SendlyValidationError) {
    console.error('Invalid input:', error.message);
  } else if (error instanceof SendlyApiError) {
    console.error(`HTTP ${error.statusCode}`, error.errors, error.rawBody);
  } else {
    throw error; // network failure, timeout (TimeoutError), ...
  }
}

Request identification headers

Every outgoing request to the API carries, alongside Authorization and Content-Type:

  • User-Agent: sendly-javascript-client/<version> (Node/<major>), e.g. sendly-javascript-client/2.0.0 (Node/22) – the version comes from package.json (generated into src/version.ts at build time);
  • X-Request-Id: <uuid> – a fresh UUID v4 for every request (Web Crypto's randomUUID() where available), a correlation id you can hand to Sendly support, e.g. when investigating a suspected double send.

No automatic retries

The client never retries failed requests – a retry could send an SMS twice. If you need retries, implement them yourself with idempotency in mind.

Timeouts

Requests are aborted after timeoutMs (default 30,000 ms) using AbortSignal.timeout; a request aborted by the timeout rejects with a TimeoutError (DOMException, not a SendlyError).

Encoding

Outgoing messages are encoded as UCS2 and split every 60 characters. Optionally, GSM7 encoding can be enabled (customer panel / customer support) with a limit of 160 characters for a single message.

Out of scope

The 3CX SMS API mode (/api/tcx) is mutually exclusive with the REST SMS API and is not implemented by this client.

Development

npm install
npm run build   # compiles src/ to dist/ (ESM + type declarations)
npm test        # builds, then runs node --test against the compiled output

Tests never call the real API – the HTTP layer is replaced with an injected fake fetch.

License

MIT – Copyright (c) 2026 ACTIO. See LICENSE.

About

TypeScript client for the Sendly SMS API – send SMS, bulk send, webhook parsing. Zero runtime dependencies, Node 18+.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages