> **The official React SDK for sending Kakao Alimtalk, Brand Message and SMS from Next.js**

`@sendgo/react` wraps the [`@sendgo/node`](https://github.com/send-go/node) core as ready-made **Server Actions** for the Next.js App Router.

> **Server-side only.** These functions are marked `'use server'`. Your access key and secret key must never reach the browser — if you call them from a Client Component, Next.js sends the *invocation* to the server, not the keys, which is exactly what you want. Never construct a client inside a `'use client'` module.

Requires React 18+ and Next.js 14+.

---

## Install

```bash
npm install @sendgo/react
```

The core `@sendgo/node` comes along as a dependency.

---

## Configure

The Server Actions read these environment variables, so there is no client construction to wire up:

```env
SENDGO_ACCESS_KEY=your_access_key
SENDGO_SECRET_KEY=your_secret_key
SENDGO_KAKAO_SENDER_KEY=your_kakao_sender_key
SENDGO_SMS_SENDER_KEY=your_sms_sender_key
SENDGO_API_VERSION=v2
```

Do **not** prefix these with `NEXT_PUBLIC_` — that would inline them into the browser bundle.

---

## Quick start — Server Action

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

import { sendAlimtalk } from '@sendgo/react';

export async function notifyOrderConfirmed(phone: string, orderNo: string) {
  return sendAlimtalk({
    templateCode: 'ORDER_CONFIRM_001',
    contacts: [{ contact: phone, name: 'Gildong Hong', var1: orderNo }],
  });
}
```

```tsx
// app/orders/[id]/page.tsx
import { notifyOrderConfirmed } from '../actions';

export default function OrderPage({ params }: { params: { id: string } }) {
  return (
    <form action={async () => { await notifyOrderConfirmed('01012345678', params.id); }}>
      <button type="submit">Send confirmation</button>
    </form>
  );
}
```

---

## Available Server Actions

| Action | Channel |
|--------|---------|
| `sendAlimtalk(params)` | Kakao Alimtalk |
| `sendFriendtalk(params)` | Kakao Friendtalk |
| `sendBrandMessage(params)` | Kakao Brand Message |
| `broadcastBrandMessage(params)` | Brand Message broadcast (`targeting: 'F'`) |
| `listBrandMessages(params?)` | Brand Message campaign list |
| `getBrandMessage(campaignId)` | Brand Message campaign detail |
| `sendSms(params)` | SMS |
| `sendLms(params)` | LMS |
| `sendMms(params)` | MMS |
| `createSendgoClient(config?)` | the raw `Sendgo` client, for anything above |

---

## Brand Message

> **Friendtalk was discontinued on 2025-12-31.** Since 2026-01-01 Kakao delivers
> Friendtalk requests as Brand Message (free-form) automatically. `friendtalk` still
> works and is still the only path for free-form `FT`/`FI`/`FW` to individual
> recipients — use Brand Message for template-based rich types (`FL`/`FC`/`FM`/`FP`/`FA`),
> non-friend targeting (`N`/`I`) and broadcasts (`F`).
Brand Message is the successor channel to Friendtalk: it reaches recipients who are **not channel friends** (`targeting: 'N'`) and can **broadcast to every consenting channel friend** (`targeting: 'F'`). Message types map one-to-one with Friendtalk (`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.

> v2 only. Set `SENDGO_API_VERSION=v2`.

```typescript
'use server';

import {
  sendBrandMessage,
  broadcastBrandMessage,
  listBrandMessages,
  getBrandMessage,
} from '@sendgo/react';

// Single send — channel friends
export async function promoteToFriends(phone: string) {
  return sendBrandMessage({
    targeting: 'M',
    messageType: 'FL',
    friendTemplateUuid: '9cd5460b-6458-4edc-9b11-c26d3013c340',
    contacts: [{ contact: phone, var1: '29,000 KRW' }],
  });
}

// Broadcast — every consenting channel friend (no recipient list)
export async function announce() {
  return broadcastBrandMessage({
    messageType: 'FW',
    friendTemplateUuid: '9cd5460b-6458-4edc-9b11-c26d3013c340',
  });
}

// A broadcast is asynchronous upstream, so poll for the result
export async function broadcastStatus(campaignId: string) {
  return getBrandMessage(campaignId);
}

export async function recentCampaigns() {
  return listBrandMessages({ count: 10 });
}
```

`broadcastBrandMessage` takes `Omit<BrandMessageParams, 'targeting' | 'contacts'>`, so TypeScript rejects passing a recipient list to a broadcast rather than letting the API do it at runtime.

---

## Route Handler

When you need a plain HTTP endpoint — for a webhook or a non-React caller:

```typescript
// app/api/notify/route.ts
import { NextResponse } from 'next/server';
import { sendAlimtalk } from '@sendgo/react';
import { SendgoError } from '@sendgo/react';

export async function POST(request: Request) {
  const { phone, orderNo } = await request.json();

  try {
    await sendAlimtalk({
      templateCode: 'ORDER_CONFIRM_001',
      contacts: [{ contact: phone, var1: orderNo }],
    });

    return NextResponse.json({ ok: true });
  } catch (error) {
    if (error instanceof SendgoError) {
      // Don't leak provider detail to the caller; log it and return a generic status.
      console.error(`Sendgo ${error.statusCode} [${error.errorCode}]: ${error.message}`);

      return NextResponse.json({ ok: false }, { status: error.statusCode >= 500 ? 502 : 400 });
    }

    throw error;
  }
}
```

---

## Client-side hook

`useAlimtalk` gives a Client Component pending/error state around a Server Action, without ever touching the keys. It returns `{ send, loading, error, data, reset }`:

```tsx
'use client';

import { useAlimtalk } from '@sendgo/react';

export function SendButton({ phone, orderNo }: { phone: string; orderNo: string }) {
  const { send, loading, error, data, reset } = useAlimtalk();

  return (
    <>
      <button
        disabled={loading}
        onClick={() => send({
          templateCode: 'ORDER_CONFIRM_001',
          contacts: [{ contact: phone, var1: orderNo }],
        })}
      >
        {loading ? 'Sending…' : 'Send'}
      </button>
      {error && (
        <p role="alert">
          {error.message} <button onClick={reset}>Dismiss</button>
        </p>
      )}
      {data && <p>Sent.</p>}
    </>
  );
}
```

---

## Custom configuration

If your keys do not come from environment variables — a multi-tenant app resolving them per request, for instance — build the client yourself:

```typescript
'use server';

import { createSendgoClient } from '@sendgo/react';

export async function sendForTenant(tenant: Tenant, phone: string) {
  const sendgo = createSendgoClient({
    accessKey: tenant.sendgoAccessKey,
    secretKey: tenant.sendgoSecretKey,
    kakaoSenderKey: tenant.kakaoSenderKey,
    apiVersion: 'v2',
  });

  return sendgo.alimtalk.send({
    templateCode: 'ORDER_CONFIRM_001',
    contacts: [{ contact: phone }],
  });
}
```

Called with no argument, `createSendgoClient()` returns the shared environment-configured singleton.

---

## Types

Every request shape is re-exported from the core, so payloads are checked at compile time:

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

---

## 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
'use server';

import { createShortUrl, shortUrlStats } from '@sendgo/react';

export async function shorten(targetUrl: string) {
  const created = await createShortUrl({ targetUrl });

  return created.data.shortUrl;
}

export async function reactions(code: string) {
  return shortUrlStats(code, { from: '2026-08-01' });
}
```

`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/react` (npm)
- **Repository**: [send-go/react](https://github.com/send-go/react)
- **Registry**: https://www.npmjs.com/package/@sendgo/react
- **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 `SENDGO_KAKAO_SENDER_KEY`, and a caller ID under **Sender numbers** for SMS.