> **The official Dart SDK for sending Kakao Alimtalk, Brand Message and SMS from a Dart server**

`sendgo_flutter` is the official Dart SDK for the [Sendgo](https://sendgo.io) messaging API.

> **Server-side only.** Never put your access key and secret key in a Flutter app — anything shipped to a device can be extracted from the bundle. Use this SDK from a Dart backend (Shelf, Serverpod, Dart Frog), Cloud Functions, or your own API, and have the app call *your* endpoint instead.

Requires Dart 3.3 or newer.

---

## Install

```bash
dart pub add sendgo_flutter
```

```yaml
# pubspec.yaml
dependencies:
  sendgo_flutter: ^1.0.2
```

---

## Quick start

```dart
import 'dart:io';
import 'package:sendgo_flutter/sendgo_flutter.dart';

final client = SendgoClient(
  accessKey: Platform.environment['SENDGO_ACCESS_KEY']!,
  secretKey: Platform.environment['SENDGO_SECRET_KEY']!,
  kakaoSenderKey: Platform.environment['SENDGO_KAKAO_SENDER_KEY'],
  smsSenderKey: Platform.environment['SENDGO_SMS_SENDER_KEY'],
  apiVersion: 'v2',
);

// Send an Alimtalk
await client.alimtalk.send(AlimtalkRequest(
  templateCode: 'ORDER_CONFIRM_001',
  contacts: [
    Contact(contact: '01012345678', name: 'Gildong Hong', var1: 'ORD-001'),
  ],
));
```

Each channel is a field on the client: `alimtalk`, `friendtalk`, `brandMessage`, `sms`.
Tokens are issued and refreshed inside the SDK.

---

## Alimtalk in detail

```dart
// Multiple recipients
await client.alimtalk.send(AlimtalkRequest(
  templateCode: 'ORDER_CONFIRM_001',
  contacts: [
    Contact(contact: '01011111111', name: 'Gildong Hong', var1: 'ORD-001'),
    Contact(contact: '01022222222', name: 'Chulsoo Kim', var1: 'ORD-002'),
  ],
));

// Scheduled send
await client.alimtalk.send(AlimtalkRequest(
  templateCode: 'PROMO_SUMMER_2026',
  scheduleType: 'SCHEDULED',
  at: '2026-07-28 09:00:00',
  contacts: [Contact(contact: '01012345678', var1: 'Summer sale')],
));

// Fall back to SMS when the Alimtalk fails
await client.alimtalk.send(AlimtalkRequest(
  templateCode: 'DELIVERY_START_001',
  replaceSms: 'Y',
  smsSubject: '[Shipping notice]',
  smsContent: 'Your order has shipped.',
  contacts: [Contact(contact: '01012345678', var1: 'ORD-001')],
));
```

For named template variables beyond `var1`–`var8`, `Contact` also takes a map:

```dart
Contact(contact: '01012345678', variables: {'title': 'Order', 'date': '2026-08-10'});
```

`Contact.toJson()` merges `variables` into the payload, so named and numbered variables can be mixed.

---

## 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`.
```dart
await client.friendtalk.send(FriendtalkRequest(
  messageType: 'FI',
  content: "This week's featured products.",
  imageUrl: 'https://cdn.example.com/banner.jpg',
  imageLink: 'https://example.com/event',
  contacts: [Contact(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'`) and **broadcast to every consenting channel friend** (`targeting: 'F'`).

> v2 only. Set `apiVersion: 'v2'`.

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

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

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

`broadcast()` calls `request.asBroadcast()`, which returns a copy with `targeting: 'F'` and no contacts. `toJson()` then omits the `contacts` key entirely rather than sending an empty list, which the API would reject.

---

## SMS / LMS / MMS

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

// LMS (long text, up to 2,000 bytes)
await client.sms.sendLms(SmsRequest(
  subject: '[Important] Scheduled maintenance',
  content: 'Maintenance: 2026-07-25 02:00–06:00',
  contacts: [Contact(contact: '01012345678')],
));

// MMS
await client.sms.sendMms(SmsRequest(
  subject: '[Event] July deals',
  content: "Check out this month's deals!",
  contacts: [Contact(contact: '01012345678')],
));
```

---

## Using it from a Flutter app safely

The app must not hold Sendgo keys. Put the SDK behind your own endpoint and call that:

```dart
// server/bin/server.dart — Shelf
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import 'package:sendgo_flutter/sendgo_flutter.dart';

final client = SendgoClient(
  accessKey: Platform.environment['SENDGO_ACCESS_KEY']!,
  secretKey: Platform.environment['SENDGO_SECRET_KEY']!,
  kakaoSenderKey: Platform.environment['SENDGO_KAKAO_SENDER_KEY'],
  apiVersion: 'v2',
);

final router = Router()
  ..post('/notify', (Request request) async {
    final body = jsonDecode(await request.readAsString()) as Map<String, dynamic>;

    await client.alimtalk.send(AlimtalkRequest(
      templateCode: 'ORDER_CONFIRM_001',
      contacts: [Contact(contact: body['phone'] as String, var1: body['orderNo'] as String)],
    ));

    return Response.ok('{"ok":true}');
  });
```

The Flutter app then calls `POST /notify` on your server, authenticated with your own session — the Sendgo keys never leave the backend.

---

## Error handling

```dart
try {
  await client.alimtalk.send(request);
} on SendgoException catch (e) {
  switch (e.errorCode) {
    case 'INVALID_ACCESS_KEY':
    case 'INVALID_SECRET_KEY':
      log.severe('Check the Sendgo API keys');
    case 'PAYMENT_REQUIRED':
      log.severe('Out of Sendgo credit');
    case 'IP_NOT_ALLOWED':
      log.severe('IP is not allow-listed');
    case 'INVALID_TEMPLATE_CODE':
      log.warning('Unknown template');
    default:
      if (e.statusCode >= 500) {
        await retryLater(request);   // transient
      } else {
        log.severe('Sendgo ${e.statusCode}: ${e.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

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

---

## 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`.

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

final code = created['data']['code'] as String;
final link = created['data']['shortUrl'] as String;

// Reaction stats — daily series + device / referrer / country breakdowns
final 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_flutter` (pub.dev)
- **Repository**: [send-go/flutter](https://github.com/send-go/flutter)
- **Registry**: https://pub.dev/packages/sendgo_flutter
- **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.