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

`sendgo` is the official Ruby SDK for the [Sendgo](https://sendgo.io) messaging API.
It uses only `net/http` and `json` from the standard library, and is the core behind the `sendgo-rails` extension.

Requires Ruby 3.1 or newer.

---

## Install

```bash
gem install sendgo
```

Or in a `Gemfile`:

```ruby
gem "sendgo"
```

---

## Quick start

```ruby
require "sendgo"

client = Sendgo::Client.new(
  access_key: ENV.fetch("SENDGO_ACCESS_KEY"),
  secret_key: ENV.fetch("SENDGO_SECRET_KEY"),
  kakao_sender_key: ENV["SENDGO_KAKAO_SENDER_KEY"],
  sms_sender_key: ENV["SENDGO_SMS_SENDER_KEY"],
  api_version: "v2"
)

# Send an Alimtalk
client.alimtalk.send(
  template_code: "ORDER_CONFIRM_001",
  contacts: [
    { contact: "01012345678", name: "Gildong Hong", var1: "ORD-001", var2: "29,000 KRW" }
  ]
)

# Send an SMS
client.sms.send_sms(
  content: "[Sendgo] Your code is 123456 (valid for 5 minutes)",
  contacts: [{ contact: "01012345678" }]
)
```

`Sendgo::Client.new` raises `ArgumentError` when `access_key` or `secret_key` is missing, so a misconfigured client fails at construction rather than at the first send.
All methods take keyword arguments only.

---

## Alimtalk in detail

```ruby
# Multiple recipients
client.alimtalk.send(
  template_code: "ORDER_CONFIRM_001",
  contacts: [
    { contact: "01011111111", name: "Gildong Hong", var1: "ORD-001" },
    { contact: "01022222222", name: "Chulsoo Kim", var1: "ORD-002" }
  ]
)

# Scheduled send
client.alimtalk.send(
  template_code: "PROMO_SUMMER_2026",
  schedule_type: "SCHEDULED",
  at: "2026-07-28 09:00:00",
  contacts: [{ contact: "01012345678", var1: "Summer sale" }]
)

# Fall back to SMS when the Alimtalk fails
client.alimtalk.send(
  template_code: "DELIVERY_START_001",
  replace_sms: "Y",
  sms_subject: "[Shipping notice]",
  sms_content: "Your order has shipped.\nTracking: #{'#{var2}'}",
  contacts: [{ contact: "01012345678", var1: "ORD-001", var2: "1234567890" }]
)
```

Template placeholders use the `#{var2}` form, which collides with Ruby string interpolation — put them in single-quoted strings, or escape as shown above.

---

## 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`.
```ruby
# Text
client.friendtalk.send(
  content: "Hello! Check out this month's deals.",
  contacts: [{ contact: "01012345678" }]
)

# Image
client.friendtalk.send(
  message_type: "FI",
  content: "This week's featured products.",
  image_url: "https://cdn.example.com/banner.jpg",
  image_link: "https://example.com/event",
  contacts: [{ contact: "01012345678" }]
)

# With buttons
client.friendtalk.send(
  content: "Your coupon has arrived. Use it now!",
  buttons: [{ name: "Get coupon", type: "WL", linkMo: "https://example.com/coupon" }],
  contacts: [{ 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 `api_version: "v2"`.

```ruby
# Single send — channel friends
client.brand_message.send(
  targeting: "M",
  message_type: "FL",
  friend_template_uuid: "9cd5460b-6458-4edc-9b11-c26d3013c340",
  contacts: [{ contact: "01012345678", var1: "29,000 KRW" }]
)

# Broadcast — every consenting channel friend (no contacts)
client.brand_message.broadcast(
  message_type: "FW",
  friend_template_uuid: "9cd5460b-6458-4edc-9b11-c26d3013c340"
)

# Campaign lookups
campaigns = client.brand_message.campaigns(from: "2026-08-01", count: 10)
one = client.brand_message.campaign("1f0a6d0e-6b3b-4f0f-9b2f-2f6f6a1b7c11")
```

`broadcast` drops `contacts` and forces `targeting: "F"` — an empty recipient array would be rejected as an invalid request.

The carousel payload argument is named `list` and is passed through unchanged.

---

## SMS / LMS / MMS

```ruby
# SMS (up to 90 bytes)
client.sms.send_sms(
  content: "[Sendgo] Your code is 123456 (valid for 5 minutes)",
  contacts: [{ contact: "01012345678" }]
)

# LMS (long text, up to 2,000 bytes)
client.sms.send_lms(
  subject: "[Important] Scheduled maintenance",
  content: "Maintenance: 2026-07-25 02:00–06:00",
  contacts: [{ contact: "01012345678" }]
)

# MMS
client.sms.send_mms(
  subject: "[Event] July deals",
  content: "Check out this month's deals!",
  contacts: [{ contact: "01012345678" }]
)
```

---

## Rails

Use `sendgo-rails`, which builds a memoised client from `config.sendgo` with ENV fallbacks:

```ruby
# config/application.rb
config.sendgo.access_key = ENV["SENDGO_ACCESS_KEY"]
config.sendgo.secret_key = ENV["SENDGO_SECRET_KEY"]
config.sendgo.kakao_sender_key = ENV["SENDGO_KAKAO_SENDER_KEY"]
config.sendgo.api_version = "v2"
```

```ruby
Sendgo::Rails.client.alimtalk.send(
  template_code: "ORDER_CONFIRM_001",
  contacts: [{ contact: order.phone, var1: order.number }]
)
```

`Sendgo::Rails.client` returns the full core client, so `brand_message`, `friendtalk` and `sms` are all available on it. Call `Sendgo::Rails.reset!` in tests to drop the memoised instance.

### Active Job

Sending is a network call, so it belongs in a job:

```ruby
class SendOrderConfirmJob < ApplicationJob
  retry_on Sendgo::SendgoError, wait: :polynomially_longer, attempts: 3

  def perform(phone, order_no)
    Sendgo::Rails.client.alimtalk.send(
      template_code: "ORDER_CONFIRM_001",
      contacts: [{ contact: phone, var1: order_no }]
    )
  rescue Sendgo::SendgoError => e
    # A 4xx will not succeed on retry — fail immediately instead of burning attempts.
    raise e if e.status_code >= 500

    Rails.logger.error("Sendgo #{e.status_code} [#{e.error_code}]: #{e.message}")
  end
end
```

---

## Error handling

```ruby
begin
  client.alimtalk.send(
    template_code: "ORDER_CONFIRM_001",
    contacts: [{ contact: "01012345678" }]
  )
rescue Sendgo::SendgoError => e
  case e.error_code
  when "INVALID_ACCESS_KEY", "INVALID_SECRET_KEY"
    notify_ops("Check the Sendgo API keys.")
  when "PAYMENT_REQUIRED"
    notify_ops("Out of Sendgo credit.")
  when "IP_NOT_ALLOWED"
    notify_ops("IP is not allow-listed.")
  when "INVALID_TEMPLATE_CODE"
    logger.warn("Unknown template")
  else
    e.status_code >= 500 ? retry_later : logger.error("Sendgo #{e.status_code}: #{e.message}")
  end
end
```

Branch on `error_code`, 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

| Argument | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|
| `access_key` | `String` | **required** | — | Sendgo access key |
| `secret_key` | `String` | **required** | — | Sendgo secret key |
| `kakao_sender_key` | `String` | optional | `nil` | Kakao sender profile key |
| `sms_sender_key` | `String` | optional | `nil` | SMS caller ID key |
| `api_version` | `String` | optional | `"v1"` | API version (`v1` \| `v2`) |
| `base_url` | `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`.

```ruby
created = sendgo.short_url.create(
  target_url: "https://example.com/promotions/summer-sale",
  title: "Summer sale landing"
)

code = created.dig("data", "code")
link = created.dig("data", "shortUrl")

# Reaction stats — daily series + device / referrer / country breakdowns
stats = sendgo.short_url.stats(code, from: "2026-08-01")

sendgo.short_url.list(count: 10)
sendgo.short_url.show(code)
sendgo.short_url.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` (RubyGems)
- **Repository**: [send-go/ruby](https://github.com/send-go/ruby)
- **Registry**: https://rubygems.org/gems/sendgo
- **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 `kakao_sender_key`, and a caller ID under **Sender numbers** for SMS.