> ## Documentation Index
> Fetch the complete documentation index at: https://developers.safarapi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Bearer API key + HMAC-signed writes

SafarAPI uses two layers of authentication:

1. **Bearer API key** (on every request) — proves your identity
2. **HMAC signature** (production writes only: POST/PUT/DELETE/PATCH) — proves the request body has not been tampered with and is not a replay. Sandbox `sk_test_*` keys are exempt (see below).

## API key format

```
sk_live_ABCDWXYZ_aN5vKjL9mQrT8Hp7Yx2Wc1Bs6Vd3Fz4Eg
   │       │        │
   │       │        └── 48-char secret (shown once)
   │       └─── 8-char prefix (stored in clear for identification)
   └─── environment marker (live or test)
```

The full bearer token is `Authorization: Bearer sk_live_<prefix>_<secret>`.

<Note>
  Test keys (`sk_test_*`) and live keys (`sk_live_*`) are issued separately. Test keys only access sandbox data; live keys only access production data.
</Note>

## Signing writes

| Header            | Value                                                   | Required          |
| ----------------- | ------------------------------------------------------- | ----------------- |
| `Idempotency-Key` | UUID v4, unique per logical operation                   | Both environments |
| `X-Timestamp`     | Unix epoch in seconds, within ±5 minutes of server time | Production only   |
| `X-Signature`     | `hex(HMAC_SHA256(secret, canonical))`                   | Production only   |

<Note>
  Sandbox `sk_test_*` keys are **exempt from request signing**: omit `X-Timestamp`
  and `X-Signature` on writes. This lets the [interactive playground](/api-reference/openapi)
  run write calls end to end. `Idempotency-Key` is still required. Production
  `sk_live_*` keys must sign every write — the canonical string below applies.
</Note>

The **canonical string** is:

```
{X-Timestamp}\n{METHOD}\n{path-with-query}\n{body}
```

<Warning>
  The body is the **exact bytes** you send. Reformatting (whitespace, key order in JSON) breaks the signature.
</Warning>

### Code samples

<CodeGroup>
  ```javascript Node.js theme={"theme":"github-dark-dimmed"}
  import crypto from 'node:crypto';

  function signRequest({ secret, method, path, body, timestamp }) {
    const canonical = `${timestamp}\n${method}\n${path}\n${body}`;
    return crypto.createHmac('sha256', secret).update(canonical).digest('hex');
  }

  const timestamp = Math.floor(Date.now() / 1000);
  const body = JSON.stringify({ quote_id: '...', /* ... */ });
  const signature = signRequest({
    secret: 'aN5vKjL9...',
    method: 'POST',
    path: '/api/partner/v1/bookings',
    body,
    timestamp,
  });

  await fetch('https://api.safarapi.com/api/partner/v1/bookings', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer sk_live_ABCDWXYZ_${secret}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': crypto.randomUUID(),
      'X-Timestamp': String(timestamp),
      'X-Signature': signature,
    },
    body,
  });
  ```

  ```python Python theme={"theme":"github-dark-dimmed"}
  import hmac
  import hashlib
  import time
  import uuid
  import json
  import requests

  def sign(secret: str, method: str, path: str, body: str, timestamp: int) -> str:
      canonical = f"{timestamp}\n{method}\n{path}\n{body}"
      return hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()

  secret = "aN5vKjL9..."
  timestamp = int(time.time())
  body = json.dumps({"quote_id": "...", "partner_reference": "YBANK-BOOK-001"})
  signature = sign(secret, "POST", "/api/partner/v1/bookings", body, timestamp)

  requests.post(
      "https://api.safarapi.com/api/partner/v1/bookings",
      data=body,
      headers={
          "Authorization": f"Bearer sk_live_ABCDWXYZ_{secret}",
          "Content-Type": "application/json",
          "Idempotency-Key": str(uuid.uuid4()),
          "X-Timestamp": str(timestamp),
          "X-Signature": signature,
      },
  )
  ```

  ```php PHP theme={"theme":"github-dark-dimmed"}
  <?php
  function safarapi_sign(string $secret, string $method, string $path, string $body, int $timestamp): string {
      $canonical = $timestamp . "\n" . $method . "\n" . $path . "\n" . $body;
      return hash_hmac('sha256', $canonical, $secret);
  }

  $secret = 'aN5vKjL9...';
  $timestamp = time();
  $body = json_encode(['quote_id' => '...', 'partner_reference' => 'YBANK-BOOK-001']);
  $signature = safarapi_sign($secret, 'POST', '/api/partner/v1/bookings', $body, $timestamp);

  $curl = curl_init('https://api.safarapi.com/api/partner/v1/bookings');
  curl_setopt_array($curl, [
      CURLOPT_POST => true,
      CURLOPT_POSTFIELDS => $body,
      CURLOPT_HTTPHEADER => [
          'Authorization: Bearer sk_live_ABCDWXYZ_' . $secret,
          'Content-Type: application/json',
          'Idempotency-Key: ' . bin2hex(random_bytes(16)),
          'X-Timestamp: ' . $timestamp,
          'X-Signature: ' . $signature,
      ],
      CURLOPT_RETURNTRANSFER => true,
  ]);
  $response = curl_exec($curl);
  ```

  ```java Java theme={"theme":"github-dark-dimmed"}
  import javax.crypto.Mac;
  import javax.crypto.spec.SecretKeySpec;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.URI;
  import java.nio.charset.StandardCharsets;
  import java.util.HexFormat;
  import java.util.UUID;

  class SafarApiSigner {
      static String sign(String secret, long timestamp, String method, String path, String body) throws Exception {
          String canonical = timestamp + "\n" + method + "\n" + path + "\n" + body;
          Mac mac = Mac.getInstance("HmacSHA256");
          mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
          return HexFormat.of().formatHex(mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8)));
      }

      public static void main(String[] args) throws Exception {
          String secret = "aN5vKjL9...";
          long ts = System.currentTimeMillis() / 1000;
          String body = "{\"quote_id\":\"...\"}";
          String signature = sign(secret, ts, "POST", "/api/partner/v1/bookings", body);

          HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.safarapi.com/api/partner/v1/bookings"))
                  .header("Authorization", "Bearer sk_live_ABCDWXYZ_" + secret)
                  .header("Content-Type", "application/json")
                  .header("Idempotency-Key", UUID.randomUUID().toString())
                  .header("X-Timestamp", String.valueOf(ts))
                  .header("X-Signature", signature)
                  .POST(HttpRequest.BodyPublishers.ofString(body))
                  .build();

          HttpClient.newHttpClient().send(request, java.net.http.HttpResponse.BodyHandlers.ofString());
      }
  }
  ```
</CodeGroup>

## Common errors

| Code                       | Reason                                                           |
| -------------------------- | ---------------------------------------------------------------- |
| `auth.api_key.missing`     | `Authorization` header absent                                    |
| `auth.api_key.malformed`   | Header doesn't follow `Bearer sk_(live\|test)_<prefix>_<secret>` |
| `auth.api_key.invalid`     | Prefix unknown or secret mismatch                                |
| `auth.timestamp.missing`   | `X-Timestamp` absent on write                                    |
| `auth.timestamp.skew`      | Timestamp more than 5 min off server time                        |
| `auth.signature.missing`   | `X-Signature` absent on write                                    |
| `auth.signature.invalid`   | HMAC computation mismatch (body altered, wrong secret, etc.)     |
| `idempotency.key.required` | `Idempotency-Key` missing on write                               |
| `idempotency.key.conflict` | Same key reused with a different body                            |

All errors return JSON with `code`, `message`, `request_id`.
