> **The Node.js / TypeScript SDK for sending Kakao Alimtalk, Brand Message and SMS**

`@sendgo/node` is the official Node.js SDK for the [Sendgo](https://sendgo.io) messaging API.
It is written in TypeScript, ships its own type declarations, and uses the built-in `fetch` — no runtime dependencies.

> **Server-side only.** Your access key and secret key must never reach a browser. Use this SDK from a server process, a Next.js Server Action / Route Handler, a Nuxt server route, or a background worker.

---

## Install

```bash
npm install @sendgo/node
# or
pnpm add @sendgo/node
yarn add @sendgo/node
```

Requires Node.js 18 or newer (for global `fetch`).

---

## Quick start

```typescript
import Sendgo from '@sendgo/node';

const sendgo = new Sendgo({
  accessKey: process.env.SENDGO_ACCESS_KEY!,
  secretKey: process.env.SENDGO_SECRET_KEY!,
  kakaoSenderKey: process.env.SENDGO_KAKAO_SENDER_KEY,
  smsSenderKey: process.env.SENDGO_SMS_SENDER_KEY,
  apiVersion: 'v2',
});

// Send an Alimtalk
await sendgo.alimtalk.send({
  templateCode: 'ORDER_CONFIRM_001',
  contacts: [
    { contact: '01012345678', name: 'Gildong Hong', var1: 'ORD-001', var2: '29,000 KRW' },
  ],
});

// Send an SMS
await sendgo.sms.sendSms({
  content: '[Sendgo] Your code is 123456 (valid for 5 minutes)',
  contacts: [{ contact: '01012345678' }],
});
```

Tokens are issued and refreshed automatically — you never handle them yourself.

---

## Alimtalk in detail

```typescript
// Multiple recipients
await sendgo.alimtalk.send({
  templateCode: 'ORDER_CONFIRM_001',
  contacts: [
    { contact: '01011111111', name: 'Gildong Hong', var1: 'ORD-001', var2: '29,000 KRW' },
    { contact: '01022222222', name: 'Chulsoo Kim', var1: 'ORD-002', var2: '15,000 KRW' },
  ],
});

// Scheduled send
await sendgo.alimtalk.send({
  templateCode: 'PROMO_SUMMER_2026',
  scheduleType: 'SCHEDULED',
  at: '2026-07-28 09:00:00',
  contacts: [{ contact: '01012345678', var1: 'Summer sale — 50% off' }],
});

// Fall back to SMS when the Alimtalk fails
await sendgo.alimtalk.send({
  templateCode: 'DELIVERY_START_001',
  replaceSms: 'Y',
  smsSubject: '[Shipping notice]',
  smsContent: 'Your order has shipped.\nTracking: #{var2}',
  contacts: [{ contact: '01012345678', var1: 'ORD-001', var2: '1234567890' }],
});
```

---

## Friendtalk

> ⚠️ **Deprecated — Friendtalk was discontinued on 2025-12-31 under Kakao's policy.**
> Since 2026-01-01, Friendtalk send requests are automatically delivered as
> **Brand Message (free-form)** by Kakao. Calls still succeed, and this is still the
> only path for free-form types (`FT`/`FI`/`FW`) sent to individual recipients, so
> there is no need to change working code right now.
>
> Use **Brand Message** instead for:
> - template-based rich types (`FL`/`FC`/`FM`/`FP`/`FA`)
> - recipients who are **not** channel friends (`targeting` = `N` / `I`)
> - broadcasts to every opted-in channel friend (`targeting` = `F`)
>
> Message types map one-to-one and the server does the conversion — `FT`→`BT`,
> `FI`→`BI`, `FW`→`BW`, `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`.
```typescript
// Text
await sendgo.friendtalk.send({
  content: 'Hello! Check out this month\'s deals.',
  contacts: [{ contact: '01012345678' }],
});

// Image
await sendgo.friendtalk.send({
  messageType: 'FI',
  content: 'This week\'s featured products.',
  imageUrl: 'https://cdn.example.com/banner.jpg',
  imageLink: 'https://example.com/event',
  contacts: [{ contact: '01012345678' }],
});

// With buttons
await sendgo.friendtalk.send({
  content: 'Your coupon has arrived. Use it now!',
  buttons: [{ name: 'Get coupon', type: 'WL', linkMo: 'https://example.com/coupon' }],
  contacts: [{ contact: '01012345678' }],
});
```

---

## Brand Message

Brand Message is the successor channel to Friendtalk. Message types map one-to-one
(`FT`→`BT`, `FI`→`BI`, `FW`→`BW`, `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`);
pass the **Friendtalk code** and the server converts it.

Unlike Friendtalk it can also:

- reach recipients who are **not channel friends** (`targeting: 'N'`)
- **broadcast to every consenting channel friend** (`targeting: 'F'`, no recipient list needed)
- send **template-based rich messages** — lists, carousels, commerce, video

> v2 only. For plain text or images (`FT`/`FI`/`FW`) to channel friends, the Friendtalk API is simpler.

```typescript
// Single send — channel friends
await sendgo.brandMessage.send({
  targeting: 'M',
  messageType: 'FL',
  friendTemplateUuid: '9cd5460b-6458-4edc-9b11-c26d3013c340',
  contacts: [{ contact: '01012345678', var1: '29,000 KRW' }],
});

// Broadcast — every consenting channel friend (no contacts)
await sendgo.brandMessage.broadcast({
  messageType: 'FW',
  friendTemplateUuid: '9cd5460b-6458-4edc-9b11-c26d3013c340',
});

// Campaign lookups
const list = await sendgo.brandMessage.campaigns({ count: 10 });
const one = await sendgo.brandMessage.campaign('1f0a6d0e-6b3b-4f0f-9b2f-2f6f6a1b7c11');
```

`broadcast()` omits `contacts` entirely rather than sending an empty array — an empty recipient list would be rejected as an invalid request.

---

## SMS / LMS / MMS

```typescript
// SMS (up to 90 bytes)
await sendgo.sms.sendSms({
  content: '[Sendgo] Your code is 123456 (valid for 5 minutes)',
  contacts: [{ contact: '01012345678' }],
});

// LMS (long text, up to 2,000 bytes)
await sendgo.sms.sendLms({
  subject: '[Important] Scheduled maintenance',
  content: 'Maintenance is scheduled.\n\n■ When: 2026-07-25 02:00–06:00\n■ Impact: all services',
  contacts: [{ contact: '01012345678' }],
});

// MMS (with an image)
await sendgo.sms.sendMms({
  subject: '[Event] July deals',
  content: 'Check out this month\'s deals!',
  contacts: [{ contact: '01012345678' }],
});

// Scheduled SMS
await sendgo.sms.sendSms({
  content: '[Reminder] Please confirm your appointment.',
  scheduleType: 'SCHEDULED',
  at: '2026-07-23 08:00:00',
  contacts: [{ contact: '01012345678' }],
});
```

---

## Framework integration

### Next.js — Server Action

For Next.js there is a dedicated package, `@sendgo/react`, which exposes ready-made Server Actions. To use this core SDK directly:

```typescript
// app/actions/notify.ts
'use server';

import Sendgo from '@sendgo/node';

const sendgo = new Sendgo({
  accessKey: process.env.SENDGO_ACCESS_KEY!,
  secretKey: process.env.SENDGO_SECRET_KEY!,
  kakaoSenderKey: process.env.SENDGO_KAKAO_SENDER_KEY,
  apiVersion: 'v2',
});

export async function notifyOrderConfirmed(phone: string, orderNo: string) {
  return sendgo.alimtalk.send({
    templateCode: 'ORDER_CONFIRM_001',
    contacts: [{ contact: phone, var1: orderNo }],
  });
}
```

### Express

```typescript
import express from 'express';
import Sendgo from '@sendgo/node';

const sendgo = new Sendgo({
  accessKey: process.env.SENDGO_ACCESS_KEY!,
  secretKey: process.env.SENDGO_SECRET_KEY!,
  kakaoSenderKey: process.env.SENDGO_KAKAO_SENDER_KEY,
  apiVersion: 'v2',
});

const app = express();
app.use(express.json());

app.post('/notify', async (req, res, next) => {
  try {
    await sendgo.alimtalk.send({
      templateCode: 'ORDER_CONFIRM_001',
      contacts: [{ contact: req.body.phone, var1: req.body.orderNo }],
    });
    res.json({ ok: true });
  } catch (error) {
    next(error);
  }
});
```

### NestJS

Use `@sendgo/nestjs`, which registers the client as an injectable provider:

```typescript
constructor(private readonly sendgo: SendgoService) {}

await this.sendgo.brandMessage.send({ /* ... */ });
```

---

## Error handling

```typescript
import { SendgoError } from '@sendgo/node';

try {
  await sendgo.alimtalk.send({
    templateCode: 'ORDER_CONFIRM_001',
    contacts: [{ contact: '01012345678' }],
  });
} catch (error) {
  if (!(error instanceof SendgoError)) throw error;

  switch (error.errorCode) {
    case 'INVALID_ACCESS_KEY':
    case 'INVALID_SECRET_KEY':
      await alertOps('Check the Sendgo API keys.');
      break;
    case 'PAYMENT_REQUIRED':
      await alertOps('Out of Sendgo credit.');
      break;
    case 'IP_NOT_ALLOWED':
      await alertOps('IP is not allow-listed.');
      break;
    case 'INVALID_TEMPLATE_CODE':
      logger.warn('Unknown template');
      break;
    default:
      if (error.statusCode >= 500) await retryQueue.push(job); // retryable
      else logger.error(`Sendgo error ${error.statusCode}: ${error.message}`);
  }
}
```

Branch on `errorCode`, not on the message text — messages can change, codes are the contract.
`TOKEN_EXPIRED` and `TOKEN_MISMATCH` are handled inside the SDK: the token is reissued and the request retried once.

---

## Configuration options

| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| `accessKey` | `string` | **required** | — | Sendgo access key |
| `secretKey` | `string` | **required** | — | Sendgo secret key |
| `kakaoSenderKey` | `string` | optional | — | Kakao sender profile key |
| `smsSenderKey` | `string` | optional | — | SMS caller ID key |
| `apiVersion` | `'v1' \| 'v2'` | optional | `'v1'` | API version |
| `baseUrl` | `string` | optional | `'https://sendgo.io'` | API base URL |

---

## TypeScript

Every request shape is exported, so payloads are checked at compile time.

```typescript
import type {
  AlimtalkParams,
  FriendtalkParams,
  BrandMessageParams,
  BrandMessageTargeting,
  SmsParams,
  Contact,
  SendgoConfig,
  SendgoResponse,
} from '@sendgo/node';
```

---

## Short URL

Short URLs shrink the links in your message body and count whether they were
actually clicked. SMS is billed by byte, so a shorter link leaves more room for copy.

> v2 only.

Shortening the same target URL again **returns the existing link**. Pass `forceNew`
to mint a new code when you want per-campaign reaction figures kept separate.

`deactivate` does not delete the link — it only stops the redirect. Use it when a link
in an already-sent message has to be killed; the accumulated stats stay, and visitors
to a stopped link get `410 Gone`.

```typescript
const created = await sendgo.shortUrl.create({
  targetUrl: 'https://example.com/promotions/summer-sale',
  title: 'Summer sale landing',
});

const { code, shortUrl } = created.data;

// Reaction stats — daily series + device / referrer / country breakdowns
const stats = await sendgo.shortUrl.stats(code, { from: '2026-08-01' });

await sendgo.shortUrl.list({ count: 10 });
await sendgo.shortUrl.show(code);
await sendgo.shortUrl.deactivate(code);   // Stops the redirect only; the stats stay
```

`stats` returns a daily series (`daily`) plus breakdowns by device (`byDevice`), referrer (`byReferer`) and country (`byCountry`). The daily series is read from a pre-aggregated table, so response time stays flat no matter how many clicks accumulate.

---

## Package information

- **Package**: `@sendgo/node` (npm)
- **Repository**: [send-go/node](https://github.com/send-go/node)
- **Registry**: https://www.npmjs.com/package/@sendgo/node
- **License**: MIT

### Getting your API keys

Sign in to Sendgo and open **API/SDK → API integration** to issue an access key and secret key.
Register a Kakao sender profile under **Kakao channel** to get your `kakaoSenderKey`, and a caller ID under **Sender numbers** for SMS.