> **The official .NET SDK for sending Kakao Alimtalk, Brand Message and SMS**

`Sendgo.SDK` is the official .NET SDK for the [Sendgo](https://sendgo.io) messaging API.
Every method is async, nullable reference types are enabled, and requests are `record` types so payloads are immutable and easy to copy with `with`.

Targets .NET 8.0.

---

## Install

```bash
dotnet add package Sendgo.SDK
```

---

## Quick start

```csharp
using Sendgo;
using Sendgo.Models;

var client = new SendgoClient(new SendgoOptions
{
    AccessKey = Environment.GetEnvironmentVariable("SENDGO_ACCESS_KEY")!,
    SecretKey = Environment.GetEnvironmentVariable("SENDGO_SECRET_KEY")!,
    KakaoSenderKey = Environment.GetEnvironmentVariable("SENDGO_KAKAO_SENDER_KEY"),
    SmsSenderKey = Environment.GetEnvironmentVariable("SENDGO_SMS_SENDER_KEY"),
    ApiVersion = "v2",
});

// Send an Alimtalk
await client.SendAlimtalkAsync(new AlimtalkRequest
{
    TemplateCode = "ORDER_CONFIRM_001",
    Contacts = new[]
    {
        new Contact { PhoneNumber = "01012345678", Name = "Gildong Hong", Var1 = "ORD-001" },
    },
});
```

Note that `Contact.PhoneNumber` serialises as `contact` in the request — the property is named for C# readability while the wire format stays unchanged.

`AccessKey` and `SecretKey` are `required`, so a missing key is a compile-time error rather than a runtime surprise.

---

## Alimtalk in detail

```csharp
// Multiple recipients
await client.SendAlimtalkAsync(new AlimtalkRequest
{
    TemplateCode = "ORDER_CONFIRM_001",
    Contacts = new[]
    {
        new Contact { PhoneNumber = "01011111111", Name = "Gildong Hong", Var1 = "ORD-001" },
        new Contact { PhoneNumber = "01022222222", Name = "Chulsoo Kim", Var1 = "ORD-002" },
    },
});

// Scheduled send
await client.SendAlimtalkAsync(new AlimtalkRequest
{
    TemplateCode = "PROMO_SUMMER_2026",
    ScheduleType = "SCHEDULED",
    At = "2026-07-28 09:00:00",
    Contacts = new[] { new Contact { PhoneNumber = "01012345678", Var1 = "Summer sale" } },
});

// Fall back to SMS when the Alimtalk fails
await client.SendAlimtalkAsync(new AlimtalkRequest
{
    TemplateCode = "DELIVERY_START_001",
    ReplaceSms = "Y",
    SmsSubject = "[Shipping notice]",
    SmsContent = "Your order has shipped.",
    Contacts = new[] { new Contact { PhoneNumber = "01012345678", Var1 = "ORD-001" } },
});
```

Because requests are records, a shared base payload can be varied per send:

```csharp
var baseRequest = new AlimtalkRequest { TemplateCode = "ORDER_CONFIRM_001", Contacts = Array.Empty<Contact>() };

await client.SendAlimtalkAsync(baseRequest with { Contacts = batchOne });
await client.SendAlimtalkAsync(baseRequest with { Contacts = batchTwo });
```

---

## 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`.
```csharp
await client.SendFriendtalkAsync(new
{
    messageType = "FI",
    content = "This week's featured products.",
    imageUrl = "https://cdn.example.com/banner.jpg",
    imageLink = "https://example.com/event",
    contacts = new[] { new { contact = "01012345678" } },
});
```

`SendFriendtalkAsync` accepts `object`, so anonymous types work for the many optional Friendtalk shapes.

---

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

```csharp
// Single send — channel friends
await client.SendBrandMessageAsync(new BrandMessageRequest
{
    Targeting = "M",
    MessageType = "FL",
    FriendTemplateUuid = "9cd5460b-6458-4edc-9b11-c26d3013c340",
    Contacts = new[] { new Contact { PhoneNumber = "01012345678", Var1 = "29,000 KRW" } },
});

// Broadcast — every consenting channel friend (Contacts is dropped)
await client.BroadcastBrandMessageAsync(new BrandMessageRequest
{
    MessageType = "FW",
    FriendTemplateUuid = "9cd5460b-6458-4edc-9b11-c26d3013c340",
});

// Campaign lookups
var list = await client.GetBrandMessagesAsync(count: 10);
var one = await client.GetBrandMessageAsync("1f0a6d0e-6b3b-4f0f-9b2f-2f6f6a1b7c11");
```

`BroadcastBrandMessageAsync` applies `with { Targeting = "F", Contacts = null }` and the serialiser omits null properties, so a broadcast never sends an empty recipient array — which the API would reject.

---

## SMS / LMS / MMS

```csharp
// SMS (up to 90 bytes)
await client.SendSmsAsync(new SmsRequest
{
    Content = "[Sendgo] Your code is 123456 (valid for 5 minutes)",
    Contacts = new[] { new Contact { PhoneNumber = "01012345678" } },
});

// LMS (long text, up to 2,000 bytes)
await client.SendLmsAsync(new SmsRequest
{
    Subject = "[Important] Scheduled maintenance",
    Content = "Maintenance: 2026-07-25 02:00–06:00",
    Contacts = new[] { new Contact { PhoneNumber = "01012345678" } },
});

// MMS
await client.SendMmsAsync(new SmsRequest
{
    Subject = "[Event] July deals",
    Content = "Check out this month's deals!",
    Contacts = new[] { new Contact { PhoneNumber = "01012345678" } },
});
```

`SendSmsAsync`, `SendLmsAsync` and `SendMmsAsync` all take the same `SmsRequest` and set `MessageType` for you.

---

## ASP.NET Core

Use `Sendgo.AspNetCore`, which registers `SendgoClient` as a singleton:

```csharp
// Program.cs
builder.Services.AddSendgo(builder.Configuration.GetSection("Sendgo"));

// or configure inline
builder.Services.AddSendgo(options =>
{
    options.AccessKey = builder.Configuration["Sendgo:AccessKey"]!;
    options.SecretKey = builder.Configuration["Sendgo:SecretKey"]!;
    options.KakaoSenderKey = builder.Configuration["Sendgo:KakaoSenderKey"];
    options.ApiVersion = "v2";
});
```

```json
// appsettings.json
{
  "Sendgo": {
    "AccessKey": "your_access_key",
    "SecretKey": "your_secret_key",
    "KakaoSenderKey": "your_kakao_sender_key",
    "ApiVersion": "v2"
  }
}
```

```csharp
public class OrderNotifier(SendgoClient sendgo)
{
    public Task ConfirmedAsync(string phone, string orderNo, CancellationToken ct = default) =>
        sendgo.SendAlimtalkAsync(new AlimtalkRequest
        {
            TemplateCode = "ORDER_CONFIRM_001",
            Contacts = new[] { new Contact { PhoneNumber = phone, Var1 = orderNo } },
        }, ct);
}
```

Every send method takes a `CancellationToken`, so requests are cancelled with the HTTP request they belong to.

---

## Error handling

```csharp
using Sendgo.Exceptions;

try
{
    await client.SendAlimtalkAsync(request, ct);
}
catch (SendgoException e)
{
    switch (e.ErrorCode)
    {
        case "INVALID_ACCESS_KEY":
        case "INVALID_SECRET_KEY":
            logger.LogCritical("Check the Sendgo API keys");
            break;
        case "PAYMENT_REQUIRED":
            logger.LogCritical("Out of Sendgo credit");
            break;
        case "IP_NOT_ALLOWED":
            logger.LogCritical("IP is not allow-listed");
            break;
        case "INVALID_TEMPLATE_CODE":
            logger.LogWarning("Unknown template");
            break;
        default:
            if (e.StatusCode >= 500)
                await retryQueue.EnqueueAsync(request, ct);   // transient
            else
                logger.LogError("Sendgo {Status}: {Message}", e.StatusCode, e.Message);
            break;
    }
}
```

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

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

```csharp
var created = await sendgo.CreateShortUrlAsync(new ShortUrlRequest
{
    TargetUrl = "https://example.com/promotions/summer-sale",
    Title = "Summer sale landing",
}, ct);

// Reaction stats — daily series + device / referrer / country breakdowns
var stats = await sendgo.GetShortUrlStatsAsync(code, from: "2026-08-01", ct: ct);

await sendgo.GetShortUrlsAsync(count: 10, ct: ct);
await sendgo.GetShortUrlAsync(code, ct);
await sendgo.DeactivateShortUrlAsync(code, ct);   // 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.SDK` (NuGet)
- **Repository**: [send-go/dotnet](https://github.com/send-go/dotnet)
- **Registry**: https://www.nuget.org/packages/Sendgo.SDK
- **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.