> **The official Spring Boot starter for sending Kakao Alimtalk, Brand Message and SMS**

`io.sendgo:sendgo-spring` wraps the [`io.sendgo:sendgo-java`](https://github.com/send-go/java) core as a Spring Boot starter: set `sendgo.access-key` and a `SendgoClient` bean is auto-configured.

Requires Spring Boot 3.x and Java 17+.

---

## Install

### Gradle

```groovy
implementation "io.sendgo:sendgo-spring:1.0.1"
```

### Maven

```xml
<dependency>
    <groupId>io.sendgo</groupId>
    <artifactId>sendgo-spring</artifactId>
    <version>1.0.1</version>
</dependency>
```

The core `sendgo-java` comes along as a transitive dependency.

---

## Configure

```yaml
# application.yml
sendgo:
  access-key: ${SENDGO_ACCESS_KEY}
  secret-key: ${SENDGO_SECRET_KEY}
  kakao-sender-key: ${SENDGO_KAKAO_SENDER_KEY}
  sms-sender-key: ${SENDGO_SMS_SENDER_KEY}
  api-version: v2
```

Or in properties form:

```properties
sendgo.access-key=${SENDGO_ACCESS_KEY}
sendgo.secret-key=${SENDGO_SECRET_KEY}
sendgo.kakao-sender-key=${SENDGO_KAKAO_SENDER_KEY}
sendgo.api-version=v2
```

The starter ships configuration metadata, so your IDE autocompletes these keys.
Auto-configuration is conditional on `sendgo.access-key` being present — the app still starts without it, which keeps local profiles that do not send messages working.

Both the starter and the bare `sendgo-java` core default `api-version` to **`v2`**, so you only need to set it to pin `v1` for a legacy integration.

---

## Inject the client

```java
import io.sendgo.SendgoClient;
import io.sendgo.model.AlimtalkRequest;
import io.sendgo.model.Contact;
import org.springframework.stereotype.Service;

@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());
    }
}
```

Every channel is a method on the bean: `alimtalk()`, `friendtalk()`, `brandMessage()`, `sms()`.

### Overriding the bean

The auto-configured bean is `@ConditionalOnMissingBean`, so declaring your own takes precedence — useful for multi-tenant setups or a custom base URL per environment:

```java
@Configuration
public class SendgoConfiguration {

    @Bean
    public SendgoClient sendgoClient(TenantContext tenants) {
        return new SendgoClient(io.sendgo.SendgoConfig.builder()
                .accessKey(tenants.current().sendgoAccessKey())
                .secretKey(tenants.current().sendgoSecretKey())
                .apiVersion("v2")
                .build());
    }
}
```

---

## 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 starter's default.

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

@Service
public class CampaignService {

    private final SendgoClient sendgo;

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

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

    // Broadcast — every consenting channel friend (no recipient list)
    public Map<String, Object> announce() {
        return sendgo.brandMessage().broadcast(BrandMessageRequest.builder()
                .messageType("FW")
                .friendTemplateUuid("9cd5460b-6458-4edc-9b11-c26d3013c340")
                .build());
    }

    // A broadcast is asynchronous upstream, so poll for the result
    public Map<String, Object> status(String campaignId) {
        return sendgo.brandMessage().campaign(campaignId);
    }
}
```

---

## Sending after a transaction commits

Sending inside a transaction means a later rollback still leaves the message delivered. Use an application event committed by the transaction:

```java
public record OrderCreated(String phone, String orderNo) {}

@Service
public class OrderService {

    private final ApplicationEventPublisher events;
    private final OrderRepository orders;

    @Transactional
    public void create(OrderForm form) {
        Order order = orders.save(Order.from(form));
        events.publishEvent(new OrderCreated(order.getPhone(), order.getNumber()));
    }
}

@Component
public class OrderCreatedListener {

    private final OrderNotifier notifier;

    @Async
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void on(OrderCreated event) {
        notifier.confirmed(event.phone(), event.orderNo());
    }
}
```

`AFTER_COMMIT` guarantees the row is durable before the message goes out, and `@Async` keeps the outbound HTTP call off the request thread.

---

## Retrying transient failures

```java
@Service
public class ResilientNotifier {

    private final SendgoClient sendgo;

    @Retryable(
            retryFor = SendgoException.class,
            noRetryFor = { },              // filtered in the recover method instead
            maxAttempts = 3,
            backoff = @Backoff(delay = 1000, multiplier = 2))
    public void send(AlimtalkRequest request) {
        sendgo.alimtalk().send(request);
    }

    @Recover
    public void recover(SendgoException e, AlimtalkRequest request) {
        // A 4xx will not succeed on retry — log it and stop.
        log.error("Sendgo {} [{}]: {}", e.getStatusCode(), e.getErrorCode(), e.getMessage());
    }
}
```

If you only want 5xx retried, check `getStatusCode()` at the top of `send` and rethrow a non-retryable exception for 4xx.

---

## Testing

Replace the bean with a mock so no request leaves the test:

```java
@SpringBootTest
class OrderNotifierTest {

    @MockBean
    SendgoClient sendgo;

    @Autowired
    OrderNotifier notifier;

    @Test
    void sendsAlimtalk() {
        var alimtalk = mock(AlimtalkService.class);
        given(sendgo.alimtalk()).willReturn(alimtalk);

        notifier.confirmed("01012345678", "ORD-001");

        then(alimtalk).should().send(any(AlimtalkRequest.class));
    }
}
```

---

## 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");
        default -> {
            if (e.getStatusCode() >= 500) {
                throw e;   // let @Retryable handle it
            }
            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 core SDK: the token is reissued and the request retried once.

---

## Properties reference

| Property | Required | Default | Description |
|----------|----------|---------|-------------|
| `sendgo.access-key` | **required** | — | Sendgo access key; also enables auto-configuration |
| `sendgo.secret-key` | **required** | — | Sendgo secret key |
| `sendgo.kakao-sender-key` | optional | `null` | Kakao sender profile key |
| `sendgo.sms-sender-key` | optional | `null` | SMS caller ID key |
| `sendgo.api-version` | optional | `v2` | API version (`v1` \| `v2`) |
| `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`.

```java
@Service
public class LinkService {

    private final SendgoClient sendgo;

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

    public String shorten(String targetUrl) {
        Map<String, Object> created = sendgo.shortUrl().create(ShortUrlRequest.builder()
                .targetUrl(targetUrl)
                .build());

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

        return (String) data.get("shortUrl");
    }

    public Map<String, Object> stats(String code) {
        return sendgo.shortUrl().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**: `io.sendgo:sendgo-spring` (Maven Central)
- **Repository**: [send-go/spring](https://github.com/send-go/spring)
- **Registry**: https://central.sonatype.com/artifact/io.sendgo/sendgo-spring
- **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 `sendgo.kakao-sender-key`, and a caller ID under **Sender numbers** for SMS.