Bulk sending is **one template, many values**. Each entry in `contacts` carries its own variables, so the same wording can deliver a different order number, amount or date to each person.

## The basic shape

```typescript
await sendgo.alimtalk.send({
  templateCode: 'ORDER_CONFIRM_001',
  contacts: [
    { contact: '01011111111', name: 'Hong Gildong', var1: 'ORD-001', var2: '29,000원' },
    { contact: '01022222222', name: 'Kim Chulsoo',  var1: 'ORD-002', var2: '15,000원' },
    { contact: '01033333333', name: 'Lee Younghee', var1: 'ORD-003', var2: '52,000원' },
  ],
});
```

```python
client.alimtalk.send(
    template_code="ORDER_CONFIRM_001",
    contacts=[
        {"contact": "01011111111", "name": "Hong Gildong", "var1": "ORD-001"},
        {"contact": "01022222222", "name": "Kim Chulsoo", "var1": "ORD-002"},
    ],
)
```

```php
<?php

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

## Batching

Ten thousand recipients in one request means a timeout tells you nothing about how far it got, and retrying is expensive. Split into a few hundred at a time.

### Node.js

```typescript
const BATCH = 500;

async function sendInBatches(recipients: Recipient[]) {
  const failures: Recipient[] = [];

  for (let i = 0; i < recipients.length; i += BATCH) {
    const chunk = recipients.slice(i, i + BATCH);

    try {
      await sendgo.alimtalk.send({
        templateCode: 'ORDER_CONFIRM_001',
        contacts: chunk.map((r) => ({
          contact: r.phone,
          name: r.name,
          var1: r.orderNo,
          var2: r.amount,
        })),
      });
    } catch (error) {
      // One bad batch must not stop the rest.
      console.error(`batch ${i / BATCH} failed`, error);
      failures.push(...chunk);
    }
  }

  return failures;
}
```

### PHP · Laravel

```php
<?php

use Illuminate\Support\Collection;

collect($recipients)->chunk(500)->each(function (Collection $chunk) use ($sendgo) {
    try {
        $sendgo->alimtalk->send([
            'templateCode' => 'ORDER_CONFIRM_001',
            'contacts'     => $chunk->map(fn ($r) => [
                'contact' => $r->phone,
                'name'    => $r->name,
                'var1'    => $r->order_no,
                'var2'    => number_format($r->amount).'원',
            ])->values()->all(),
        ]);
    } catch (\Sendgo\Php\Exception\SendgoException $e) {
        Log::error('batch send failed', ['message' => $e->getMessage()]);
    }
});
```

### Python

```python
BATCH = 500

for i in range(0, len(recipients), BATCH):
    chunk = recipients[i:i + BATCH]
    try:
        client.alimtalk.send(
            template_code="ORDER_CONFIRM_001",
            contacts=[{"contact": r.phone, "name": r.name, "var1": r.order_no} for r in chunk],
        )
    except SendgoError as e:
        logger.error("batch send failed: %s", e)
```

## Do it in a queue

Never run a bulk send inside a web request. The user waits, and a timeout leaves no way to resume from the middle.

```php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Sendgo\Php\Sendgo;
use Sendgo\Php\Exception\SendgoException;

class SendOrderAlimtalk implements ShouldQueue
{
    use Queueable;

    // Most send failures fail identically on retry. Do not loop forever.
    public int $tries = 3;
    public int $backoff = 30;

    public function __construct(private array $contacts) {}

    public function handle(Sendgo $sendgo): void
    {
        $sendgo->alimtalk->send([
            'templateCode' => 'ORDER_CONFIRM_001',
            'contacts'     => $this->contacts,
        ]);
    }

    public function failed(SendgoException $e): void
    {
        // Recording the failed batch is what lets you resend just the failures.
        FailedDispatch::create(['contacts' => $this->contacts, 'reason' => $e->getMessage()]);
    }
}
```

## Partial failures

A 200 response does **not** mean every recipient received it. Numbers can be dead, recipients blocked, people not on KakaoTalk.

- Do not resend the whole batch — everyone who succeeded gets it twice.
- Resend only the failures, or enable [SMS fallback](/en/cookbook/sms-fallback) so they roll over to text automatically.
- Per-recipient outcomes are in the console's send history.

## Easy to get wrong

- **Normalise numbers first.** Real databases contain `010-1234-5678`, `+821012345678` and `01012345678` side by side. Strip hyphens and country codes down to digits.
- **Deduplicate.** A repeated number sends twice and bills twice.
- **Check the credit balance up front.** Running out midway through 10,000 messages returns `PAYMENT_REQUIRED` and the remainder fails wholesale.
- **Advertising content is subject to the night ban.** Make sure a batch cannot run past 21:00 KST → [Advertising message rules](/en/cookbook/ad-message-rules)

## Next

- [Scheduled sending](/en/cookbook/scheduled-send)
- [Error codes and retry strategy](/en/cookbook/error-handling)