> **The official Rails integration for sending Kakao Alimtalk, Brand Message and SMS**

`sendgo-rails` wraps the [`sendgo`](https://github.com/send-go/ruby) core gem with a Railtie: it adds a `config.sendgo` namespace, falls back to environment variables, and exposes a memoised client as `Sendgo::Rails.client`.

---

## Install

```ruby
# Gemfile
gem "sendgo-rails"
```

```bash
bundle install
bin/rails g sendgo:install
```

The generator writes `config/initializers/sendgo.rb`. The core `sendgo` gem comes along as a dependency.

Requires Ruby 3.1+ and Rails 6.1 or newer (`railties >= 6.1`).

---

## Configure

```ruby
# config/initializers/sendgo.rb
Rails.application.config.sendgo.tap do |config|
  config.access_key       = ENV["SENDGO_ACCESS_KEY"]
  config.secret_key       = ENV["SENDGO_SECRET_KEY"]
  config.kakao_sender_key = ENV["SENDGO_KAKAO_SENDER_KEY"]
  config.sms_sender_key   = ENV["SENDGO_SMS_SENDER_KEY"]
  config.api_version      = ENV.fetch("SENDGO_API_VERSION", "v2")
  config.url              = ENV.fetch("SENDGO_URL", "https://sendgo.io")
end
```

Every setting resolves in two steps: `config.sendgo.<key>` first, then the matching environment variable. Leaving a value `nil` in the initializer is therefore the same as not setting it — the ENV value wins. That means you can drop the initializer entirely and configure the gem with environment variables alone.

Use Rails credentials instead of ENV if you prefer:

```ruby
config.access_key = Rails.application.credentials.dig(:sendgo, :access_key)
```

Note that `api_version` defaults to **`v2`** in the Rails integration (the bare Ruby core defaults to `v1`).

---

## Quick start

```ruby
Sendgo::Rails.client.alimtalk.send(
  template_code: "ORDER_CONFIRM_001",
  contacts: [{ contact: "01012345678", var1: "ORD-001" }]
)
```

`Sendgo::Rails.client` is **memoised and lazy** — nothing is built until the first call, so requiring the gem does not read your configuration at boot. Every channel from the core gem is available on it:

| Method | Channel |
|--------|---------|
| `client.alimtalk` | Kakao Alimtalk |
| `client.friendtalk` | Kakao Friendtalk |
| `client.brand_message` | Kakao Brand Message (v2 only) |
| `client.sms` | SMS / LMS / MMS |

Because the client is memoised, changing `config.sendgo` at runtime has no effect until you call `Sendgo::Rails.reset!`.

---

## Wrap it in your own class

Calling `Sendgo::Rails.client` from controllers scatters template codes through the app. A thin object keeps them in one place and gives you something to stub in tests:

```ruby
# app/services/order_notifier.rb
class OrderNotifier
  def initialize(client: Sendgo::Rails.client)
    @client = client
  end

  def confirmed(order)
    @client.alimtalk.send(
      template_code: "ORDER_CONFIRM_001",
      contacts: [{ contact: order.phone, var1: order.number }]
    )
  end
end
```

---

## 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, which is the Rails integration's default.

```ruby
client = Sendgo::Rails.client

# Single send — targeting M/N/I requires `contacts`
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 recipient list
result = client.brand_message.broadcast(
  message_type: "FW",
  friend_template_uuid: "9cd5460b-6458-4edc-9b11-c26d3013c340"
)

# A broadcast is asynchronous upstream, so poll for progress
client.brand_message.campaign(result.dig("data", "campaignId"))
client.brand_message.campaigns(from: "2026-08-01", count: 10)
```

`friend_template_uuid` is a required keyword argument on `send`; `message_type` defaults to `"FT"` and `targeting` to `"M"`. `broadcast` is `send` with `targeting` forced to `"F"`, so it accepts the same arguments.

---

## Sending from a model callback

Send **after** the transaction commits — a callback that fires inside the transaction still delivers the message if a later validation rolls the record back:

```ruby
class Order < ApplicationRecord
  after_commit :notify_customer, on: :create

  private

  def notify_customer
    OrderConfirmJob.perform_later(id)
  end
end
```

---

## ActiveJob

Sending is a network call, so keep it out of the request cycle:

```ruby
# app/jobs/order_confirm_job.rb
class OrderConfirmJob < ApplicationJob
  queue_as :notifications

  # 5xx is transient; a 4xx will not succeed on retry.
  # `:polynomially_longer` needs Rails 7.1+; on 6.1–7.0 use `:exponentially_longer`.
  retry_on Sendgo::SendgoError, wait: :polynomially_longer, attempts: 3 do |job, error|
    raise error if error.status_code >= 500

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

  def perform(order_id)
    order = Order.find(order_id)

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

`retry_on` with a block runs the block **after the attempts are exhausted**. If you want to stop immediately on a 4xx, rescue inside `perform` and call `discard_on` instead:

```ruby
discard_on Sendgo::SendgoError do |_job, error|
  Rails.logger.error("Dropped: Sendgo #{error.error_code}")
end
```

---

## Rake task

```ruby
# lib/tasks/promo.rake
namespace :promo do
  desc "Broadcast the current promotion to every consenting channel friend"
  task broadcast: :environment do
    result = Sendgo::Rails.client.brand_message.broadcast(
      message_type: "FW",
      friend_template_uuid: "9cd5460b-6458-4edc-9b11-c26d3013c340"
    )

    puts "Accepted: #{result.dig('data', 'campaignId')}"
  end
end
```

---

## Testing

`reset!` drops the memoised client, which is what makes a stubbed client stick:

```ruby
# spec/support/sendgo.rb
RSpec.configure do |config|
  config.before do
    Sendgo::Rails.reset!
    allow(Sendgo::Rails).to receive(:client).and_return(fake_sendgo)
  end
end

def fake_sendgo
  @fake_sendgo ||= instance_double(
    Sendgo::Client,
    alimtalk: instance_double(Sendgo::AlimtalkService, send: { "message" => "Success" }),
    brand_message: instance_double(Sendgo::BrandMessageService, send: { "message" => "Success" })
  )
end
```

```ruby
it "sends an alimtalk when an order is created" do
  expect(fake_sendgo.alimtalk).to receive(:send).with(
    hash_including(template_code: "ORDER_CONFIRM_001")
  )

  Order.create!(phone: "01012345678", number: "ORD-001")
end
```

Without `reset!`, a client memoised by an earlier example leaks into the next one.

---

## Error handling

```ruby
begin
  Sendgo::Rails.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"
    Rails.logger.error("Check the Sendgo API keys")
  when "PAYMENT_REQUIRED"
    Rails.logger.error("Out of Sendgo credit")
  when "IP_NOT_ALLOWED"
    Rails.logger.error("IP is not allow-listed")
  else
    raise if e.status_code >= 500   # transient — let the job retry

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

`Sendgo::SendgoError` exposes `status_code`, `error_code`, `endpoint`, `api_version` and `response_body`. 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 core gem: the token is reissued and the request retried once.

---

## Configuration reference

| `config.sendgo` key | Environment variable | Required | Default | Description |
|---------------------|----------------------|----------|---------|-------------|
| `access_key` | `SENDGO_ACCESS_KEY` | **required** | — | Sendgo access key |
| `secret_key` | `SENDGO_SECRET_KEY` | **required** | — | Sendgo secret key |
| `kakao_sender_key` | `SENDGO_KAKAO_SENDER_KEY` | optional | `nil` | Kakao sender profile key |
| `sms_sender_key` | `SENDGO_SMS_SENDER_KEY` | optional | `nil` | SMS caller ID key |
| `api_version` | `SENDGO_API_VERSION` | optional | `"v2"` | API version (`v1` \| `v2`) |
| `url` | `SENDGO_URL` | 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::Rails.client.short_url.create(
  target_url: promotion_url(@promotion),
  title: @promotion.name
)

code = created.dig("data", "code")
stats = Sendgo::Rails.client.short_url.stats(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-rails` (RubyGems)
- **Repository**: [send-go/rails](https://github.com/send-go/rails)
- **Registry**: https://rubygems.org/gems/sendgo-rails
- **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.