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:
- Employers: https://employer.audit1.info
- Payroll Companies: https://payrollcompany.audit1.info
- Carriers: https://carrier.audit1.info
- Software Companies: https://software.audit1.info
Table of Contents
- Understanding Dual Authentication
- Supported Entity Types
- Creating API Keys
- Using API Keys in Requests
- Request Signature (HMAC-SHA256)
- Security Best Practices
- Environment Management
- Audit Logging
- Troubleshooting
Understanding Dual Authentication
What is Dual Authentication?
Audit1 uses a two-factor API key system for enhanced security:
- Client ID: Public identifier (can be logged, shown in UI)
- Client Secret: Private secret (stored hashed, shown only once)
Both are required for authentication. This provides stronger security than single-key systems.
Key Components
| Component | Format | Visibility | Purpose |
|---|---|---|---|
| Client ID | audit1_{env}_cli_{32chars} | Public | Identifies the API key |
| Client Secret | audit1_{env}_sec_{32chars} | Private | Proves ownership |
| Label | User-defined string | Public | Organizes keys |
| Environment | sandbox or production | Public | Data 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 Type | Use Case | Example |
|---|---|---|
| Employer | Self-service payroll reporting, audit data access | Manufacturing company reporting own payroll |
| Payroll Company | Automated payroll streaming for multiple employers | ADP, Paychex reporting payroll data |
| Carrier | Audit operations, policy endorsement ingestion | Insurance carrier accessing audit reports |
| Software Company | Platform integrations, broker portals | SaaS provider building WC audit tools |
| User | Individual developer access, testing | Developer testing API integration |
Creating API Keys
Via Portal Dashboard
-
Log in to Your Portal
- Navigate to your entity-specific portal (see URLs above)
-
Go to API Keys Section
- Dashboard → Developer → API Keys
-
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"
-
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_f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3Basic 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
- Generate timestamp (Unix milliseconds)
- Build signature payload:
timestamp + method + path + body - Calculate HMAC-SHA256 using your Client Secret
- 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
| Feature | Sandbox | Production |
|---|---|---|
| Prefix | audit1_test_cli_* / audit1_test_sec_* | audit1_live_cli_* / audit1_live_sec_* |
| Data | Test data only | Real business data |
| Base URL | Same: https://api.audit1.info/api/v1 | Same: https://api.audit1.info/api/v1 |
| Isolation | Completely separate database | Production database |
| Purpose | Development, testing, integration | Live business operations |
Testing Workflow
- Develop with sandbox keys
- Test thoroughly in sandbox environment
- Request production access when ready
- Create production keys in portal
- 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_PRODUCTIONAudit 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:
- Portal Dashboard → Developer → API Usage
- View by date range, status code, or endpoint
- 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
- Create new key in portal (same label + " - v2")
- Deploy new key to staging environment
- Test thoroughly in staging
- Deploy to production (zero-downtime)
- Monitor for errors (24-48 hours)
- 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
- Documentation: https://docs.audit1.com
- API Status: https://status.audit1.com
- Support Email: [email protected]
- Developer Slack: Join Community
Reporting Security Issues
If you discover a security vulnerability:
- DO NOT open a public issue
- Email: [email protected]
- Include: affected endpoint, reproduction steps
- We'll respond within 24 hours
Additional Resources
Updated 1 day ago
