# Authentication

> Authenticate 23 Telecom API requests with an X-API-Key header, optional HMAC-SHA256 request signing, or a JWT token. Examples for every auth mode in 8 programming languages.
> Source: https://docs.23telecom.co.uk/authentication/

Instructions for LLMs: This is one page of the 23 Telecom messaging API docs
(SMS today; more channels planned). Base URL: https://restlink23telecom.com/api/v1,
auth via the X-API-Key header. Match errors on the error_code field, never on
description text. Full docs: https://docs.23telecom.co.uk/llms-full.txt · Schemas: https://docs.23telecom.co.uk/openapi.yaml

The API supports three authentication methods. For production integrations,
use an **API key** — optionally with HMAC request signing.

| Method | Best for | Header(s) |
| --- | --- | --- |
| [API key](#api-key-recommended) | Production integrations | `X-API-Key` |
| [API key + HMAC](#api-key-with-hmac-signature) | High-security environments | `X-API-Key`, `X-Timestamp`, `X-Signature` |
| [JWT token](#jwt-token) | Testing, portal-only endpoints | `Authorization: Bearer …` |

## API key (recommended)

Create keys in the [portal](https://restlink23telecom.com) under
**Settings → API Keys** (see [API keys & permissions](/api-keys)).
Pass the key in the `X-API-Key` header with every request:

```
X-API-Key: sk_prod_a1b2c3d4e5f6g7h8i9j0...
```

```
GET https://restlink23telecom.com/api/v1/sms/stats
Header: X-API-Key: <your key>
```
*(The web page shows this example in cURL, Node.js, Python, PHP, Ruby, Java, Go and .NET.)*

  Store keys in environment variables or a secrets manager — never commit them
  to version control. All examples in these docs read the key from the
  `API_KEY` environment variable.

## API key with HMAC signature

For enhanced security, enable signature verification when creating the key.
Every request must then carry three headers:

| Header | Description |
| --- | --- |
| `X-API-Key` | Your API key |
| `X-Timestamp` | Current Unix timestamp in seconds |
| `X-Signature` | HMAC-SHA256 signature (hex) |

**Signature computation:**

```
message   = "{METHOD}|{path}|{timestamp}|{body}"
signature = HMAC-SHA256(api_secret, message)
```

Three rules to get it right:

- `path` is the URL path **without** the query string — `/api/v1/sms/stats`,
  not `/api/v1/sms/stats?period=7d`.
- `body` is the raw request body string; use an empty string `""` for GET.
- Timestamps must be within **5 minutes** of server time, and each
  timestamp + signature pair is accepted **once** (replay protection).

  
    ```js
    import crypto from 'node:crypto';

    const apiKey = process.env.API_KEY;
    const apiSecret = process.env.API_SECRET;

    const timestamp = Math.floor(Date.now() / 1000).toString();
    const method = 'POST';
    const path = '/api/v1/sms/send';
    const body = JSON.stringify({
      to: ['+14155551234'],
      message: 'Hello!',
      sender_id: 'MyApp',
    });

    const signature = crypto
      .createHmac('sha256', apiSecret)
      .update(`${method}|${path}|${timestamp}|${body}`)
      .digest('hex');

    const res = await fetch('https://restlink23telecom.com' + path, {
      method,
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': apiKey,
        'X-Timestamp': timestamp,
        'X-Signature': signature,
      },
      body,
    });
    console.log(await res.json());
    ```
  
  
    ```py
    import hmac, hashlib, time, os, requests

    api_key    = os.environ["API_KEY"]
    api_secret = os.environ["API_SECRET"]

    timestamp = str(int(time.time()))
    method    = "POST"
    path      = "/api/v1/sms/send"
    body      = '{"to":["+14155551234"],"message":"Hello!","sender_id":"MyApp"}'

    message   = f"{method}|{path}|{timestamp}|{body}"
    signature = hmac.new(api_secret.encode(), message.encode(), hashlib.sha256).hexdigest()

    res = requests.post(
        "https://restlink23telecom.com" + path,
        headers={
            "Content-Type": "application/json",
            "X-API-Key": api_key,
            "X-Timestamp": timestamp,
            "X-Signature": signature,
        },
        data=body,
    )
    print(res.json())
    ```
  
  
    ```php
    <?php
    $apiKey    = getenv('API_KEY');
    $apiSecret = getenv('API_SECRET');

    $timestamp = (string) time();
    $method    = 'POST';
    $path      = '/api/v1/sms/send';
    $body      = json_encode(['to' => ['+14155551234'], 'message' => 'Hello!', 'sender_id' => 'MyApp']);

    $signature = hash_hmac('sha256', "$method|$path|$timestamp|$body", $apiSecret);

    $ch = curl_init("https://restlink23telecom.com$path");
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POSTFIELDS     => $body,
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/json',
            "X-API-Key: $apiKey",
            "X-Timestamp: $timestamp",
            "X-Signature: $signature",
        ],
    ]);
    echo curl_exec($ch);
    ```
  
  
    ```go
    package main

    import (
        "crypto/hmac"
        "crypto/sha256"
        "encoding/hex"
        "fmt"
        "net/http"
        "os"
        "strconv"
        "strings"
        "time"
    )

    func main() {
        apiKey, apiSecret := os.Getenv("API_KEY"), os.Getenv("API_SECRET")

        timestamp := strconv.FormatInt(time.Now().Unix(), 10)
        method, path := "POST", "/api/v1/sms/send"
        body := `{"to":["+14155551234"],"message":"Hello!","sender_id":"MyApp"}`

        mac := hmac.New(sha256.New, []byte(apiSecret))
        mac.Write([]byte(method + "|" + path + "|" + timestamp + "|" + body))
        signature := hex.EncodeToString(mac.Sum(nil))

        req, _ := http.NewRequest(method, "https://restlink23telecom.com"+path, strings.NewReader(body))
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("X-API-Key", apiKey)
        req.Header.Set("X-Timestamp", timestamp)
        req.Header.Set("X-Signature", signature)

        res, err := http.DefaultClient.Do(req)
        if err != nil {
            panic(err)
        }
        defer res.Body.Close()
        fmt.Println(res.Status)
    }
    ```
  

Signature failures return `401 SIGNATURE_REQUIRED` (headers missing) or
`401 INVALID_SIGNATURE` (verification failed) — see [errors](/reference/errors).

## JWT token

You can also authenticate by logging in with portal credentials. This is
mainly useful for testing and for the few endpoints that are JWT-only (for
example [listing workspaces](/account/workspaces)). For production
integrations use API keys.

```bash title="Log in"
curl -X POST https://restlink23telecom.com/api/v1/user/auth \
  -H "Content-Type: application/json" \
  -d '{"login": "your_username", "password": "your_password"}'
```

```json title="200 OK"
{
  "status": true,
  "token": "eyJhbGciOiJIUzI1NiIs..."
}
```

Pass the token in subsequent requests:

```
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
```

  Tokens are invalidated when you change or reset your password, or when a
  token is explicitly revoked. Your integration should handle `401
  TOKEN_EXPIRED` / `401 INVALID_TOKEN` by re-authenticating.