Webhooks

Push payroll data or receive event notifications

🔔 Webhooks

Audit1 supports two webhook flows:

FlowDirectionAuthUse Case
📥 InboundYou → Audit1HMAC signaturePush payroll data to Audit1
📤 OutboundAudit1 → YouHMAC signatureGet notified when events occur
📘

Both are set up in your portal: Settings > Connections > Set Up Connection > Webhooks.


📥 Inbound — Push Data to Audit1

Push payroll data to a webhook URL instead of calling the REST API. Each URL is tied to a specific connection.

Setup

  1. Go to Settings > Connections > Set Up Connection > Webhooks
  2. The wizard generates a webhook URL and signing secret
  3. Your URL: https://webhooks.audit1.com/api/v1/webhook/inbound/{connection_id}

Request

The body is the same format as POST /payroll/reports.

Required headers:

HeaderValue
X-Webhook-SignatureHMAC-SHA256(secret, "${timestamp}.${body}") hex digest
X-Webhook-TimestampUnix milliseconds (within 5 min of server time)

Signature Computation

signed_payload = "${timestamp}.${raw_request_body}"
signature = HMAC-SHA256(webhook_secret, signed_payload)
📘

The signature is a raw hex string — no sha256= prefix.

Example

TIMESTAMP=$(date +%s%N | cut -b1-13)
BODY='{"employer_fein":"12-3456789","policy_number":"WC1025561","employees":[{"first_name":"John","last_name":"Doe","class_code":"8810","state":"CA","gross_wages":5000.00,"hours_worked":80}],"pay_period":{"start_date":"2026-03-01","end_date":"2026-03-15"}}'
SECRET="your_webhook_secret"

SIGNATURE=$(echo -n "${TIMESTAMP}.${BODY}" | openssl dgst -sha256 -hmac "${SECRET}" | sed 's/^.* //')

curl -X POST "https://webhooks.audit1.com/api/v1/webhook/inbound/YOUR_CONNECTION_ID" \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: ${SIGNATURE}" \
  -H "X-Webhook-Timestamp: ${TIMESTAMP}" \
  -d "${BODY}"

Errors

StatusErrorCause
401Missing required headersX-Webhook-Signature or X-Webhook-Timestamp missing
401Request timestamp expiredTimestamp > 5 minutes old
401Invalid webhook signatureHMAC doesn't match
403Webhook connection is inactiveConnection was disconnected
404Webhook connection not foundBad connection ID

📤 Outbound — Receive Events from Audit1

Register an endpoint URL, and Audit1 sends HTTP POST requests when events occur.

Available Events

EventDescription
payroll.submission.received📥 Payroll file received and processing
audit.status.changed🔄 Audit status changed
audit.closed✅ Audit finalized
policy.updated📋 Policy information modified
employee.created👤 New employee record
class_code.changed🏷️ Employee class code updated
document.uploaded📄 Document uploaded
document.requested📩 Document requested from you
invoice.issued💰 New invoice generated
payment.posted💳 Payment posted
premium.delta📈 Premium amount changed

Payload Format

{
  "id": "evt_682abc123def456789012345",
  "type": "payroll.submission.received",
  "timestamp": "2026-03-28T10:30:00.000Z",
  "data": {
    "file_id": "682abc123def456789012345",
    "employer_id": "681xyz789abc123456789012",
    "employee_count": 150,
    "status": "completed"
  }
}
⚠️

Audit1 sends X-Webhook-Signature and X-Webhook-Timestamp headers. You must verify the signature.

Verifying Signatures

expected = HMAC-SHA256(your_webhook_secret, "${timestamp}.${raw_body}")

Node.js:

const crypto = require("crypto");

function verifyWebhook(rawBody, signature, timestamp, secret) {
  const now = Date.now();
  if (Math.abs(now - parseInt(timestamp)) > 5 * 60 * 1000) return false;

  const expected = crypto.createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`).digest("hex");

  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

// Express handler
app.post("/webhooks/audit1", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.headers["x-webhook-signature"];
  const ts = req.headers["x-webhook-timestamp"];
  if (!verifyWebhook(req.body.toString(), sig, ts, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send("Invalid signature");
  }
  res.status(200).send("OK");
  processEvent(JSON.parse(req.body));
});

Python:

import hmac, hashlib, time, os
from flask import Flask, request

@app.post("/webhooks/audit1")
def webhook_handler():
    sig = request.headers.get("X-Webhook-Signature")
    ts = request.headers.get("X-Webhook-Timestamp")
    raw = request.get_data(as_text=True)

    if abs(int(time.time() * 1000) - int(ts)) > 5 * 60 * 1000:
        return "timestamp expired", 401

    expected = hmac.new(os.environ["WEBHOOK_SECRET"].encode(),
        f"{ts}.{raw}".encode(), hashlib.sha256).hexdigest()

    if not hmac.compare_digest(sig, expected):
        return "invalid signature", 401

    return "OK", 200

Retry Policy

If your endpoint doesn't respond 200 OK, Audit1 retries with exponential backoff:

AttemptDelay
1⚡ Immediate
21 minute
35 minutes
430 minutes
52 hours
66 hours

After 6 failures, the webhook is marked as failed.

Best Practices

  • ✅ Respond 200 OK immediately, then process in the background
  • ✅ Always verify signatures — never skip
  • ✅ Use HTTPS endpoints only
  • Deduplicate using event.id
  • ❌ Don't do heavy processing before responding (30s timeout)

🔧 Managing Webhooks via API

Register

curl -X POST https://apiv2.audit1.com/api/v2/webhooks \
  -H "X-Client-ID: audit1_live_cli_..." \
  -H "X-Client-Secret: audit1_live_sec_..." \
  -H "Content-Type: application/json" \
  -d '{
    "owner_id": "681xyz789abc123456789012",
    "owner_type": "employer",
    "url": "https://your-domain.com/webhooks/audit1",
    "events": ["payroll.submission.received", "audit.status.changed"]
  }'
⚠️

Response includes a secret (shown once) — save it for signature verification.

List

curl "https://apiv2.audit1.com/api/v2/webhooks?owner_id=...&owner_type=employer" \
  -H "X-Client-ID: ..." -H "X-Client-Secret: ..."

Update

curl -X PATCH https://apiv2.audit1.com/api/v2/webhooks/{id} \
  -H "X-Client-ID: ..." -H "X-Client-Secret: ..." \
  -H "Content-Type: application/json" \
  -d '{ "events": ["payroll.submission.received", "audit.closed"] }'

Delete

curl -X DELETE "https://apiv2.audit1.com/api/v2/webhooks/{id}?owner_id=..." \
  -H "X-Client-ID: ..." -H "X-Client-Secret: ..."

🧪 Testing Locally

Use ngrok to expose your local server:

node webhook-server.js        # running on localhost:8080
npx ngrok http 8080           # creates public HTTPS URL

Use the ngrok URL when registering your webhook.


📧

Need help? Email [email protected] with your Client ID prefix and the error you're seeing.