For AI agents: a documentation index is available at /llms.txt. A markdown version of this page is available at /guide/api/authentication.md.
Authentication
Sign every request with HMAC-SHA256 and encrypt the payload with AES-256-CBC.
The Developer API uses an API key plus an HMAC signature. The key identifies you; the signature proves you hold the secret and that nobody altered the request in transit. The secret itself is never sent.
Store the secret carefully
The secret key is shown once, when the key pair is created. It cannot be retrieved afterwards — only rotated. Keep it in a secrets manager, never in source control or client-side code.
#Credentials
| Value | Where it goes | Notes |
|---|---|---|
| API key | X-API-KEY header | Public identifier, prefixed pk_ |
| Secret key | never transmitted | Used only to compute the signature and encrypt the payload |
#Required headers
Every request carries six headers:
Acceptstringrequiredapplication/json
Content-Typestringrequiredapplication/json
X-API-KEYstringrequiredYour API key, starting with pk_.
X-TIMESTAMPintegerrequiredUnix timestamp in seconds. Must be within ±5 minutes of server time, so keep the calling machine's clock in sync via NTP.
X-NONCEstringrequiredA one-time random string, minimum 8 characters. Each nonce is remembered for 10 minutes and cannot be reused within that window.
X-SIGNATUREstringrequiredHex-encoded HMAC-SHA256 of the canonical string, keyed with your secret.
#The canonical string
The signature is computed over a deterministic representation of the request — five fields joined by newlines, in this exact order:
METHOD\nPATH\nTIMESTAMP\nNONCE\nSHA256(BODY)
| Field | Value |
|---|---|
METHOD | Uppercase HTTP method, always POST |
PATH | Request path including the leading slash, e.g. /api/v1/partner/tasks/list |
TIMESTAMP | The same value sent in X-TIMESTAMP |
NONCE | The same value sent in X-NONCE |
SHA256(BODY) | Lowercase hex SHA-256 of the raw request body, byte for byte |
Hash the bytes you actually send
Serialise the body once, hash that exact string, and send that exact string. Re-encoding the JSON between hashing and sending — different key order, added whitespace — changes the hash and the signature will not match.
#Encrypting the payload
Request parameters are not sent as plain JSON. They are encrypted and wrapped in a single field:
{
"encrypted_data": "<base64 string>"
}
The encryption is AES-256-CBC:
- Key — the SHA-256 hash of your secret key (32 raw bytes)
- IV — 16 random bytes, generated per request
- Output — the IV prepended to the ciphertext, then Base64-encoded
Responses come back as ordinary JSON — you do not need to decrypt them.
#Worked example
<?php
$apiKey = 'pk_your_api_key';
$secretKey = 'your_secret_key';
$path = '/api/v1/partner/tasks/list';
$payload = ['project_id' => '550e8400-e29b-41d4-a716-446655440000'];
// 1. Encrypt the payload with AES-256-CBC.
$iv = random_bytes(16);
$cipher = openssl_encrypt(
json_encode($payload),
'aes-256-cbc',
hash('sha256', $secretKey, true),
OPENSSL_RAW_DATA,
$iv
);
$body = json_encode(['encrypted_data' => base64_encode($iv . $cipher)]);
// 2. Sign the canonical string.
$timestamp = time();
$nonce = bin2hex(random_bytes(8));
$canonical = implode("\n", ['POST', $path, $timestamp, $nonce, hash('sha256', $body)]);
$signature = hash_hmac('sha256', $canonical, $secretKey);
// 3. Send it.
$ch = curl_init("https://v4-api.orangescrum.com{$path}");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'Content-Type: application/json',
"X-API-KEY: {$apiKey}",
"X-TIMESTAMP: {$timestamp}",
"X-NONCE: {$nonce}",
"X-SIGNATURE: {$signature}",
],
]);
echo curl_exec($ch);
#Scopes
Each key carries a list of scopes. A key with * can reach everything; a
narrower key is rejected with 403 Forbidden when it calls outside its scope.
Ask for the narrowest set that does the job.
#Why a request fails
401 — Invalid signature
Almost always the body hash. Confirm you hashed the exact bytes you sent,
that the canonical string uses \n (not \r\n), and that the field order is
METHOD, PATH, TIMESTAMP, NONCE, SHA256(BODY).
401 — Timestamp outside window
The clock on the calling machine has drifted more than five minutes. Sync it with NTP; do not paper over it by reading the server clock first.
401 — Nonce already used
The same nonce was replayed inside the 10-minute window. Generate a fresh random value per request — never a counter that resets.
401 — Unknown or revoked key
The key does not exist, is inactive, has expired, or has been revoked. Check it in the admin portal.
Every attempt — successful or not — is written to the audit trail with the calling IP, the endpoint and the reason for failure.
🔎Validate your setupCall /api/v1/partner/validate first. It exercises the whole signing path
without touching any data.