본문 바로가기
Documentation menu

Java / SDK REFERENCE

Spring Boot SDK Guide

The official Spring Boot starter for sending Kakao Alimtalk, Brand Message and SMS — auto-configured client bean from application properties.

이 문서의 목차
패키지
io.sendgo:sendgo-spring
언어
Java
레지스트리
Maven Central
설치
implementation "io.sendgo:sendgo-spring:1.0.1"

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

io.sendgo:sendgo-spring wraps the io.sendgo:sendgo-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

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

Maven

<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

# 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:

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

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:

@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.

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:

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

@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:

@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

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.

@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

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.

이 패키지로 할 수 있는 것

관련 패키지

Focus on building. Leave messaging to Sendgo.Back to top ↑