For AI agents: a documentation index is available at /llms.txt. A markdown version of this page is available at /guide/api/authentication.md.

APIIntroduction

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

ValueWhere it goesNotes
API keyX-API-KEY headerPublic identifier, prefixed pk_
Secret keynever transmittedUsed only to compute the signature and encrypt the payload

#Required headers

Every request carries six headers:

Acceptstringrequired

application/json

Content-Typestringrequired

application/json

X-API-KEYstringrequired

Your API key, starting with pk_.

X-TIMESTAMPintegerrequired

Unix timestamp in seconds. Must be within ±5 minutes of server time, so keep the calling machine's clock in sync via NTP.

X-NONCEstringrequired

A one-time random string, minimum 8 characters. Each nonce is remembered for 10 minutes and cannot be reused within that window.

X-SIGNATUREstringrequired

Hex-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:

scss
METHOD\nPATH\nTIMESTAMP\nNONCE\nSHA256(BODY)
FieldValue
METHODUppercase HTTP method, always POST
PATHRequest path including the leading slash, e.g. /api/v1/partner/tasks/list
TIMESTAMPThe same value sent in X-TIMESTAMP
NONCEThe 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:

json
{
  "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
<?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 setup

Call /api/v1/partner/validate first. It exercises the whole signing path without touching any data.