API Keys Guide

Learn how to create and manage API keys for dual-factor authentication

API Keys Management Guide

📝 Quick Reference

Dual Authentication Format

🔑 Client ID:     audit1_test_cli_1234567890abcdef1234567890abcdef
🔐 Client Secret: audit1_test_sec_abcdefabcdef1234567890123456789

🔑 Client ID:     audit1_live_cli_9876543210fedcba9876543210fedcba  
🔐 Client Secret: audit1_live_sec_fedcbafedcba9876543210987654321
                            │          │
                            │          └─── 32-character unique ID
                            └────────────── Environment (test/live)

Key Lifecycle

Create → Store Securely → Use in Requests → Rotate Regularly → Revoke if Compromised

Security Checklist

  • Store client secrets in environment variables or secrets vault
  • Never commit secrets to version control
  • Use different keys for dev/staging/production
  • Rotate keys every 90 days
  • Use HMAC-SHA256 signatures for critical operations
  • Monitor key usage via audit logs
  • Revoke unused keys immediately
  • Label keys for easy identification

Overview

Audit1's Developer API uses dual-factor authentication with Client ID + Client Secret pairs for enhanced security. This guide covers everything you need to know about creating, managing, and using API keys securely.

Important: API keys are created and managed through your Audit1 portal dashboard (API Keys section).

Portal URLs by User Type:

Table of Contents

  1. Understanding Dual Authentication
  2. Supported Entity Types
  3. Creating API Keys
  4. Using API Keys in Requests
  5. Request Signature (HMAC-SHA256)
  6. Security Best Practices
  7. Environment Management
  8. Audit Logging
  9. Troubleshooting

Understanding Dual Authentication

What is Dual Authentication?

Audit1 uses a two-factor API key system for enhanced security:

  1. Client ID: Public identifier (can be logged, shown in UI)
  2. Client Secret: Private secret (stored hashed, shown only once)

Both are required for authentication. This provides stronger security than single-key systems.

Key Components

ComponentFormatVisibilityPurpose
Client IDaudit1_{env}_cli_{32chars}PublicIdentifies the API key
Client Secretaudit1_{env}_sec_{32chars}PrivateProves ownership
LabelUser-defined stringPublicOrganizes keys
Environmentsandbox or productionPublicData isolation

Security Benefits

Two-factor verification - Both credentials required
Secure storage - Secrets hashed with bcrypt
One-time display - Secrets never shown again after creation
Granular tracking - Each key independently monitored
Easy rotation - Replace keys without service disruption


Supported Entity Types

API keys can be created for the following entity types:

Entity TypeUse CaseExample
EmployerSelf-service payroll reporting, audit data accessManufacturing company reporting own payroll
Payroll CompanyAutomated payroll streaming for multiple employersADP, Paychex reporting payroll data
CarrierAudit operations, policy endorsement ingestionInsurance carrier accessing audit reports
Software CompanyPlatform integrations, broker portalsSaaS provider building WC audit tools
UserIndividual developer access, testingDeveloper testing API integration

Creating API Keys

Via Portal Dashboard

  1. Log in to Your Portal

    • Navigate to your entity-specific portal (see URLs above)
  2. Go to API Keys Section

    • Dashboard → DeveloperAPI Keys
  3. Create New Key

    • Click "Create API Key" button
    • Fill in the form:
      • Label: Descriptive name (e.g., "Production - Payroll Integration")
      • Environment: Sandbox or Production
    • Click "Generate"
  4. Save Your Credentials

    • Client ID and Client Secret are displayed
    • ⚠️ Client Secret is shown only once!
    • Store securely in password manager or secrets vault
    • Copy both values before closing

Example Creation Response

{
  "client_id": "audit1_test_cli_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  "client_secret": "audit1_test_sec_f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3",
  "label": "Production - Payroll Integration",
  "environment": "sandbox",
  "created_at": "2026-01-06T10:30:00Z",
  "message": "Store client_secret securely - it will not be shown again"
}

Using API Keys in Requests

Required Headers

All authenticated requests must include:

X-Client-ID: audit1_test_cli_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
X-Client-Secret: audit1_test_sec_f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3

Basic Request Example

curl -X POST https://api.audit1.info/api/v1/payroll/reports \
  -H "X-Client-ID: audit1_test_cli_a1b2c3d4..." \
  -H "X-Client-Secret: audit1_test_sec_f6e5d4c3..." \
  -H "Content-Type: application/json" \
  -d '{
    "employer_id": "emp_12345",
    "period_start": "2026-01-01",
    "period_end": "2026-01-31",
    "employees": [...]
  }'

Code Examples

JavaScript/Node.js:

const clientId = process.env.AUDIT1_CLIENT_ID;
const clientSecret = process.env.AUDIT1_CLIENT_SECRET;

const response = await fetch('https://api.audit1.info/api/v1/payroll/reports', {
  method: 'POST',
  headers: {
    'X-Client-ID': clientId,
    'X-Client-Secret': clientSecret,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    employer_id: 'emp_12345',
    period_start: '2026-01-01',
    period_end: '2026-01-31',
    employees: [...]
  })
});

const data = await response.json();
console.log(data);

Python:

import os
import requests

client_id = os.environ['AUDIT1_CLIENT_ID']
client_secret = os.environ['AUDIT1_CLIENT_SECRET']

headers = {
    'X-Client-ID': client_id,
    'X-Client-Secret': client_secret,
    'Content-Type': 'application/json'
}

payload = {
    'employer_id': 'emp_12345',
    'period_start': '2026-01-01',
    'period_end': '2026-01-31',
    'employees': [...]
}

response = requests.post(
    'https://api.audit1.info/api/v1/payroll/reports',
    headers=headers,
    json=payload
)

print(response.json())

PHP:

<?php
$clientId = getenv('AUDIT1_CLIENT_ID');
$clientSecret = getenv('AUDIT1_CLIENT_SECRET');

$ch = curl_init('https://api.audit1.info/api/v1/payroll/reports');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'X-Client-ID: ' . $clientId,
    'X-Client-Secret: ' . $clientSecret,
    'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'employer_id' => 'emp_12345',
    'period_start' => '2026-01-01',
    'period_end' => '2026-01-31',
    'employees' => [...]
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
?>

Request Signature (HMAC-SHA256)

Optional but Recommended

For enhanced security, sign your requests with HMAC-SHA256. This prevents:

  • Replay attacks: Timestamp validation (5-minute window)
  • Request tampering: Signature includes full request payload
  • Man-in-the-middle attacks: Cryptographic verification

How It Works

  1. Generate timestamp (Unix milliseconds)
  2. Build signature payload: timestamp + method + path + body
  3. Calculate HMAC-SHA256 using your Client Secret
  4. Include signature in request headers

Signature Headers

X-Signature: a1b2c3d4e5f6...  (HMAC-SHA256 hex digest)
X-Timestamp: 1704538800000     (Unix timestamp in milliseconds)

Implementation Examples

JavaScript:

const crypto = require('crypto');

function generateSignature(clientSecret, timestamp, method, path, body) {
  const payload = `${timestamp}.${method}.${path}.${body}`;
  const hmac = crypto.createHmac('sha256', clientSecret);
  hmac.update(payload);
  return hmac.digest('hex');
}

const timestamp = Date.now();
const method = 'POST';
const path = '/api/v1/payroll/reports';
const body = JSON.stringify({ employer_id: 'emp_12345', ... });

const signature = generateSignature(clientSecret, timestamp, method, path, body);

// Include in request
const response = await fetch('https://api.audit1.info/api/v1/payroll/reports', {
  method: 'POST',
  headers: {
    'X-Client-ID': clientId,
    'X-Client-Secret': clientSecret,
    'X-Signature': signature,
    'X-Timestamp': timestamp.toString(),
    'Content-Type': 'application/json'
  },
  body: body
});

Python:

import hmac
import hashlib
import time
import json

def generate_signature(client_secret, timestamp, method, path, body):
    payload = f"{timestamp}.{method}.{path}.{body}"
    signature = hmac.new(
        client_secret.encode('utf-8'),
        payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    return signature

timestamp = int(time.time() * 1000)
method = 'POST'
path = '/api/v1/payroll/reports'
body = json.dumps({'employer_id': 'emp_12345', ...})

signature = generate_signature(client_secret, timestamp, method, path, body)

headers = {
    'X-Client-ID': client_id,
    'X-Client-Secret': client_secret,
    'X-Signature': signature,
    'X-Timestamp': str(timestamp),
    'Content-Type': 'application/json'
}

response = requests.post(
    'https://api.audit1.info/api/v1/payroll/reports',
    headers=headers,
    data=body
)

PHP:

<?php
function generateSignature($clientSecret, $timestamp, $method, $path, $body) {
    $payload = "$timestamp.$method.$path.$body";
    return hash_hmac('sha256', $payload, $clientSecret);
}

$timestamp = round(microtime(true) * 1000);
$method = 'POST';
$path = '/api/v1/payroll/reports';
$body = json_encode(['employer_id' => 'emp_12345', ...]);

$signature = generateSignature($clientSecret, $timestamp, $method, $path, $body);

$ch = curl_init('https://api.audit1.info/api/v1/payroll/reports');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'X-Client-ID: ' . $clientId,
    'X-Client-Secret: ' . $clientSecret,
    'X-Signature: ' . $signature,
    'X-Timestamp: ' . $timestamp,
    'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
?>

Security Best Practices

Storage

DO:

  • Store secrets in environment variables
  • Use secrets management (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault)
  • Encrypt secrets at rest in databases
  • Restrict access to production keys

DON'T:

  • Hardcode secrets in source code
  • Commit secrets to version control (use .gitignore)
  • Share secrets via email or Slack
  • Store secrets in plain text files

Key Management

DO:

  • Use descriptive labels ("Production - Gusto Integration")
  • Create separate keys per environment
  • Create separate keys per integration/service
  • Rotate keys every 90 days
  • Revoke keys immediately when compromised
  • Monitor key usage for anomalies

DON'T:

  • Reuse keys across environments
  • Share keys between services
  • Leave unused keys active
  • Use overly broad permissions

Development Workflow

# .env file (never commit!)
AUDIT1_CLIENT_ID_SANDBOX=audit1_test_cli_...
AUDIT1_CLIENT_SECRET_SANDBOX=audit1_test_sec_...
AUDIT1_CLIENT_ID_PRODUCTION=audit1_live_cli_...
AUDIT1_CLIENT_SECRET_PRODUCTION=audit1_live_sec_...

# .env.example (commit this)
AUDIT1_CLIENT_ID_SANDBOX=your_sandbox_client_id_here
AUDIT1_CLIENT_SECRET_SANDBOX=your_sandbox_client_secret_here
AUDIT1_CLIENT_ID_PRODUCTION=your_production_client_id_here
AUDIT1_CLIENT_SECRET_PRODUCTION=your_production_client_secret_here

.gitignore Example

# API Keys and Secrets
.env
.env.local
.env.*.local
*.pem
*.key
secrets/
config/secrets.yml

Environment Management

Sandbox vs Production

FeatureSandboxProduction
Prefixaudit1_test_cli_* / audit1_test_sec_*audit1_live_cli_* / audit1_live_sec_*
DataTest data onlyReal business data
Base URLSame: https://api.audit1.info/api/v1Same: https://api.audit1.info/api/v1
IsolationCompletely separate databaseProduction database
PurposeDevelopment, testing, integrationLive business operations

Testing Workflow

  1. Develop with sandbox keys
  2. Test thoroughly in sandbox environment
  3. Request production access when ready
  4. Create production keys in portal
  5. Deploy with production keys

Environment Variable Setup

# Development
export AUDIT1_CLIENT_ID=$AUDIT1_CLIENT_ID_SANDBOX
export AUDIT1_CLIENT_SECRET=$AUDIT1_CLIENT_SECRET_SANDBOX

# Production
export AUDIT1_CLIENT_ID=$AUDIT1_CLIENT_ID_PRODUCTION
export AUDIT1_CLIENT_SECRET=$AUDIT1_CLIENT_SECRET_PRODUCTION

Audit Logging

What's Logged

Every API request is logged with:

  • Request ID (unique identifier)
  • Client ID used
  • Owner ID and type
  • Timestamp
  • HTTP method and path
  • IP address and user agent
  • Response status code
  • Response time
  • Signature validation result (if applicable)
  • Any errors

Viewing Audit Logs

Access your audit logs via:

  1. Portal DashboardDeveloperAPI Usage
  2. View by date range, status code, or endpoint
  3. Filter by Client ID to see specific key usage

Log Retention

  • Sandbox: 30 days
  • Production: 1 year
  • Available for export (CSV, JSON)

Troubleshooting

Authentication Errors

Error: Missing authentication headers

{
  "error": "Unauthorized",
  "message": "Missing authentication headers. Required: X-Client-ID, X-Client-Secret"
}

Solution: Include both X-Client-ID and X-Client-Secret headers in your request.

Error: Invalid client_id

{
  "error": "Unauthorized",
  "message": "Invalid client_id. Generate API keys at your portal dashboard."
}

Solution:

  • Verify Client ID is correct (copy from portal)
  • Ensure key hasn't been revoked
  • Check you're using the right environment key

Error: Invalid client_secret

{
  "error": "Unauthorized",
  "message": "Invalid client_secret"
}

Solution:

  • Client Secret is incorrect (copy carefully)
  • If lost, you cannot retrieve it - generate new key
  • Check for extra spaces or newlines

Error: Environment mismatch

{
  "error": "Unauthorized",
  "message": "Environment mismatch. This client_id is for sandbox"
}

Solution: Client ID prefix doesn't match its environment. Use matching key pair.

Signature Errors

Error: Invalid request signature

{
  "error": "Unauthorized",
  "message": "Invalid request signature",
  "details": "Signature mismatch"
}

Solution:

  • Verify signature generation logic
  • Ensure timestamp is current (within 5 minutes)
  • Check payload format: timestamp.method.path.body
  • Verify HMAC-SHA256 implementation

Error: Request timestamp expired

{
  "error": "Unauthorized",
  "message": "Invalid request signature",
  "details": "Request timestamp expired (>5 minutes old)"
}

Solution:

  • Generate new timestamp for each request
  • Check system clock is synchronized (NTP)
  • Don't reuse signatures

Key Rotation

When to Rotate

  • Scheduled: Every 90 days (recommended)
  • Compromise: Immediately if suspected breach
  • Personnel changes: When team members leave
  • Service changes: Major infrastructure updates

Rotation Process

  1. Create new key in portal (same label + " - v2")
  2. Deploy new key to staging environment
  3. Test thoroughly in staging
  4. Deploy to production (zero-downtime)
  5. Monitor for errors (24-48 hours)
  6. Revoke old key after grace period

Zero-Downtime Rotation

// Support both old and new keys temporarily
const clientId = process.env.AUDIT1_CLIENT_ID_NEW || process.env.AUDIT1_CLIENT_ID;
const clientSecret = process.env.AUDIT1_CLIENT_SECRET_NEW || process.env.AUDIT1_CLIENT_SECRET;

Support

Getting Help

Reporting Security Issues

If you discover a security vulnerability:

  1. DO NOT open a public issue
  2. Email: [email protected]
  3. Include: affected endpoint, reproduction steps
  4. We'll respond within 24 hours

Additional Resources