SafarAPI uses two layers of authentication:
- Bearer API key (on every request) — proves your identity
- 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).
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>.
Test keys (sk_test_*) and live keys (sk_live_*) are issued separately. Test keys only access sandbox data; live keys only access production data.
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 |
Sandbox sk_test_* keys are exempt from request signing: omit X-Timestamp
and X-Signature on writes. This lets the interactive playground
run write calls end to end. Idempotency-Key is still required. Production
sk_live_* keys must sign every write — the canonical string below applies.
The canonical string is:
{X-Timestamp}\n{METHOD}\n{path-with-query}\n{body}
The body is the exact bytes you send. Reformatting (whitespace, key order in JSON) breaks the signature.
Code samples
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,
});
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
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);
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());
}
}
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.