본문 바로가기
Documentation menu

C# / .NET / SDK REFERENCE

ASP.NET Core SDK Guide

The official ASP.NET Core extension for sending Kakao Alimtalk, Brand Message and SMS — registers the client for dependency injection.

이 문서의 목차
패키지
Sendgo.AspNetCore
언어
C# / .NET
레지스트리
NuGet
설치
dotnet add package Sendgo.AspNetCore

The official ASP.NET Core extension for sending Kakao Alimtalk, Brand Message and SMS

Sendgo.AspNetCore wraps the Sendgo.SDK core with an AddSendgo extension that registers SendgoClient as a singleton.

Targets .NET 8.0.


Install

dotnet add package Sendgo.AspNetCore

The core Sendgo.SDK comes along as a dependency.


Register the client

From configuration

// Program.cs
builder.Services.AddSendgo(builder.Configuration.GetSection("Sendgo"));
// appsettings.json
{
  "Sendgo": {
    "AccessKey": "your_access_key",
    "SecretKey": "your_secret_key",
    "KakaoSenderKey": "your_kakao_sender_key",
    "SmsSenderKey": "your_sms_sender_key",
    "ApiVersion": "v2"
  }
}

Keep the keys out of appsettings.json in source control — bind them from user secrets in development and from environment variables or a vault in production:

dotnet user-secrets set "Sendgo:AccessKey" "your_access_key"
dotnet user-secrets set "Sendgo:SecretKey" "your_secret_key"

Environment variables use the double-underscore separator: Sendgo__AccessKey.

With a lambda

builder.Services.AddSendgo(options =>
{
    options.AccessKey = builder.Configuration["Sendgo:AccessKey"]!;
    options.SecretKey = builder.Configuration["Sendgo:SecretKey"]!;
    options.KakaoSenderKey = builder.Configuration["Sendgo:KakaoSenderKey"];
    options.ApiVersion = "v2";
});

Both overloads register the same singleton, so the token cache is shared across the whole app. SendgoClient implements IDisposable, and the DI container disposes the singleton on shutdown — do not wrap it in a using.


Inject the client

using Sendgo;
using Sendgo.Models;

public class OrderNotifier(SendgoClient sendgo, ILogger<OrderNotifier> logger)
{
    public Task ConfirmedAsync(string phone, string orderNo, CancellationToken ct = default) =>
        sendgo.SendAlimtalkAsync(new AlimtalkRequest
        {
            TemplateCode = "ORDER_CONFIRM_001",
            Contacts = new[] { new Contact { PhoneNumber = phone, Var1 = orderNo } },
        }, ct);
}
builder.Services.AddScoped<OrderNotifier>();

Note that Contact.PhoneNumber serialises as contact on the wire — the property is named for C# readability.

Minimal API endpoint

app.MapPost("/notify", async (
    NotifyRequest body,
    SendgoClient sendgo,
    CancellationToken ct) =>
{
    await sendgo.SendAlimtalkAsync(new AlimtalkRequest
    {
        TemplateCode = "ORDER_CONFIRM_001",
        Contacts = new[] { new Contact { PhoneNumber = body.Phone, Var1 = body.OrderNo } },
    }, ct);

    return Results.Accepted();
});

record NotifyRequest(string Phone, string OrderNo);

Passing the endpoint's CancellationToken through means an aborted request also cancels the outbound call.


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 ApiVersion = "v2".

app.MapPost("/campaigns/targeted", async (
    string phone, SendgoClient sendgo, CancellationToken ct) =>
{
    var result = await sendgo.SendBrandMessageAsync(new BrandMessageRequest
    {
        Targeting = "M",
        MessageType = "FL",
        FriendTemplateUuid = "9cd5460b-6458-4edc-9b11-c26d3013c340",
        Contacts = new[] { new Contact { PhoneNumber = phone, Var1 = "29,000 KRW" } },
    }, ct);

    return Results.Ok(result);
});

app.MapPost("/campaigns/broadcast", async (SendgoClient sendgo, CancellationToken ct) =>
{
    // No recipient list — Kakao expands the audience
    var result = await sendgo.BroadcastBrandMessageAsync(new BrandMessageRequest
    {
        MessageType = "FW",
        FriendTemplateUuid = "9cd5460b-6458-4edc-9b11-c26d3013c340",
    }, ct);

    return Results.Accepted(value: result);
});

app.MapGet("/campaigns", (SendgoClient sendgo, CancellationToken ct) =>
    sendgo.GetBrandMessagesAsync(count: 10, ct: ct));

app.MapGet("/campaigns/{campaignId}", (string campaignId, SendgoClient sendgo, CancellationToken ct) =>
    sendgo.GetBrandMessageAsync(campaignId, ct));

A broadcast is processed asynchronously upstream, so the send response only acknowledges acceptance — poll the detail endpoint for progress.


Background sending

Sending is an outbound HTTP call, so keep it off the request path for anything the caller does not need to wait on:

public class NotificationQueue
{
    private readonly Channel<AlimtalkRequest> _channel =
        Channel.CreateBounded<AlimtalkRequest>(1000);

    public ValueTask EnqueueAsync(AlimtalkRequest request, CancellationToken ct) =>
        _channel.Writer.WriteAsync(request, ct);

    public IAsyncEnumerable<AlimtalkRequest> ReadAllAsync(CancellationToken ct) =>
        _channel.Reader.ReadAllAsync(ct);
}

public class NotificationWorker(
    NotificationQueue queue,
    SendgoClient sendgo,
    ILogger<NotificationWorker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var request in queue.ReadAllAsync(stoppingToken))
        {
            try
            {
                await sendgo.SendAlimtalkAsync(request, stoppingToken);
            }
            catch (SendgoException e) when (e.StatusCode < 500)
            {
                // A 4xx will not succeed on retry — drop it and record why.
                logger.LogError("Sendgo {Code}: {Message}", e.ErrorCode, e.Message);
            }
            catch (SendgoException e)
            {
                logger.LogWarning("Transient Sendgo failure {Status}, re-queueing", e.StatusCode);
                await queue.EnqueueAsync(request, stoppingToken);
            }
        }
    }
}
builder.Services.AddSingleton<NotificationQueue>();
builder.Services.AddHostedService<NotificationWorker>();

Testing

SendgoClient is sealed and creates its own HttpClient internally, so it cannot be subclassed or handed a stub message handler. Put your own interface in front of it and mock that — which is also what keeps your call sites independent of the SDK:

public interface IOrderNotifier
{
    Task ConfirmedAsync(string phone, string orderNo, CancellationToken ct = default);
}

public class OrderNotifier(SendgoClient sendgo) : IOrderNotifier
{
    public Task ConfirmedAsync(string phone, string orderNo, CancellationToken ct = default) =>
        sendgo.SendAlimtalkAsync(new AlimtalkRequest
        {
            TemplateCode = "ORDER_CONFIRM_001",
            Contacts = new[] { new Contact { PhoneNumber = phone, Var1 = orderNo } },
        }, ct);
}
builder.Services.AddScoped<IOrderNotifier, OrderNotifier>();
public class TestFactory : WebApplicationFactory<Program>
{
    public Mock<IOrderNotifier> Notifier { get; } = new();

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            services.RemoveAll<IOrderNotifier>();
            services.AddScoped(_ => Notifier.Object);
        });
    }
}

For an end-to-end test that must exercise the SDK itself, point Sendgo:BaseUrl at a local stub server (WireMock.Net, or a second WebApplication) instead of mocking the client.


Error handling

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

using Sendgo.Exceptions;

app.UseExceptionHandler(handler => handler.Run(async context =>
{
    var error = context.Features.Get<IExceptionHandlerFeature>()?.Error;

    if (error is not SendgoException e)
    {
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        return;
    }

    context.Response.StatusCode = e.ErrorCode switch
    {
        // Our configuration is wrong, not the caller's request.
        "INVALID_ACCESS_KEY" or "INVALID_SECRET_KEY" or "IP_NOT_ALLOWED"
            => StatusCodes.Status500InternalServerError,
        "PAYMENT_REQUIRED" => StatusCodes.Status402PaymentRequired,
        _ => e.StatusCode >= 500 ? StatusCodes.Status502BadGateway : StatusCodes.Status400BadRequest,
    };

    await context.Response.WriteAsJsonAsync(new { error = e.ErrorCode });
}));

Branch on ErrorCode, 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.


Configuration reference

The configuration section binds directly onto SendgoOptions.

Key Required Default Description
Sendgo:AccessKey required — Sendgo access key
Sendgo:SecretKey required — Sendgo secret key
Sendgo:KakaoSenderKey optional null Kakao sender profile key
Sendgo:SmsSenderKey optional null SMS caller ID key
Sendgo:ApiVersion optional v1 API version (v1 | v2)
Sendgo:BaseUrl 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.MapPost("/shorten", async (
    string targetUrl, SendgoClient sendgo, CancellationToken ct) =>
{
    var created = await sendgo.CreateShortUrlAsync(new ShortUrlRequest
    {
        TargetUrl = targetUrl,
    }, ct);

    return Results.Ok(created);
});

app.MapGet("/shorten/{code}/stats", (string code, SendgoClient sendgo, CancellationToken ct) =>
    sendgo.GetShortUrlStatsAsync(code, ct: ct));

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:KakaoSenderKey, and a caller ID under Sender numbers for SMS.

이 패키지로 할 수 있는 것

관련 패키지

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