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.AspNetCoreThe 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.
friendtalkstill works and is still the only path for free-formFT/FI/FWto 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
- Package:
Sendgo.AspNetCore(NuGet) - Repository: send-go/aspnetcore
- Registry: https://www.nuget.org/packages/Sendgo.AspNetCore
- 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:KakaoSenderKey, and a caller ID under Sender numbers for SMS.
이 패키지로 할 수 있는 것
Send a Kakao Alimtalk — PHP, Node.js, Python, Java, Go examples →
Send Kakao Alimtalk with an approved template code. Runnable code for Laravel, Node.js, Python, PHP, Java, Go, Ruby and .NET, plus every required field and why sends fail.
Send SMS, LMS and MMS in South Korea — code examples →
Send Korean SMS (90 bytes), LMS (long text) and MMS (with images) through Sendgo. Type selection, byte counting, verification-code patterns and advertising rules.