Track Applicant Webhooks

When a candidate applies to a track and is added to the Applied stage with invite_status set to Completed, Xobin sends a signed POST request to your configured webhook URL.

This event fires for all application paths, including:

  • Public apply form submissions

  • Invited candidates completing the apply form

  • Direct API applicant creation

  • Bulk CSV/Naukri uploads

  • Resume ingestion flows

Event

  • Event name: tracks.applicant.completed

  • Header: x-xobin-event: tracks.applicant.completed

Configuration

There are two configuration APIs:

API

Scope

Use it when

Track Webhook Settings

Company-wide default

You want one webhook URL for all tracks unless a track overrides it.

Track Webhook

One specific track

One track should use a different URL, secret, or enabled status.

Recommended setup:

  1. Configure the company-wide default with POST /tracks/settings/webhooks.

  2. For tracks that need custom behavior, configure an override with PUT /tracks/<track_id>/webhook.

  3. Use GET /tracks/<track_id>/webhook to confirm the effective config Xobin will use for that track.

Resolution order:

  1. Per-track override when present and inherit is false

  2. Company default otherwise

  3. No delivery when the effective config is disabled or has no URL

Example: company default

{
    "webhook": {
        "status": true,
        "url": "https://example.com/xobin/default-track-webhook",
        "secret": "company-wide-signing-secret"
    }
}

Example: per-track override

{
    "webhook": {
        "status": true,
        "url": "https://example.com/xobin/backend-engineer-webhook",
        "secret": "track-specific-signing-secret",
        "inherit": false
    }
}

Example: make a track use company default

{
    "webhook": {
        "inherit": true
    }
}

Sample payload

{
    "event": "tracks.applicant.completed",
    "event_id": "550e8400-e29b-41d4-a716-446655440000",
    "occurred_at": "2026-07-06T10:30:00+00:00",
    "company_id": 123,
    "track": {
        "id": 456,
        "title": "Backend Engineer",
        "job_details": {}
    },
    "stage": {
        "stage_id": "65a1b2c3d4e5f6789abcdef0",
        "name": "Applied",
        "type": "job",
        "color": "#4CAF50",
        "source_id": null
    },
    "applicant": {
        "application_id": 789,
        "candidate_id": 101,
        "name": "Jane Candidate",
        "email": "[email protected]",
        "invite_status": "Completed",
        "fit_score": 82,
        "fit_reason": "Strong match for Python, API design, and backend experience.",
        "parsed_resume": {
            "name": "Jane Candidate",
            "email": "[email protected]",
            "skills": ["Python", "Flask", "MySQL"],
            "fitment_score": 82,
            "fitment_reason": "Strong match for Python, API design, and backend experience."
        },
        "fields": [
            {
                "id": "field-id-1",
                "label": "Phone",
                "type": "text",
                "value": "+1 555 0100"
            },
            {
                "id": "field-id-2",
                "label": "Resume",
                "type": "media",
                "value": "https://storage.googleapis.com/signed-resume-url",
                "is_resume": true,
                "fit_score": 82,
                "fit_reason": "Strong match for Python, API design, and backend experience.",
                "parsed_resume": {
                    "name": "Jane Candidate",
                    "email": "[email protected]",
                    "skills": ["Python", "Flask", "MySQL"],
                    "fitment_score": 82,
                    "fitment_reason": "Strong match for Python, API design, and backend experience."
                }
            }
        ]
    },
    "source": "public_apply"
}

Media and resume fields

  • For fields with type media, video, snapshot, or file, Xobin returns a signed URL when the stored value is a Google Cloud Storage URL.

  • For resume fields, Xobin includes parsed_resume, fit_score, and fit_reason when those values are available for the applicant.

  • Resume enrichment is included both on the resume field and on the top-level applicant object for easier consumption.

Possible source values:

  • api_v2

  • public_apply

  • invite_completion

  • bulk_upload

  • resume_ingestion

Authenticate webhook payload

Each incoming POST request includes:

  • x-xobin-signature: HMAC-SHA256 signature of the raw JSON body

  • x-xobin-event: tracks.applicant.completed

  • x-company-id: company identifier

If you configured a custom secret in the webhook settings, use that value for verification. Otherwise use your company API key.

To validate the request:

  1. Take the raw request body bytes.

  2. Compute HMAC-SHA256 using your signing secret.

  3. Base64 URL-safe encode the digest without padding.

  4. Compare the result with x-xobin-signature.

 1 import hmac
 2 import hashlib
 3 import base64
 4
 5 def verify_webhook(data, hmac_header, secret):
 6     digest = hmac.new(
 7         secret.encode("utf-8"),
 8         data.encode("utf-8"),
 9         digestmod=hashlib.sha256,
10     ).digest()
11     computed_hmac = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
12     return hmac.compare_digest(computed_hmac, hmac_header)

Delivery semantics

  • Delivery is best-effort and does not block applicant creation.

  • Failed deliveries are logged for observability.

  • Applicant creation succeeds even if the webhook delivery fails.