Docs Authentication

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

  1. Get the current Unix timestamp
  2. Hash the request body with SHA256 (or hash empty string "" for bodyless requests)
  3. Concatenate: timestamp + method + path + body_hash
  4. Sign the concatenated string with HMAC-SHA256 using your API secret
  5. Send the hex-encoded signature in the x-api-signature header

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:

  1. Body encoding: The body must be JSON-serialized identically when computing the hash and sending the request. Use JSON.stringify() / json.dumps() consistently.

  2. Empty body: For GET and DELETE requests, hash the empty string "", not the string "null" or "undefined".

  3. Path format: Use the full path starting with /api/v1/external/.... Do not include the domain, query string, or trailing slash.

  4. Method case: The HTTP method in the signature must be uppercase: GET, POST, PATCH, DELETE.

  5. 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:

  1. Log in to the Tiqora dashboard
  2. Go to Settings → API Keys
  3. Click Rotate on the affected key
  4. The old secret stops working immediately
  5. 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."
  }
}