Webhooks
Push payroll data or receive event notifications
🔔 Webhooks
Audit1 supports two webhook flows:
| Flow | Direction | Auth | Use Case |
|---|---|---|---|
| 📥 Inbound | You → Audit1 | HMAC signature | Push payroll data to Audit1 |
| 📤 Outbound | Audit1 → You | HMAC signature | Get 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
- Go to Settings > Connections > Set Up Connection > Webhooks
- The wizard generates a webhook URL and signing secret
- 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:
| Header | Value |
|---|---|
X-Webhook-Signature | HMAC-SHA256(secret, "${timestamp}.${body}") hex digest |
X-Webhook-Timestamp | Unix 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 — nosha256=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
| Status | Error | Cause |
|---|---|---|
| 401 | Missing required headers | X-Webhook-Signature or X-Webhook-Timestamp missing |
| 401 | Request timestamp expired | Timestamp > 5 minutes old |
| 401 | Invalid webhook signature | HMAC doesn't match |
| 403 | Webhook connection is inactive | Connection was disconnected |
| 404 | Webhook connection not found | Bad connection ID |
📤 Outbound — Receive Events from Audit1
Register an endpoint URL, and Audit1 sends HTTP POST requests when events occur.
Available Events
| Event | Description |
|---|---|
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 sendsX-Webhook-SignatureandX-Webhook-Timestampheaders. 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", 200Retry Policy
If your endpoint doesn't respond 200 OK, Audit1 retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | ⚡ Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 6 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 asecret(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 URLUse the ngrok URL when registering your webhook.
Need help? Email [email protected] with your Client ID prefix and the error you're seeing.
Updated 1 day ago
