Authentication
Every API request requires a Client ID and Client Secret sent as headers.
API Key Format #
Client ID: audit1_{env}_cli_{32chars}
Client Secret: audit1_{env}_sec_{32chars}
| Environment | Client ID Prefix | Client Secret Prefix |
|---|---|---|
| Sandbox | audit1_test_cli_ |
audit1_test_sec_ |
| Production | audit1_live_cli_ |
audit1_live_sec_ |
Required Headers #
X-Client-ID: audit1_test_cli_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
X-Client-Secret: audit1_test_sec_f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3
Content-Type: application/json
Creating API Keys #
- Log into your portal:
- Employers: employer.audit1.com
- Payroll Companies: provider.audit1.com
- Go to Settings > Connections > Set Up Connection > API Keys
- Choose New API v2.0 > Generate API Credentials
- Copy both values — the Client Secret is shown only once
Info
API keys can be created for employers, payroll companies, carriers, and software companies.
Key Scope #
Every API key is created with a scope that restricts which APIs it can call:
| Scope | What it unlocks |
|---|---|
read |
Read endpoints only. Cannot start or cancel a payment. |
payroll |
Payroll reports, file status, employee sync, webhooks |
payments |
Payment Links, ACH, bank accounts, payment webhooks |
all |
Everything (used when no scope is given) |
Calls to an endpoint outside your key's scope are refused — 403 insufficient_scope on the payment APIs, 401 Unauthorized elsewhere. Issue separate keys per workload — never reuse a payments-scoped key on payroll endpoints and vice versa.
Tip
Use read for the key that gets copied. Credentials end up in scripts, log lines, screenshots and chat threads. A read key cannot start a debit and cannot cancel one, so the copy is harmless. Keep the payments key in a secrets manager and out of everything else.
An unrecognised scope is rejected at creation (400 scope_invalid) rather than stored — a typo like payment would otherwise produce a key that silently could not pay.
IP Allowlist #
An API key can be pinned to the addresses it may be used from. A copied key is then useless everywhere else, which is the single most effective thing you can do about a credential that has leaked.
By default there is no allowlist and nothing changes — a key without one works from anywhere, exactly as before. Ask Audit1 to pin a key once you know your egress addresses.
A pinned key presented from any other address gets a distinct error:
{
"error": "IP_NOT_ALLOWED",
"message": "This API key is restricted to an address allowlist and 203.0.113.7 is not on it. Nothing was read or written."
}
| Rule | Detail |
|---|---|
| Format | Exact IPv4 or IPv6 addresses. No CIDR ranges, no wildcards, no hostnames. |
| Which address | The one Audit1's platform observed. An X-Forwarded-For header you set yourself does not change the decision. |
| When it runs | After your secret is verified — so a refusal means a valid key from an unexpected place, and Audit1 alerts on it. |
| Logging | Every request records the address enforcement used, successful ones included. |
Warning
Call https://apiv2.audit1.com. Reaching the API through any other host can change the address Audit1 observes and get a pinned key refused. Tell Audit1 before your egress IP or base URL changes — otherwise payments stop.
Calling from inside Google Cloud? An IP allowlist cannot work for you. Traffic from Cloud Run, GKE or GCE to this host rides Google's private backbone and arrives with no attributable client address — measured, as the single X-Forwarded-For entry 0.0.0.0. There is nothing to pin. Use the service-account binding below instead.
Service Account Binding (Google Cloud callers) #
The same protection as the IP allowlist, for callers that have no usable address: your service account proves who it is with a Google-signed identity token.
By default there is no binding and nothing changes — a key without one works with no token, exactly as before. The token is a second factor, never a replacement: X-Client-ID and X-Client-Secret are still required and are still checked first.
Mint the token from the metadata server — no keys, nothing to rotate:
curl -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://apiv2.audit1.com"
Send it in its own header, next to the usual credentials (Authorization stays free):
X-Client-ID: audit1_live_cli_…
X-Client-Secret: audit1_live_sec_…
X-GCP-ID-Token: eyJhbGciOiJSUzI1NiIs…
The audience is per-host. A token minted for
https://apiv2.audit1.comis refused athttps://payments.audit1.com, and vice versa — that is the replay boundary doing its job. Call both services? Mint two tokens.
| status | error | what to fix |
|---|---|---|
403 |
SERVICE_ACCOUNT_TOKEN_REQUIRED |
Your key is bound and you sent no X-GCP-ID-Token. |
403 |
INVALID_SERVICE_ACCOUNT_TOKEN |
Expired, malformed, or minted for a different audience. |
403 |
SERVICE_ACCOUNT_NOT_ALLOWED |
Google signed it, but that service account is not on your key's list. |
503 |
SERVICE_ACCOUNT_VERIFIER_UNAVAILABLE |
Audit1 could not reach Google to verify. Temporary — retry. |
The two controls compose as an OR, not an AND. A key with both an IP allowlist and a service-account binding passes on either: a matching address, or a valid token from a listed account. That is what lets one credential serve a pinned office and a cloud service. Only Google service accounts (*.gserviceaccount.com) can be bound.
Environments #
Both sandbox and production use the same URL. Your key prefix determines the environment automatically.
https://apiv2.audit1.com/api/v2
This is the Developer API host
— payroll reports, employee sync, and API key / webhook management. The Payment API (payment links, ACH debits, bank accounts, payment webhooks) is a separate service on its own host, https://payments.audit1.com/api/v1 — see Payment API Overview. The two hosts don't serve each other's endpoints; calling a payment endpoint on apiv2.audit1.com (or vice versa) returns 404.
| Sandbox | Production | |
|---|---|---|
| Data | Isolated test data | Real business data |
| Processing | Faster (1-2 min) | Normal (5-10 min) |
| Billing | No charges | Standard billing |
| Switching | Just swap the API key — no code changes needed |
// Environment is determined by your key prefix — no code changes
const clientId = process.env.AUDIT1_CLIENT_ID; // test or live
const clientSecret = process.env.AUDIT1_CLIENT_SECRET;
Signed Requests (Optional HMAC) #
For extra security, sign requests with HMAC-SHA256. This prevents replay attacks and request tampering.
Additional Headers #
X-Signature: a1b2c3d4e5f6...
X-Timestamp: 1704538800000
How to Compute #
payload = "${timestamp}.${method}.${path}.${body}"
signature = HMAC-SHA256(client_secret, payload)
timestamp: Unix milliseconds (must be within 5 minutes of server time)method: HTTP method (POST,GET)path: Request path (/api/v2/payroll/reports)body: Raw request body (empty string for GET)
JavaScript #
const crypto = require("crypto");
function signRequest(clientSecret, method, path, body) {
const timestamp = Date.now().toString();
const payload = `${timestamp}.${method}.${path}.${body}`;
const signature = crypto
.createHmac("sha256", clientSecret)
.update(payload)
.digest("hex");
return { "X-Signature": signature, "X-Timestamp": timestamp };
}
Python #
import hmac, hashlib, time
def sign_request(client_secret, method, path, body):
timestamp = str(int(time.time() * 1000))
payload = f"{timestamp}.{method}.{path}.{body}"
signature = hmac.new(
client_secret.encode(), payload.encode(), hashlib.sha256
).hexdigest()
return {"X-Signature": signature, "X-Timestamp": timestamp}
️ Key Management Best Practices #
| ✓ Do | ✗ Don't |
|---|---|
| Store secrets in environment variables or a secrets manager | Hardcode secrets in source code |
| Use separate keys for sandbox and production | Reuse keys across environments |
| Rotate keys every 90 days | Leave unused keys active |
| Ask Audit1 to pin production keys to your egress IPs | Assume a leaked key is harmless because it is "just" an API key |
Use a read-scoped key for anything ad-hoc |
Use a payments key for lookups |
| Revoke immediately if compromised | Share secrets via email or Slack |
| Call from backend servers only | Expose secrets in browser-side code |
| Label keys descriptively (e.g., "Prod - Payroll Integration") | Store secrets in plain text files |
Audit Logging #
Every API request is logged with: request ID, client ID, timestamp, HTTP method/path, response status, response time, signature validation result, and the client address enforcement resolved.
View logs in your portal under Developer > API Usage.
| Retention | Duration |
|---|---|
| Sandbox | 30 days |
| Production | 1 year |