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.completedHeader:
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:
Configure the company-wide default with
POST /tracks/settings/webhooks.For tracks that need custom behavior, configure an override with
PUT /tracks/<track_id>/webhook.Use
GET /tracks/<track_id>/webhookto confirm theeffectiveconfig Xobin will use for that track.
Resolution order:
Per-track override when present and
inheritisfalseCompany default otherwise
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, orfile, Xobin returns a signed URL when the stored value is a Google Cloud Storage URL.For resume fields, Xobin includes
parsed_resume,fit_score, andfit_reasonwhen those values are available for the applicant.Resume enrichment is included both on the resume field and on the top-level
applicantobject for easier consumption.
Possible source values:
api_v2public_applyinvite_completionbulk_uploadresume_ingestion
Authenticate webhook payload¶
Each incoming POST request includes:
x-xobin-signature: HMAC-SHA256 signature of the raw JSON bodyx-xobin-event:tracks.applicant.completedx-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:
Take the raw request body bytes.
Compute
HMAC-SHA256using your signing secret.Base64 URL-safe encode the digest without padding.
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.