Sendgo authentication is two steps: **exchange keys for a token, send with the token.**

Most of the time you never need this page — the SDKs handle all of it. It matters when you call REST directly or build a client for a language with no SDK.

## What you need

An `accessKey` and `secretKey` from **Integration → Apps** in the [Sendgo console](https://sendgo.io).

```bash
export SENDGO_ACCESS_KEY=your_access_key
export SENDGO_SECRET_KEY=your_secret_key
```

These carry the full sending permission for the account. Do not commit them. If you already did, revoking and reissuing in the console is the only fix — reverting the commit does not un-leak anything.

## Issue a token

Base64-encode `accessKey:secretKey` and send it as Basic auth.

```bash
curl -X POST https://sendgo.io/api/v2/token \
  -H "Authorization: Basic $(printf '%s:%s' "$SENDGO_ACCESS_KEY" "$SENDGO_SECRET_KEY" | base64)"
```

```json
{
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  }
}
```

## Call with the token

Everything after this uses bearer auth, and **v1 and v2 differ**.

| Version | Header |
| --- | --- |
| v1 | `Authorization: Bearer base64(token)` |
| v2 | `Authorization: Bearer token` |

Forgetting the second encode on v1 gives you a 401. Use **v2**.

```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",
    "kakaoSenderKey": "your_kakao_sender_key",
    "senderKey": "your_sms_sender_key",
    "contacts": [{ "contact": "01012345678", "var1": "ORD-001" }]
  }'
```

## With an SDK

Pass the keys and stop thinking about it. Issuance, caching, refresh on expiry and the 401/403 retry all happen inside.

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

const sendgo = new Sendgo({
  accessKey:  process.env.SENDGO_ACCESS_KEY!,
  secretKey:  process.env.SENDGO_SECRET_KEY!,
  apiVersion: 'v2',
});
// No token code. It fetches one on the first send.
```

```python
from sendgo import Sendgo

client = Sendgo(
    access_key=os.environ["SENDGO_ACCESS_KEY"],
    secret_key=os.environ["SENDGO_SECRET_KEY"],
    api_version="v2",
)
```

```php
<?php

$sendgo = new Sendgo\Php\Sendgo([
    'access_key'  => $_ENV['SENDGO_ACCESS_KEY'],
    'secret_key'  => $_ENV['SENDGO_SECRET_KEY'],
    'api_version' => 'v2',
]);
```

```go
client, err := sendgo.New(sendgo.Config{
    AccessKey:  os.Getenv("SENDGO_ACCESS_KEY"),
    SecretKey:  os.Getenv("SENDGO_SECRET_KEY"),
    ApiVersion: "v2",
})
```

**Build the client once and reuse it.** A new client per request throws away the cached token and fetches a new one every time. The Laravel, Spring and NestJS extensions register a singleton, so they avoid this by construction.

## Authentication errors

| Code | HTTP | Cause |
| --- | --- | --- |
| `INVALID_ACCESS_KEY` | 401 | Key does not exist, or the secret is wrong |
| `ACCESS_KEY_NOT_APPROVED` | 403 | App is still pending approval |
| `IP_NOT_ALLOWED` | 403 | Called from outside the app's IP allowlist |

`IP_NOT_ALLOWED` shows up constantly during local development. Leave the allowlist empty on a development app, or add your own IP. For production, check the **actual outbound IP** — behind a NAT gateway or load balancer it is not the instance IP.

## Next

- [Send your first Kakao Alimtalk in five minutes](/en/cookbook/quickstart)
- [Error codes and retry strategy](/en/cookbook/error-handling)