Authentication
Tiqora's external API uses HMAC-SHA256 authentication. This provides strong request integrity verification — each request is signed with your secret key, so even if an attacker intercepts the headers, they cannot forge valid requests.
Required Headers
Every API request must include these three headers:
| Header | Type | Description |
|---|---|---|
x-api-key |
string | Your full API key (e.g., tk_live_a1b2c3...) |
x-api-signature |
string | HMAC-SHA256 hex signature |
x-api-timestamp |
string | Current Unix timestamp in seconds |
How Signature Generation Works
The signature is computed from four components concatenated together:
message = timestamp + HTTP_METHOD + path + SHA256(request_body)
signature = HMAC-SHA256(your_api_secret, message)
Breakdown
| Component | Example | Notes |
|---|---|---|
timestamp |
1705312200 |
Unix seconds, must be within 5 minutes of server time |
HTTP_METHOD |
POST |
Uppercase: GET, POST, PATCH, DELETE |
path |
/api/v1/external/tickets |
Full path including prefix, no query string |
SHA256(body) |
e3b0c44298fc... |
SHA256 hex digest of request body. Use empty string "" for GET/DELETE |
Step by Step
- Get the current Unix timestamp
- Hash the request body with SHA256 (or hash empty string
""for bodyless requests) - Concatenate:
timestamp + method + path + body_hash - Sign the concatenated string with HMAC-SHA256 using your API secret
- Send the hex-encoded signature in the
x-api-signatureheader
Implementation Examples
JavaScript / Node.js
const crypto = require('crypto');
function signRequest(method, path, body, apiSecret) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const bodyString = body ? JSON.stringify(body) : '';
const bodyHash = crypto.createHash('sha256').update(bodyString).digest('hex');
const message = timestamp + method.toUpperCase() + path + bodyHash;
const signature = crypto
.createHmac('sha256', apiSecret)
.update(message)
.digest('hex');
return { timestamp, signature };
}
// Usage
const { timestamp, signature } = signRequest(
'POST',
'/api/v1/external/tickets',
{ subject: 'Help needed', description: 'Details...' },
'your_api_secret'
);
Python
import hashlib
import hmac
import time
def sign_request(method, path, body, api_secret):
timestamp = str(int(time.time()))
body_string = json.dumps(body) if body else ''
body_hash = hashlib.sha256(body_string.encode()).hexdigest()
message = timestamp + method.upper() + path + body_hash
signature = hmac.new(
api_secret.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
return timestamp, signature
PHP
function signRequest(string $method, string $path, ?array $body, string $apiSecret): array
{
$timestamp = (string) time();
$bodyString = $body ? json_encode($body) : '';
$bodyHash = hash('sha256', $bodyString);
$message = $timestamp . strtoupper($method) . $path . $bodyHash;
$signature = hash_hmac('sha256', $message, $apiSecret);
return ['timestamp' => $timestamp, 'signature' => $signature];
}
Ruby
require 'openssl'
require 'json'
require 'time'
def sign_request(method, path, body, api_secret)
timestamp = Time.now.to_i.to_s
body_string = body ? body.to_json : ''
body_hash = OpenSSL::Digest::SHA256.hexdigest(body_string)
message = "#{timestamp}#{method.upcase}#{path}#{body_hash}"
signature = OpenSSL::HMAC.hexdigest('sha256', api_secret, message)
{ timestamp: timestamp, signature: signature }
end
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"time"
)
func signRequest(method, path string, body interface{}, apiSecret string) (string, string) {
timestamp := fmt.Sprintf("%d", time.Now().Unix())
bodyBytes := []byte("")
if body != nil {
bodyBytes, _ = json.Marshal(body)
}
bodyHash := sha256.Sum256(bodyBytes)
bodyHashHex := hex.EncodeToString(bodyHash[:])
message := timestamp + strings.ToUpper(method) + path + bodyHashHex
mac := hmac.New(sha256.New, []byte(apiSecret))
mac.Write([]byte(message))
signature := hex.EncodeToString(mac.Sum(nil))
return timestamp, signature
}
Timestamp Validation
Requests with timestamps older than 5 minutes are rejected. This prevents replay attacks.
{
"error": {
"code": "UNAUTHORIZED",
"message": "Authentication failed."
}
}
Hint: Make sure your server's clock is synchronized with NTP. Clock drift of more than 5 minutes will cause all requests to fail.
Common Authentication Errors
| Symptom | Cause | Fix |
|---|---|---|
401 UNAUTHORIZED |
Invalid or missing headers | Verify all three headers are present |
401 UNAUTHORIZED |
Wrong API key | Check the key is correct and active |
401 UNAUTHORIZED |
Signature mismatch | Verify your signing logic matches the spec exactly |
401 UNAUTHORIZED |
Timestamp expired | Ensure clock is synced, timestamp is within 5 minutes |
401 UNAUTHORIZED |
Wrong body hash | For GET requests, hash empty string "", not null |
Debugging Signatures
If your signatures are failing, check these common issues:
Body encoding: The body must be JSON-serialized identically when computing the hash and sending the request. Use
JSON.stringify()/json.dumps()consistently.Empty body: For
GETandDELETErequests, hash the empty string"", not the string"null"or"undefined".Path format: Use the full path starting with
/api/v1/external/.... Do not include the domain, query string, or trailing slash.Method case: The HTTP method in the signature must be uppercase:
GET,POST,PATCH,DELETE.Timestamp type: The timestamp must be a string of Unix seconds (not milliseconds). JavaScript's
Date.now()returns milliseconds — divide by 1000.
Key Rotation
If your API secret is compromised:
- Log in to the Tiqora dashboard
- Go to Settings → API Keys
- Click Rotate on the affected key
- The old secret stops working immediately
- Update your application with the new secret
Warning: Rotating a key invalidates the old secret instantly. Have your new secret ready to deploy before rotating in production.
Rate Limiting
All API requests are rate-limited per API key:
| Plan | Limit |
|---|---|
| Free | 100 requests/minute |
| Professional | 500 requests/minute |
| Enterprise | 2,000 requests/minute |
Rate limit status is returned in response headers:
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 498
X-RateLimit-Reset: 1705312260
When rate limited, you'll receive a 429 response:
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Please retry after 30 seconds."
}
}