본문 바로가기
Documentation menu

Python / SDK REFERENCE

FastAPI SDK Guide

The official FastAPI extension for sending Kakao Alimtalk, Brand Message and SMS — injected as a dependency, configured from environment variables.

이 문서의 목차
패키지
sendgo-fastapi
언어
Python
레지스트리
PyPI
설치
pip install sendgo-fastapi

The official FastAPI extension for sending Kakao Alimtalk, Brand Message and SMS

sendgo-fastapi wraps the sendgo-python core as a FastAPI dependency, configured through a pydantic-settings model.

Requires FastAPI 0.110+ and Python 3.10+.


Install

pip install sendgo-fastapi

The core sendgo-python comes along as a dependency.


Configure

SendgoSettings reads these environment variables, so there is nothing to wire up in code:

SENDGO_ACCESS_KEY=your_access_key
SENDGO_SECRET_KEY=your_secret_key
SENDGO_KAKAO_SENDER_KEY=your_kakao_sender_key
SENDGO_SMS_SENDER_KEY=your_sms_sender_key
SENDGO_API_VERSION=v2

access_key and secret_key have no defaults, so pydantic raises a validation error at startup if either is missing — a misconfigured deploy fails fast instead of at the first send.


Quick start

from fastapi import Depends, FastAPI
from sendgo_fastapi import SendgoDep

app = FastAPI()

@app.post("/notify")
async def notify(phone: str, order_no: str, sendgo = Depends(SendgoDep)):
    return sendgo.alimtalk.send(
        template_code="ORDER_CONFIRM_001",
        contacts=[{"contact": phone, "var1": order_no}],
    )

The dependency yields the core client, so every channel is available:

Attribute Channel
sendgo.alimtalk Kakao Alimtalk
sendgo.friendtalk Kakao Friendtalk
sendgo.brand_message Kakao Brand Message (v2 only)
sendgo.sms SMS / LMS / MMS

Annotated form

With Annotated the dependency reads as part of the type, which keeps long signatures tidy:

from typing import Annotated
from fastapi import Depends
from sendgo import Sendgo
from sendgo_fastapi import SendgoDep

SendgoClient = Annotated[Sendgo, Depends(SendgoDep)]

@app.post("/notify")
async def notify(phone: str, order_no: str, sendgo: SendgoClient):
    return sendgo.alimtalk.send(
        template_code="ORDER_CONFIRM_001",
        contacts=[{"contact": phone, "var1": order_no}],
    )

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. Set SENDGO_API_VERSION=v2.

@app.post("/campaigns/targeted")
async def targeted(phone: str, sendgo: SendgoClient):
    return sendgo.brand_message.send(
        targeting="M",
        message_type="FL",
        friend_template_uuid="9cd5460b-6458-4edc-9b11-c26d3013c340",
        contacts=[{"contact": phone, "var1": "29,000 KRW"}],
    )

@app.post("/campaigns/broadcast")
async def broadcast(sendgo: SendgoClient):
    # No recipient list — Kakao expands the audience
    return sendgo.brand_message.broadcast(
        message_type="FW",
        friend_template_uuid="9cd5460b-6458-4edc-9b11-c26d3013c340",
    )

@app.get("/campaigns")
async def campaigns(sendgo: SendgoClient, count: int = 10):
    # `from` is a Python keyword, so the argument is `from_`.
    return sendgo.brand_message.campaigns(count=count)

@app.get("/campaigns/{campaign_id}")
async def campaign(campaign_id: str, sendgo: SendgoClient):
    return sendgo.brand_message.campaign(campaign_id)

A broadcast is processed asynchronously upstream, so the send response only acknowledges acceptance — poll GET /campaigns/{campaign_id} for progress.


Background tasks

The core client is synchronous, so calling it directly inside an async def endpoint blocks the event loop. Move sending to a background task:

from fastapi import BackgroundTasks, Depends, FastAPI
from sendgo import Sendgo, SendgoError
from sendgo_fastapi import SendgoDep

def deliver(sendgo: Sendgo, phone: str, order_no: str) -> None:
    try:
        sendgo.alimtalk.send(
            template_code="ORDER_CONFIRM_001",
            contacts=[{"contact": phone, "var1": order_no}],
        )
    except SendgoError as e:
        logger.error("Sendgo %s [%s]: %s", e.status_code, e.error_code, e)

@app.post("/orders", status_code=202)
async def create_order(
    phone: str,
    order_no: str,
    background: BackgroundTasks,
    sendgo = Depends(SendgoDep),
):
    background.add_task(deliver, sendgo, phone, order_no)

    return {"accepted": True}

For anything that must survive a restart, use a real queue (Celery, ARQ, Dramatiq) rather than BackgroundTasks.

Running in a thread pool

If you need the result inside the request, keep the event loop free with run_in_threadpool:

from starlette.concurrency import run_in_threadpool

@app.post("/notify")
async def notify(phone: str, sendgo: SendgoClient):
    return await run_in_threadpool(
        sendgo.alimtalk.send,
        template_code="ORDER_CONFIRM_001",
        contacts=[{"contact": phone}],
    )

Testing

Override the dependency so no request leaves your test suite:

from unittest.mock import MagicMock
from fastapi.testclient import TestClient
from sendgo_fastapi import SendgoDep

fake = MagicMock()
app.dependency_overrides[SendgoDep] = lambda: fake

client = TestClient(app)
client.post("/notify", params={"phone": "01012345678", "order_no": "ORD-001"})

fake.alimtalk.send.assert_called_once()

app.dependency_overrides.clear()

Error handling

Map Sendgo errors onto HTTP status codes rather than letting them surface as 500s:

from fastapi import HTTPException
from sendgo import SendgoError

@app.post("/notify")
async def notify(phone: str, sendgo: SendgoClient):
    try:
        return sendgo.alimtalk.send(
            template_code="ORDER_CONFIRM_001",
            contacts=[{"contact": phone}],
        )
    except SendgoError as e:
        if e.error_code in ("INVALID_ACCESS_KEY", "INVALID_SECRET_KEY", "IP_NOT_ALLOWED"):
            # Our configuration is wrong, not the caller's request.
            logger.critical("Sendgo configuration error: %s", e.error_code)
            raise HTTPException(status_code=500, detail="Messaging unavailable") from e

        if e.error_code == "PAYMENT_REQUIRED":
            raise HTTPException(status_code=402, detail="Out of credit") from e

        if e.status_code >= 500:
            raise HTTPException(status_code=502, detail="Upstream error") from e

        raise HTTPException(status_code=400, detail=e.error_code) from e

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 SDK: the token is reissued and the request retried once.


Settings reference

Environment variable Field Required Default Description
SENDGO_ACCESS_KEY access_key required — Sendgo access key
SENDGO_SECRET_KEY secret_key required — Sendgo secret key
SENDGO_KAKAO_SENDER_KEY kakao_sender_key optional None Kakao sender profile key
SENDGO_SMS_SENDER_KEY sms_sender_key optional None SMS caller ID key
SENDGO_API_VERSION api_version optional "v1" API version (v1 | v2)
SENDGO_BASE_URL base_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.

@app.post("/shorten")
async def shorten(target_url: str, sendgo: SendgoClient):
    created = sendgo.short_url.create(target_url=target_url)

    return {"shortUrl": created["data"]["shortUrl"]}

@app.get("/shorten/{code}/stats")
async def link_stats(code: str, sendgo: SendgoClient):
    return sendgo.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

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 ↑