Skip to content

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:

POST /v1/webhooks/{ats_id}

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)
  • event is missing or is not a supported event type
  • a required field is missing or has the wrong type, or client_id is 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