> **The official Vue SDK for sending Kakao Alimtalk, Brand Message and SMS from Nuxt**

`@sendgo/vue` wraps the [`@sendgo/node`](https://github.com/send-go/node) core as a **Vue plugin** that provides the client through Vue's injection system.

> **Server-side only.** Your access key and secret key must never reach the browser. Register the plugin as a **server-only** Nuxt plugin, or use the client directly inside a `server/api` route. Never register it in a universal plugin.

Requires Vue 3.4+ and Nuxt 3+.

---

## Install

```bash
npm install @sendgo/vue
```

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

---

## Register the plugin (Nuxt, server-only)

The `.server.ts` suffix is what keeps the keys out of the client bundle:

```typescript
// plugins/sendgo.server.ts
import { SendgoPlugin } from '@sendgo/vue';

export default defineNuxtPlugin((app) => {
  app.vueApp.use(SendgoPlugin, {
    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',
  });
});
```

---

## Use the injected client

```vue
<script setup lang="ts">
import { inject } from 'vue';
import { SENDGO_KEY } from '@sendgo/vue';
import type Sendgo from '@sendgo/node';

// Only resolves during SSR — the plugin is server-only by design.
const sendgo = inject<Sendgo>(SENDGO_KEY);

await sendgo?.alimtalk.send({
  templateCode: 'ORDER_CONFIRM_001',
  contacts: [{ contact: '01012345678', var1: 'ORD-001' }],
});
</script>
```

`SENDGO_KEY` provides the **whole core client**, so every channel is available on it:

| Property | Channel |
|----------|---------|
| `sendgo.alimtalk` | Kakao Alimtalk |
| `sendgo.friendtalk` | Kakao Friendtalk |
| `sendgo.brandMessage` | Kakao Brand Message (v2 only) |
| `sendgo.sms` | SMS / LMS / MMS |

---

## Nuxt server route (recommended)

For anything triggered by a user action, a server route is clearer than SSR injection — the browser calls your endpoint, and the keys stay on the server:

```typescript
// server/api/notify.post.ts
import { Sendgo, SendgoError } from '@sendgo/vue';

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 default defineEventHandler(async (event) => {
  const { phone, orderNo } = await readBody(event);

  try {
    await sendgo.alimtalk.send({
      templateCode: 'ORDER_CONFIRM_001',
      contacts: [{ contact: phone, var1: orderNo }],
    });

    return { ok: true };
  } catch (error) {
    if (error instanceof SendgoError) {
      // Log the provider detail; return a generic status to the caller.
      console.error(`Sendgo ${error.statusCode} [${error.errorCode}]: ${error.message}`);

      throw createError({
        statusCode: error.statusCode >= 500 ? 502 : 400,
        statusMessage: 'Message delivery failed',
      });
    }

    throw error;
  }
});
```

The client is constructed once at module scope, so the token cache is reused across requests.

---

## 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 `apiVersion: 'v2'`.

```typescript
// server/api/campaign.post.ts
export default defineEventHandler(async (event) => {
  const { mode, phone } = await readBody(event);

  if (mode === 'broadcast') {
    // No recipient list — Kakao expands the audience
    return sendgo.brandMessage.broadcast({
      messageType: 'FW',
      friendTemplateUuid: '9cd5460b-6458-4edc-9b11-c26d3013c340',
    });
  }

  return sendgo.brandMessage.send({
    targeting: 'M',
    messageType: 'FL',
    friendTemplateUuid: '9cd5460b-6458-4edc-9b11-c26d3013c340',
    contacts: [{ contact: phone, var1: '29,000 KRW' }],
  });
});
```

```typescript
// server/api/campaigns.get.ts — a broadcast is asynchronous upstream, so poll
export default defineEventHandler(async (event) => {
  const { campaignId } = getQuery(event);

  return campaignId
    ? sendgo.brandMessage.campaign(String(campaignId))
    : sendgo.brandMessage.campaigns({ count: 10 });
});
```

---

## Client-side composable

`useAlimtalk` wraps a call to your own server route with pending/error state. It returns `{ send, loading, error, data, reset }`:

```vue
<script setup lang="ts">
import { useAlimtalk } from '@sendgo/vue';

const { send, loading, error, data, reset } = useAlimtalk();

const notify = () => send({
  templateCode: 'ORDER_CONFIRM_001',
  contacts: [{ contact: '01012345678', var1: 'ORD-001' }],
});
</script>

<template>
  <button :disabled="loading" @click="notify">
    {{ loading ? 'Sending…' : 'Send' }}
  </button>
  <p v-if="error" role="alert">
    {{ error.message }}
    <button @click="reset">Dismiss</button>
  </p>
  <p v-else-if="data">Sent.</p>
</template>
```

---

## Plain Vue 3 (no Nuxt)

Without Nuxt there is no server/client split, so the plugin belongs in a Node process — an Express or Fastify backend rendering with `@vue/server-renderer`, or a worker. Do not register it in a browser-only app.

```typescript
import { createSSRApp } from 'vue';
import { SendgoPlugin } from '@sendgo/vue';

const app = createSSRApp(App);
app.use(SendgoPlugin, {
  accessKey: process.env.SENDGO_ACCESS_KEY!,
  secretKey: process.env.SENDGO_SECRET_KEY!,
  apiVersion: 'v2',
});
```

---

## Types

Every request shape is re-exported from the core:

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

`Sendgo` (the client class) and `SendgoError` are exported as values, not just types.

---

## 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
// server/api/shorten.post.ts
export default defineEventHandler(async (event) => {
  const { targetUrl } = await readBody(event);

  const created = await sendgo.shortUrl.create({ targetUrl });

  return { shortUrl: created.data.shortUrl, code: created.data.code };
});
```

`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/vue` (npm)
- **Repository**: [send-go/vue](https://github.com/send-go/vue)
- **Registry**: https://www.npmjs.com/package/@sendgo/vue
- **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.