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

`github.com/send-go/go` is the official Go SDK for the [Sendgo](https://sendgo.io) messaging API.
It uses only the standard library — no third-party dependencies.

---

## Install

```bash
go get github.com/send-go/go
```

```go
import "github.com/send-go/go/sendgo"
```

---

## Quick start

```go
package main

import (
    "log"
    "os"

    "github.com/send-go/go/sendgo"
)

func main() {
    client, err := sendgo.New(sendgo.Config{
        AccessKey:      os.Getenv("SENDGO_ACCESS_KEY"),
        SecretKey:      os.Getenv("SENDGO_SECRET_KEY"),
        KakaoSenderKey: os.Getenv("SENDGO_KAKAO_SENDER_KEY"),
        SmsSenderKey:   os.Getenv("SENDGO_SMS_SENDER_KEY"),
        APIVersion:     "v2",
    })
    if err != nil {
        log.Fatal(err)
    }

    // Send an Alimtalk
    err = client.Alimtalk.Send(sendgo.AlimtalkRequest{
        TemplateCode: "ORDER_CONFIRM_001",
        Contacts: []sendgo.Contact{
            {Contact: "01012345678", Name: "Gildong Hong", Var1: "ORD-001", Var2: "29,000 KRW"},
        },
    })
    if err != nil {
        log.Fatal(err)
    }
}
```

`sendgo.New` returns an error when the required keys are missing, so configuration mistakes surface at startup rather than at the first send.

---

## Alimtalk in detail

```go
// Multiple recipients
err = client.Alimtalk.Send(sendgo.AlimtalkRequest{
    TemplateCode: "ORDER_CONFIRM_001",
    Contacts: []sendgo.Contact{
        {Contact: "01011111111", Name: "Gildong Hong", Var1: "ORD-001"},
        {Contact: "01022222222", Name: "Chulsoo Kim", Var1: "ORD-002"},
    },
})

// Scheduled send — `At` is a *string so "unset" and "empty" stay distinct
at := "2026-07-28 09:00:00"
err = client.Alimtalk.Send(sendgo.AlimtalkRequest{
    TemplateCode: "PROMO_SUMMER_2026",
    ScheduleType: "SCHEDULED",
    At:           &at,
    Contacts:     []sendgo.Contact{{Contact: "01012345678", Var1: "Summer sale"}},
})
```

For template variables beyond `Var1`–`Var8`, `Contact` also carries an arbitrary map:

```go
{Contact: "01012345678", Variables: map[string]string{"title": "Order", "date": "2026-08-10"}}
```

`Contact` implements a custom `MarshalJSON` that flattens `Variables` into the request body, so named variables and the numbered `Var*` fields can be mixed freely.

---

## 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`.
```go
imageURL := "https://cdn.example.com/banner.jpg"

err = client.Friendtalk.Send(sendgo.FriendtalkRequest{
    MessageType: "FI",
    Content:     "This week's featured products.",
    ImageURL:    &imageURL,
    Contacts:    []sendgo.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"`.

```go
// Single send — channel friends. Unlike the other channels this returns the
// response body, because you need the campaignId to poll results.
resp, err := client.BrandMessage.Send(sendgo.BrandMessageRequest{
    Targeting:          "M",
    MessageType:        "FL",
    FriendTemplateUUID: "9cd5460b-6458-4edc-9b11-c26d3013c340",
    Contacts: []sendgo.Contact{
        {Contact: "01012345678", Var1: "29,000 KRW"},
    },
})

// Broadcast — every consenting channel friend (Contacts is dropped)
resp, err = client.BrandMessage.Broadcast(sendgo.BrandMessageRequest{
    MessageType:        "FW",
    FriendTemplateUUID: "9cd5460b-6458-4edc-9b11-c26d3013c340",
})

// Campaign lookups
list, err := client.BrandMessage.Campaigns(sendgo.BrandMessageListQuery{Count: 10})
one, err := client.BrandMessage.Campaign("1f0a6d0e-6b3b-4f0f-9b2f-2f6f6a1b7c11")
```

`Broadcast` clears `Contacts` before sending. The field is tagged `omitempty`, so a broadcast request omits the key entirely rather than sending an empty array, which the API would reject.

---

## SMS / LMS / MMS

```go
// SMS (up to 90 bytes)
err = client.SMS.SendSMS(sendgo.SmsRequest{
    Content:  "[Sendgo] Your code is 123456 (valid for 5 minutes)",
    Contacts: []sendgo.Contact{{Contact: "01012345678"}},
})

// LMS (long text)
subject := "[Important] Scheduled maintenance"
err = client.SMS.SendLMS(sendgo.SmsRequest{
    Subject:  &subject,
    Content:  "Maintenance: 2026-07-25 02:00–06:00",
    Contacts: []sendgo.Contact{{Contact: "01012345678"}},
})
```

---

## Error handling

```go
import "errors"

err := client.Alimtalk.Send(req)
if err != nil {
    var sendgoErr *sendgo.SendgoError
    if errors.As(err, &sendgoErr) {
        switch sendgoErr.ErrorCode {
        case "INVALID_ACCESS_KEY", "INVALID_SECRET_KEY":
            log.Printf("check the Sendgo API keys")
        case "PAYMENT_REQUIRED":
            log.Printf("out of Sendgo credit")
        case "IP_NOT_ALLOWED":
            log.Printf("IP is not allow-listed")
        default:
            if sendgoErr.StatusCode >= 500 {
                // transient — safe to retry with backoff
                return retryLater(req)
            }
            log.Printf("sendgo %d: %s", sendgoErr.StatusCode, sendgoErr.Message)
        }
        return err
    }
    // network-level failure: the request may or may not have been accepted
    return err
}
```

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.

A send endpoint that returns an error has **not** accepted the request, so 4xx/5xx responses are safe to retry without duplicate delivery. A network timeout is different — check the campaign list before retrying.

---

## Concurrency

`*Client` is safe for concurrent use. The token manager serialises refreshes, so sharing one client across goroutines will not issue duplicate tokens. Create it once at startup and pass it around.

---

## Configuration options

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

```go
created, err := client.ShortURL.Create(sendgo.ShortURLRequest{
	TargetURL: "https://example.com/promotions/summer-sale",
	Title:     "Summer sale landing",
})
if err != nil {
	log.Fatal(err)
}

data := created["data"].(map[string]any)
code := data["code"].(string)

// Reaction stats — daily series + device / referrer / country breakdowns
stats, err := client.ShortURL.Stats(code, sendgo.ShortURLStatsQuery{From: "2026-08-01"})

client.ShortURL.List(sendgo.ShortURLListQuery{Count: 10})
client.ShortURL.Show(code)
client.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**: `github.com/send-go/go` (Go Modules)
- **Repository**: [send-go/go](https://github.com/send-go/go)
- **Reference**: https://pkg.go.dev/github.com/send-go/go
- **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.