Documentation menu
PHP / SDK REFERENCE
Symfony SDK Guide
The official Symfony bundle for sending Kakao Alimtalk, Brand Message and SMS — autowired service, config validation, Messenger-friendly.
이 문서의 목차
- 패키지
- sendgo/symfony
- 언어
- PHP
- 레지스트리
- Packagist
composer require sendgo/symfonyThe official Symfony bundle for sending Kakao Alimtalk, Brand Message and SMS
sendgo/symfony wraps the sendgo/php core as a Symfony bundle: it registers Sendgo\Php\Sendgo in the container, validates your configuration at compile time, and makes the client autowirable.
Requires PHP 8.2+ and Symfony 6.4 or 7.x.
Install
composer require sendgo/symfony
The core sendgo/php comes along as a dependency.
Register the bundle
With Symfony Flex the bundle registers itself. Without Flex, add it by hand:
<?php
// config/bundles.php
return [
// ...
Sendgo\Symfony\SendgoBundle::class => ['all' => true],
];
Configure
# .env
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
# config/packages/sendgo.yaml
sendgo:
access_key: '%env(SENDGO_ACCESS_KEY)%'
secret_key: '%env(SENDGO_SECRET_KEY)%'
kakao_sender_key: '%env(SENDGO_KAKAO_SENDER_KEY)%'
sms_sender_key: '%env(SENDGO_SMS_SENDER_KEY)%'
api_version: 'v2'
access_key and secret_key are declared isRequired()->cannotBeEmpty(), so a missing key fails container compilation — the deploy breaks at cache warm-up rather than at the first send.
Note that api_version defaults to v2 in the bundle (the bare PHP core defaults to v1).
Inject the client
The bundle registers the service under its FQCN, so constructor autowiring just works — no services.yaml entry needed:
<?php
// src/Controller/OrderController.php
namespace App\Controller;
use App\Entity\Order;
use Sendgo\Php\Sendgo;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
class OrderController extends AbstractController
{
public function __construct(private Sendgo $sendgo) {}
#[Route('/orders/{id}/confirm', methods: ['POST'])]
public function confirm(Order $order): JsonResponse
{
$this->sendgo->alimtalk->send([
'templateCode' => 'ORDER_CONFIRM_001',
'contacts' => [[
'contact' => $order->getUser()->getPhone(),
'var1' => $order->getNumber(),
]],
]);
return $this->json(['success' => true]);
}
}
Every channel is a property on the client:
| Property | Channel |
|---|---|
$sendgo->alimtalk |
Kakao Alimtalk |
$sendgo->friendtalk |
Kakao Friendtalk |
$sendgo->brandMessage |
Kakao Brand Message (v2 only) |
$sendgo->sms |
SMS / LMS / MMS |
A public sendgo alias is also registered, so $container->get('sendgo') works where autowiring is not available (a legacy service, a console script wired by hand).
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, which is the bundle's default.
<?php
// src/Service/CampaignService.php
namespace App\Service;
use Sendgo\Php\Sendgo;
class CampaignService
{
public function __construct(private Sendgo $sendgo) {}
// Single send — targeting M/N/I requires `contacts`
public function promote(string $phone): array
{
return $this->sendgo->brandMessage->send([
'targeting' => 'M',
'messageType' => 'FL',
'friendTemplateUuid' => '9cd5460b-6458-4edc-9b11-c26d3013c340',
'contacts' => [['contact' => $phone, 'var1' => '29,000 KRW']],
]);
}
// Broadcast — every consenting channel friend, no recipient list
public function announce(): array
{
return $this->sendgo->brandMessage->broadcast([
'messageType' => 'FW',
'friendTemplateUuid' => '9cd5460b-6458-4edc-9b11-c26d3013c340',
]);
}
// A broadcast is asynchronous upstream, so poll for progress
public function status(string $campaignId): array
{
return $this->sendgo->brandMessage->campaign($campaignId);
}
public function recent(): array
{
return $this->sendgo->brandMessage->campaigns(['count' => 10]);
}
}
broadcast() is send() with targeting forced to F, so anything you can pass to one you can pass to the other.
Asynchronous sending with Messenger
Sending is an outbound HTTP call. Route it through Messenger so a slow upstream never holds a web request open:
<?php
// src/Message/SendAlimtalk.php
namespace App\Message;
class SendAlimtalk
{
/** @param array<int, array<string, string>> $contacts */
public function __construct(
public readonly string $templateCode,
public readonly array $contacts,
) {}
}
<?php
// src/MessageHandler/SendAlimtalkHandler.php
namespace App\MessageHandler;
use App\Message\SendAlimtalk;
use Psr\Log\LoggerInterface;
use Sendgo\Php\Exception\SendgoException;
use Sendgo\Php\Sendgo;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
#[AsMessageHandler]
class SendAlimtalkHandler
{
public function __construct(
private Sendgo $sendgo,
private LoggerInterface $logger,
) {}
public function __invoke(SendAlimtalk $message): void
{
try {
$this->sendgo->alimtalk->send([
'templateCode' => $message->templateCode,
'contacts' => $message->contacts,
]);
} catch (SendgoException $e) {
if ($e->getStatusCode() < 500) {
// A 4xx will not succeed on retry — stop instead of burning the retry budget.
$this->logger->error('Sendgo {code}: {message}', [
'code' => $e->getErrorCode(),
'message' => $e->getMessage(),
]);
throw new UnrecoverableMessageHandlingException($e->getMessage(), previous: $e);
}
throw $e; // 5xx is transient — let Messenger retry
}
}
}
Throwing UnrecoverableMessageHandlingException for 4xx is the part that matters: without it, a bad template code is retried until the transport gives up and the failure looks transient.
Dispatch it from wherever the business event happens:
$bus->dispatch(new SendAlimtalk('ORDER_CONFIRM_001', [
['contact' => '01012345678', 'var1' => 'ORD-001'],
]));
Sending after a Doctrine flush
Dispatching inside a transaction means a later rollback still leaves the message delivered. Configure the Doctrine transaction middleware or dispatch from a postFlush listener rather than from the entity's lifecycle callbacks.
Error handling
<?php
use Sendgo\Php\Exception\SendgoException;
try {
$this->sendgo->alimtalk->send([...]);
} catch (SendgoException $e) {
$this->logger->error('Sendgo send failed', [
'status' => $e->getStatusCode(),
'error_code' => $e->getErrorCode(),
'endpoint' => $e->getEndpoint(),
]);
match ($e->getErrorCode()) {
'INVALID_ACCESS_KEY',
'INVALID_SECRET_KEY' => $this->alertOps('Check the Sendgo API keys'),
'IP_NOT_ALLOWED' => $this->alertOps('IP is not allow-listed'),
'PAYMENT_REQUIRED' => $this->alertOps('Out of Sendgo credit'),
'INVALID_TEMPLATE_CODE' => $this->logger->warning('Unknown template'),
default => null,
};
}
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.
Testing
The service is public, so you can replace it in the test container:
<?php
// tests/Controller/OrderControllerTest.php
namespace App\Tests\Controller;
use Sendgo\Php\AlimtalkService;
use Sendgo\Php\Sendgo;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class OrderControllerTest extends WebTestCase
{
public function testConfirmSendsAlimtalk(): void
{
$client = static::createClient();
$alimtalk = $this->createMock(AlimtalkService::class);
$alimtalk->expects($this->once())->method('send');
// The channels are `readonly` properties. Reflection can set them only
// while they are still uninitialised — which is exactly the state a
// PHPUnit double is in, because it never runs the real constructor.
// Calling setValue() on a real, constructed client throws
// "Cannot modify readonly property".
$sendgo = $this->createMock(Sendgo::class);
(new \ReflectionProperty(Sendgo::class, 'alimtalk'))->setValue($sendgo, $alimtalk);
static::getContainer()->set(Sendgo::class, $sendgo);
$client->request('POST', '/orders/1/confirm');
$this->assertResponseIsSuccessful();
}
}
For most suites it is simpler to put your own interface in front of the client — mock NotificationSenderInterface and leave Sendgo out of the test entirely. Reflection is only needed when you want to assert on the SDK call itself.
Configuration reference
| Key | Required | Default | Description |
|---|---|---|---|
access_key |
required | — | Sendgo access key |
secret_key |
required | — | Sendgo secret key |
kakao_sender_key |
optional | null |
Kakao sender profile key |
sms_sender_key |
optional | null |
SMS caller ID key |
api_version |
optional | v2 |
API version (v1 | v2) |
url |
optional | https://sendgo.io |
API base URL |
FAQ
How is this different from sendgo/php?
sendgo/php is the framework-agnostic core. sendgo/symfony adds bundle registration, a validated configuration tree, container registration and autowiring on top of it. Both send the same requests.
Does it support Symfony 6.4 and 7?
Yes — symfony/config, symfony/dependency-injection and symfony/http-kernel are all constrained to ^6.4|^7.0.
Can I have more than one client (multi-tenant)?
The bundle registers a single service. For per-tenant keys, register your own factory service that builds Sendgo\Php\Sendgo from the current tenant and inject that instead.
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.
// Accessed as a property on the autowired Sendgo\Php\Sendgo.
$short = $this->sendgo->shortUrl->create([
'targetUrl' => 'https://example.com/promotions/summer-sale',
'title' => 'Summer sale landing',
]);
$code = $short['data']['code'];
$stats = $this->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:
sendgo/symfony(Packagist) - Repository: send-go/symfony
- Registry: https://packagist.org/packages/sendgo/symfony
- 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 kakao_sender_key, and a caller ID under Sender numbers for SMS.
이 패키지로 할 수 있는 것
Send your first Kakao Alimtalk in 5 minutes →
From issuing an access key to sending a Kakao Alimtalk, with working code in Node.js, Python, PHP, Laravel, Java and Go.
Finish the Sendgo integration from your AI agent — MCP server and Account API →
Pick the organisation, issue API keys, register sender numbers and templates from a coding agent. Everything except topping up credit works without the console.
Alimtalk, Brand Message or SMS — choosing a messaging channel in Korea →
Kakao Alimtalk, Kakao Brand Message and SMS/LMS/MMS differ in what content they allow, what they cost and what you must set up first. Which to pick, by situation.
Sendgo API authentication — access keys and bearer tokens →
Exchange an accessKey and secretKey for a bearer token, and call the Sendgo API with it. Differences between v1 and v2, token caching, and the 401/403 codes.
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.
Send a Kakao Brand Message — the successor to Friendtalk →
Send Brand Messages to channel friends, non-friends, or every consenting friend at once. How targeting splits the request path, and when to keep using the Friendtalk endpoint.
SMS fallback when a Kakao Alimtalk fails →
Use replaceSms so a text message goes out when the Alimtalk cannot be delivered. Required fields, cost implications, and the mistake that silently sends nothing.
Bulk Alimtalk sending and per-recipient variables →
Send one template to many recipients with different values each, in batches. Batch sizing, partial failures, queue patterns and the data hygiene that prevents most incidents.
Scheduling an Alimtalk or SMS — scheduleType and at →
Send at a specific time with scheduleType SCHEDULED. Timestamp format, the KST timezone trap, and how scheduling interacts with the advertising night ban.
Doing everything over the API — operations automation for resellers →
Register Kakao channels, submit Alimtalk templates for review, file sender numbers, and sync opt-outs without ever sending anyone to sendgo.io. Includes webhook delivery of review outcomes.
Sendgo API error codes and retry strategy →
Every error code returned by the Sendgo send endpoints, and how to tell a failure worth retrying from one that will fail identically every time.
Short links in SMS and Alimtalk, with click tracking →
Shorten long URLs to fit inside an SMS and measure who clicked. Creating short links, reading click stats, and splitting stats per campaign.
Advertising message rules in Korea — (광고) prefix, opt-out, night ban →
What Korean law requires of promotional SMS and Kakao messages, and how to enforce it in code: the (광고) prefix, a free opt-out, and no sending between 21:00 and 08:00 KST.
Kakao Friendtalk shut down (2025-12-31) — migrating to Brand Message →
What happens to existing Friendtalk code now that the channel has ended, when you must migrate to Brand Message, and the one case where the Friendtalk endpoint is still the right call.
관련 패키지
PHP →
The pure-PHP SDK for sending Kakao Alimtalk, Brand Message and SMS from PHP — no framework required.
composer require sendgo/phpLaravel →
The official Laravel package for sending Kakao Alimtalk, Brand Message and SMS — auto-discovered service provider, facade and publishable config.
composer require sendgo/laravelWordPress →
The official WordPress plugin for sending Kakao Alimtalk, Brand Message and SMS — with automatic WooCommerce order notifications.
composer require sendgo/wordpress