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 #

  1. Log into your portal:
  2. Employers: employer.audit1.com
  3. Payroll Companies: provider.audit1.com
  4. Go to Settings > Connections > Set Up Connection > API Keys
  5. Choose New API v2.0 > Generate API Credentials
  6. 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.com is refused at https://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