> **The pure-Java SDK for sending Kakao Alimtalk, Brand Message and SMS**

`io.sendgo:sendgo-java` is the **framework-agnostic Java SDK** for the [Sendgo](https://sendgo.io) messaging API.
It builds on `java.net.http.HttpClient` and Jackson — no Spring required.

Requires Java 17 or newer.

---

## Install

### Gradle

```groovy
implementation "io.sendgo:sendgo-java:1.1.0"
```

### Maven

```xml
<dependency>
    <groupId>io.sendgo</groupId>
    <artifactId>sendgo-java</artifactId>
    <version>1.1.0</version>
</dependency>
```

---

## Quick start

```java
import io.sendgo.SendgoClient;
import io.sendgo.SendgoConfig;
import io.sendgo.model.AlimtalkRequest;
import io.sendgo.model.Contact;

SendgoClient sendgo = new SendgoClient(SendgoConfig.builder()
        .accessKey(System.getenv("SENDGO_ACCESS_KEY"))
        .secretKey(System.getenv("SENDGO_SECRET_KEY"))
        .kakaoSenderKey(System.getenv("SENDGO_KAKAO_SENDER_KEY"))
        .smsSenderKey(System.getenv("SENDGO_SMS_SENDER_KEY"))
        .apiVersion("v2")
        .build());

// Send an Alimtalk
sendgo.alimtalk().send(AlimtalkRequest.builder()
        .templateCode("ORDER_CONFIRM_001")
        .contact(Contact.builder()
                .contact("01012345678")
                .name("Gildong Hong")
                .var1("ORD-001")
                .build())
        .build());
```

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

---

## Alimtalk in detail

```java
import java.util.List;

// Multiple recipients
sendgo.alimtalk().send(AlimtalkRequest.builder()
        .templateCode("ORDER_CONFIRM_001")
        .contacts(List.of(
                Contact.builder().contact("01011111111").name("Gildong Hong").var1("ORD-001").build(),
                Contact.builder().contact("01022222222").name("Chulsoo Kim").var1("ORD-002").build()))
        .build());

// Scheduled send
sendgo.alimtalk().send(AlimtalkRequest.builder()
        .templateCode("PROMO_SUMMER_2026")
        .scheduleType("SCHEDULED")
        .at("2026-07-28 09:00:00")
        .contact(Contact.builder().contact("01012345678").var1("Summer sale").build())
        .build());
```

For named template variables beyond `var1`–`var8`, `Contact` accepts an arbitrary map:

```java
Contact.builder()
        .contact("01012345678")
        .variable("title", "Order confirmed")
        .variable("date", "2026-08-10")
        .build();
```

`Contact` serialises those through Jackson's `@JsonAnyGetter`, so named and numbered variables can be mixed in one request.

---

## 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`.
```java
import io.sendgo.model.FriendtalkRequest;

sendgo.friendtalk().send(FriendtalkRequest.builder()
        .messageType("FI")
        .content("This week's featured products.")
        .imageUrl("https://cdn.example.com/banner.jpg")
        .imageLink("https://example.com/event")
        .contact(Contact.builder().contact("01012345678").build())
        .build());
```

---

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

```java
import io.sendgo.model.BrandMessageRequest;

// Single send — channel friends
sendgo.brandMessage().send(BrandMessageRequest.builder()
        .targeting("M")
        .messageType("FL")
        .friendTemplateUuid("9cd5460b-6458-4edc-9b11-c26d3013c340")
        .contact(Contact.builder().contact("01012345678").var1("29,000 KRW").build())
        .build());

// Broadcast — every consenting channel friend (contacts is dropped)
sendgo.brandMessage().broadcast(BrandMessageRequest.builder()
        .messageType("FW")
        .friendTemplateUuid("9cd5460b-6458-4edc-9b11-c26d3013c340")
        .build());

// Campaign lookups. Pass nulls to fall back to the server defaults.
var list = sendgo.brandMessage().campaigns(null, null, 10);
var one  = sendgo.brandMessage().campaign("1f0a6d0e-6b3b-4f0f-9b2f-2f6f6a1b7c11");
```

`campaigns()` also has a no-argument overload that uses every server default.
`broadcast()` rebuilds the request with `targeting("F")` and no contacts — an empty recipient array would be rejected as an invalid request.

---

## SMS / LMS / MMS

```java
import io.sendgo.model.SmsRequest;

// SMS (up to 90 bytes)
sendgo.sms().sendSms(SmsRequest.sms()
        .content("[Sendgo] Your code is 123456 (valid for 5 minutes)")
        .contact(Contact.builder().contact("01012345678").build()));

// LMS (long text, up to 2,000 bytes)
sendgo.sms().sendLms(SmsRequest.lms()
        .subject("[Important] Scheduled maintenance")
        .content("Maintenance: 2026-07-25 02:00-06:00")
        .contact(Contact.builder().contact("01012345678").build()));

// MMS
sendgo.sms().sendMms(SmsRequest.mms()
        .subject("[Event] July deals")
        .content("Check out this month's deals!")
        .contact(Contact.builder().contact("01012345678").build()));
```

`SmsRequest` does **not** use `builder()`/`build()` like the other request types — it is created with the
`sms()` / `lms()` / `mms()` static factories and configured with fluent setters that return the request itself.

`sendSms` / `sendLms` / `sendMms` also force the message type, so the factory and the send method cannot
disagree; `send(...)` uses whatever type the request carries.

---

## Spring Boot

Use `io.sendgo:sendgo-spring`, which auto-configures a `SendgoClient` bean once `sendgo.access-key` is set:

```yaml
sendgo:
  access-key: ${SENDGO_ACCESS_KEY}
  secret-key: ${SENDGO_SECRET_KEY}
  kakao-sender-key: ${SENDGO_KAKAO_SENDER_KEY}
  api-version: v2
```

```java
@Service
public class OrderNotifier {

    private final SendgoClient sendgo;

    public OrderNotifier(SendgoClient sendgo) {
        this.sendgo = sendgo;
    }

    public void confirmed(String phone, String orderNo) {
        sendgo.alimtalk().send(AlimtalkRequest.builder()
                .templateCode("ORDER_CONFIRM_001")
                .contact(Contact.builder().contact(phone).var1(orderNo).build())
                .build());
    }
}
```

The bean is `@ConditionalOnMissingBean`, so defining your own `SendgoClient` overrides it.

---

## Error handling

```java
import io.sendgo.exception.SendgoException;

try {
    sendgo.alimtalk().send(request);
} catch (SendgoException e) {
    switch (e.getErrorCode()) {
        case "INVALID_ACCESS_KEY", "INVALID_SECRET_KEY" ->
                log.error("Check the Sendgo API keys");
        case "PAYMENT_REQUIRED" ->
                log.error("Out of Sendgo credit");
        case "IP_NOT_ALLOWED" ->
                log.error("IP is not allow-listed");
        case "INVALID_TEMPLATE_CODE" ->
                log.warn("Unknown template");
        default -> {
            if (e.getStatusCode() >= 500) {
                retryLater(request);   // transient
            } else {
                log.error("Sendgo {}: {}", e.getStatusCode(), e.getMessage());
            }
        }
    }
}
```

Branch on `getErrorCode()`, 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.

`SendgoException` is a `RuntimeException`, so it does not force `try`/`catch` on every call site — handle it where you have a recovery strategy.

---

## Configuration options

| Builder method | 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 | `"v2"` | 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`.

```java
import io.sendgo.model.ShortUrlRequest;

Map<String, Object> created = sendgo.shortUrl().create(ShortUrlRequest.builder()
        .targetUrl("https://example.com/promotions/summer-sale")
        .title("Summer sale landing")
        .build());

@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) created.get("data");
String code = (String) data.get("code");

// Reaction stats — daily series + device / referrer / country breakdowns
Map<String, Object> stats = sendgo.shortUrl().stats(code, "2026-08-01", null);

sendgo.shortUrl().list(null, null, 10);
sendgo.shortUrl().show(code);
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**: `io.sendgo:sendgo-java` (Maven Central)
- **Repository**: [send-go/java](https://github.com/send-go/java)
- **Registry**: https://central.sonatype.com/artifact/io.sendgo/sendgo-java
- **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.