How to Send Webhooks to CarvOS
This guide explains how to send data to CarvOS via incoming webhooks from your ATS.
Overview¶
Instead of (or in addition to) using the REST API, your ATS can push data to CarvOS via webhooks. Both methods are processed identically — choose based on your integration pattern.
Webhooks are sent to a per-ATS endpoint:
Your ats_id and webhook secret are configured via the self-service portal.
Signing Your Webhooks¶
All webhooks to CarvOS must include an HMAC-SHA256 signature in the X-Webhook-Signature header:
import hmac
import hashlib
import json
import time
def sign_webhook(payload: dict, secret: str) -> str:
"""Generate HMAC-SHA256 signature for webhook (t={timestamp},v1={signature} format)."""
body = json.dumps(payload, separators=(',', ':')).encode()
timestamp = str(int(time.time()))
signed_payload = f"{timestamp}.".encode() + body
signature = hmac.new(
secret.encode(),
signed_payload,
hashlib.sha256
).hexdigest()
return f"t={timestamp},v1={signature}"
Include the signature in the X-Webhook-Signature header.
Sending a Webhook¶
curl -X POST "https://api.carvos.io/v1/webhooks/your-ats-id" \
-H "X-Webhook-Signature: t=1234567890,v1=abc123..." \
-H "Content-Type: application/json" \
-d '{
"event": "candidate.created",
"event_id": "evt-unique-id",
"client_id": "your-client-id",
"user_id": "your-user-id",
"candidate_id": "candidate-456",
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com"
}'
All webhooks use a discriminated union on the event field — CarvOS routes the payload based on the event type.
Supported Incoming Events¶
Candidate Events¶
| Event | Trigger |
|---|---|
candidate.created |
New candidate added |
candidate.updated |
Candidate info changed |
candidate.deleted |
Candidate removed |
Vacancy Events¶
| Event | Trigger |
|---|---|
vacancy.created |
New job posting |
vacancy.updated |
Job details changed |
vacancy.deleted |
Position closed |
Application Events¶
| Event | Trigger |
|---|---|
application.created |
New application submitted |
application.updated |
Application data changed |
application.deleted |
Application removed |
Workspace Events¶
| Event | Trigger |
|---|---|
workspace.created |
New workspace/tenant created |
workspace.updated |
Workspace details changed |
workspace.deleted |
Workspace removed |
member.created |
New member added to workspace |
member.deleted |
Member removed from workspace |
See the API Reference for the full payload schema for each event type.
Error Handling¶
| Status | Meaning |
|---|---|
202 |
Accepted for processing |
401 |
Missing, malformed, expired, or invalid signature, or unknown ATS |
403 |
The ATS integration is disabled |
413 |
Request body larger than 1 MiB — applies to every CarvOS endpoint except file uploads, which have their own size limit |
422 |
The payload failed validation, or the event needs a workspace and the client_id has no workspace mapping |
A 422 for an invalid payload has a detail list with one entry per problem, each giving its location (loc), a message (msg) and an error type (type). The payload is invalid when:
- the body is empty or is not valid JSON
- the body is JSON but not an object (an array, string, number or
null) eventis missing or is not a supported event type- a required field is missing or has the wrong type, or
client_idis an empty string
Do not retry a 422 unchanged: fix the payload, or map the client to a workspace first.
The signature is checked before the payload, so a request with a bad signature is answered 401 even if its payload is also invalid. The one exception is a body that is not valid JSON, which is answered 422 before the signature is checked. A body over the size limit is answered 413 before either.
Warning
Signatures older than 5 minutes are rejected to prevent replay attacks.
Next Steps¶
-
Receive Webhooks
Handle outgoing webhooks from CarvOS
-
API Reference
Full payload schemas for each event type