Alimtalk works by **filling in an approved template**. You do not compose the body at send time; you fill the blanks in wording that already passed review.

```text
[#{var2}] Your order is confirmed.

Order number: #{var1}
Total: #{var3}
```

If that is approved as `ORDER_CONFIRM_001`, your code only supplies `var1`, `var2` and `var3`.

## Prerequisites

- An **approved template code**
- A **Kakao sender profile key** (`kakaoSenderKey`)
- An access key and secret key

## Required fields

`POST /api/v2/notices/send`

| Field | Required | Notes |
| --- | --- | --- |
| `templateCode` | ✅ | Approved template code |
| `contacts` | ✅ | Recipient array. Empty gives `EMPTY_CONTACTS` |
| `contacts[].contact` | ✅ | Phone number, **digits only** (`01012345678`) |
| `contacts[].name` | | Recipient name |
| `contacts[].var1`–`var8` | | Template variables |
| `kakaoSenderKey` | ✅ | Sender profile key (can live on the client instead) |
| `senderKey` | | SMS sending number, needed for SMS fallback |
| `scheduleType` | | `DIRECTLY` (default) or `SCHEDULED` |
| `at` | | `Y-m-d H:i:s`, KST, when `scheduleType` is `SCHEDULED` |
| `replaceSms` | | `Y` to fall back to SMS |
| `smsSubject` / `smsContent` | | Fallback body. Required when `replaceSms` is `Y` |

## Examples by language

### Node.js / TypeScript

```typescript
import Sendgo from '@sendgo/node';

const sendgo = new Sendgo({
  accessKey:      process.env.SENDGO_ACCESS_KEY!,
  secretKey:      process.env.SENDGO_SECRET_KEY!,
  kakaoSenderKey: process.env.SENDGO_KAKAO_SENDER_KEY,
  smsSenderKey:   process.env.SENDGO_SMS_SENDER_KEY,
  apiVersion:     'v2',
});

await sendgo.alimtalk.send({
  templateCode: 'ORDER_CONFIRM_001',
  contacts: [{
    contact: '01012345678',
    name:    'Hong Gildong',
    var1:    'ORD-001',
    var2:    'MacBook Pro',
    var3:    '3,490,000원',
  }],
});
```

### Python

```python
client.alimtalk.send(
    template_code="ORDER_CONFIRM_001",
    contacts=[
        {"contact": "01012345678", "name": "Hong Gildong", "var1": "ORD-001", "var2": "MacBook Pro"}
    ],
)
```

Django users want `sendgo-django`; FastAPI users want `sendgo-fastapi`.

### PHP

```php
<?php

$sendgo->alimtalk->send([
    'templateCode' => 'ORDER_CONFIRM_001',
    'contacts'     => [
        ['contact' => '01012345678', 'name' => 'Hong Gildong', 'var1' => 'ORD-001'],
    ],
]);
```

### Laravel

```php
<?php

namespace App\Services;

use Sendgo\Php\Sendgo;
use Sendgo\Php\Exception\SendgoException;
use Illuminate\Support\Facades\Log;

class OrderNotifier
{
    public function __construct(private Sendgo $sendgo) {}

    public function confirmed(Order $order): void
    {
        try {
            $this->sendgo->alimtalk->send([
                'templateCode' => 'ORDER_CONFIRM_001',
                'contacts'     => [[
                    'contact' => $order->user->phone,
                    'var1'    => $order->number,
                    'var3'    => number_format($order->total).'원',
                ]],
            ]);
        } catch (SendgoException $e) {
            // A failed notification must not roll back the order.
            Log::error('Alimtalk send failed', ['order' => $order->id, 'message' => $e->getMessage()]);
        }
    }
}
```

Queue this. An external call inside the request cycle ties your latency to Kakao's.

### Java / Spring Boot

```java
sendgo.alimtalk().send(AlimtalkRequest.builder()
    .templateCode("ORDER_CONFIRM_001")
    .contacts(List.of(
        Contact.builder().contact("01012345678").name("Hong Gildong").var1("ORD-001").build()
    ))
    .build());
```

### Go

```go
result, err := client.Alimtalk.Send(sendgo.AlimtalkRequest{
    TemplateCode: "ORDER_CONFIRM_001",
    Contacts: []sendgo.Contact{
        {Contact: "01012345678", Name: "Hong Gildong", Var1: "ORD-001"},
    },
})
if err != nil {
    log.Printf("alimtalk send failed: %v", err)
}
```

### Ruby

```ruby
client.alimtalk.send(
  template_code: 'ORDER_CONFIRM_001',
  contacts: [{ contact: '01012345678', name: 'Hong Gildong', var1: 'ORD-001' }]
)
```

### C# / .NET

```csharp
await client.SendAlimtalkAsync(new AlimtalkRequest
{
    TemplateCode = "ORDER_CONFIRM_001",
    Contacts = [new Contact { PhoneNumber = "01012345678", Name = "Hong Gildong", Var1 = "ORD-001" }],
});
```

### REST

```bash
curl -X POST https://sendgo.io/api/v2/notices/send \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "templateCode": "ORDER_CONFIRM_001",
    "scheduleType": "DIRECTLY",
    "replaceSms": "N",
    "kakaoSenderKey": "your_kakao_sender_key",
    "senderKey": "your_sms_sender_key",
    "contacts": [{ "contact": "01012345678", "var1": "ORD-001" }]
  }'
```

## Why sends fail

| Code | Cause | Retry helps |
| --- | --- | --- |
| `INVALID_TEMPLATE_CODE` | Unknown or unapproved template | ❌ |
| `INVALID_KAKAO_SENDER_KEY` | Wrong sender profile key | ❌ |
| `EMPTY_CONTACTS` | Empty recipient array | ❌ |
| `PAYMENT_REQUIRED` | Out of credit | ❌ (top up) |
| `ACCESS_KEY_NOT_APPROVED` | App not approved | ❌ |
| `IP_NOT_ALLOWED` | Outside the IP allowlist | ❌ |
| Network timeout | Transient | ✅ |

Almost every failure **fails identically on retry**. Log it and move on rather than looping. See [Error codes and retry strategy](/en/cookbook/error-handling).

## Easy to get wrong

- **No hyphens in phone numbers.** `01012345678`, not `010-1234-5678`.
- **Missing variables render literally.** If the template has `#{var4}` and you omit `var4`, the recipient sees that text.
- **Promotional content will not go out as Alimtalk.** Informational only.
- **Do not build a new client per request** — you throw away the cached token every time.

## Next

- [Send SMS, LMS and MMS](/en/cookbook/send-sms)
- [Error codes and retry strategy](/en/cookbook/error-handling)