Application Webhook
The Application Webhook system allows you to receive real-time notifications when candidates apply to your job offers. When a candidate submits an application, InfoJobs will send an HTTP POST request to your specified webhook URL containing the candidate's complete CV data in JSON format.
Overview
When creating an offer using the createOfferV4 operation, you can specify the offerApplicationWebhookUrl field. If provided, InfoJobs will:
- Generate a unique secret key (GUID) for that specific offer
- Return this secret in the offerApplicationWebhookSecret response field
- Send an HTTP POST request to your webhook URL immediately after each candidate applies
- Sign each request using the RFC9421 standard for security
Security - RFC9421 Signature
All webhook POST requests are signed using the RFC9421 (HTTP Message Signatures) standard to ensure message integrity and authenticity.
Signature Components
Each webhook request includes three HTTP headers:
| Header | Description |
|---|---|
|
Content-Digest String |
SHA-256 hash of the request body, Base64 encoded. Format: sha-256=:<base64-hash>: |
|
Signature-Input String |
Metadata about the signature including the algorithm and creation timestamp. Format: sig=("content-digest");alg="hmac-sha256" |
|
Signature String |
HMAC-SHA256 signature of the signature base, Base64 encoded. Format: sig=:<base64-signature>: |
Secret Key
The secret used for signing is provided in the offerApplicationWebhookSecret field of the createOfferV4 response. This secret is:
- Unique per offer (each offer has its own GUID secret)
- Immutable (never changes once generated)
- Required to verify the authenticity of webhook requests
Retry Logic
If your webhook endpoint returns a non-2xx HTTP status code, InfoJobs will retry the notification:
- Maximum attempts: 5 retries
- Retry interval: Immediate (consecutive retries with no delay)
- After exhaustion: No further retries will be made
Important: Ensure your webhook endpoint returns a 2xx status code to acknowledge successful receipt.
Signature Verification
To verify the authenticity of a webhook request, you must validate the RFC9421 signature. Below are complete code examples in multiple languages:
Code Examples
import java.security.MessageDigest
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import kotlin.io.encoding.Base64
fun main() {
val signature = Rfc9421SignatureVerifier("[JOB_OFFER_KEY]")
val result = signature.verifySignature(
"[POSTED_JSON_DOBY]",
"[POSTED_CONTENT_DIGEST_HEADER]",
"[POSTED_SIGNATURE_HEADER]"
)
println(result)
}
class Rfc9421SignatureVerifier(private val webhookSecret: String) {
fun verifySignature(
body: String,
contentDigestHeader: String,
signatureHeader: String
): VerificationResult {
try {
// 1. Verify Content-Digest
val calculatedContentDigest = calculateContentDigest(body)
if (calculatedContentDigest != contentDigestHeader) {
return VerificationResult.Failure("Content-Digest mismatch")
}
// 2. Build signature base
val signatureBase = buildSignatureBase(calculatedContentDigest)
// 3. Calculate expected signature
val calculatedSignature = calculateSignature(signatureBase, webhookSecret)
// 4. Compare signatures
if (calculatedSignature != signatureHeader) {
return VerificationResult.Failure("Signature mismatch")
}
return VerificationResult.Success
} catch (e: Exception) {
return VerificationResult.Failure("Error: ${e.message}")
}
}
private fun calculateContentDigest(payload: String): String =
MessageDigest.getInstance("SHA-256")
.digest(payload.toByteArray(Charsets.UTF_8))
.let { Base64.Default.encode(it) }
.let { "sha-256=:$it:" }
private fun buildSignatureBase(contentDigest: String): String {
return """
"content-digest": $contentDigest
"@signature-params": ("content-digest");alg="hmac-sha256"
""".trimIndent()
}
private fun calculateSignature(signatureBase: String, secret: String): String {
val hMacSHA256 = Mac.getInstance("HmacSHA256").apply {
init(SecretKeySpec(secret.toByteArray(Charsets.UTF_8), "HmacSHA256"))
}
return signatureBase
.toByteArray(Charsets.UTF_8)
.let { hMacSHA256.doFinal(it) }
.let { Base64.Default.encode(it) }
.let { "sig=:$it:" }
}
sealed class VerificationResult {
object Success : VerificationResult()
data class Failure(val reason: String) : VerificationResult()
}
}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class sample {
public static void main(String args[]) {
Rfc9421SignatureVerifier signature = new Rfc9421SignatureVerifier("[JOB_OFFER_KEY]");
Rfc9421SignatureVerifier.VerificationResult result = signature.verifySignature(
"[POSTED_JSON_DOBY]}",
"[POSTED_CONTENT_DIGEST_HEADER]",
"[POSTED_SIGNATURE_HEADER]"
);
System.out.println(result);
}
public static class Rfc9421SignatureVerifier {
private final String webhookSecret;
public Rfc9421SignatureVerifier(String webhookSecret) {
this.webhookSecret = webhookSecret;
}
public VerificationResult verifySignature(String body, String contentDigestHeader,
String signatureHeader) {
try {
// 1. Verify Content-Digest
String calculatedContentDigest = calculateContentDigest(body);
if (!calculatedContentDigest.equals(contentDigestHeader)) {
return new VerificationResult.Failure("Content-Digest mismatch");
}
// 2. Build signature base
String signatureBase = buildSignatureBase(calculatedContentDigest);
// 3. Calculate expected signature
String calculatedSignature = calculateSignature(signatureBase, webhookSecret);
// 4. Compare signatures
if (!calculatedSignature.equals(signatureHeader)) {
return new VerificationResult.Failure("Signature mismatch");
}
return new VerificationResult.Success();
} catch (Exception e) {
return new VerificationResult.Failure("Error: " + e.getMessage());
}
}
private String calculateContentDigest(String payload) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(payload.getBytes(StandardCharsets.UTF_8));
String encoded = Base64.getEncoder().encodeToString(hash);
return "sha-256=:" + encoded + ":";
}
private String buildSignatureBase(String contentDigest) {
return "\"content-digest\": " + contentDigest + "\n" +
"\"@signature-params\": (\"content-digest\");alg=\"hmac-sha256\"";
}
private String calculateSignature(String signatureBase, String secret) throws Exception {
Mac hmacSha256 = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(
secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
hmacSha256.init(secretKey);
byte[] hash = hmacSha256.doFinal(signatureBase.getBytes(StandardCharsets.UTF_8));
String encoded = Base64.getEncoder().encodeToString(hash);
return "sig=:" + encoded + ":";
}
public static abstract class VerificationResult {
public static class Success extends VerificationResult {}
public static class Failure extends VerificationResult {
public final String reason;
public Failure(String reason) { this.reason = reason; }
}
}
}
}
using System;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
public class Sample
{
public static void Main(string[] args)
{
var signature = new Rfc9421SignatureVerifier("[JOB_OFFER_KEY]");
var result = signature.VerifySignature(
"[POSTED_JSON_DOBY]",
"[POSTED_CONTENT_DIGEST_HEADER]",
"[POSTED_SIGNATURE_HEADER]"
);
Console.WriteLine(result);
}
}
public class Rfc9421SignatureVerifier
{
private readonly string webhookSecret;
public Rfc9421SignatureVerifier(string webhookSecret)
{
this.webhookSecret = webhookSecret;
}
public VerificationResult VerifySignature(string body, string contentDigestHeader,
string signatureHeader)
{
try
{
// 1. Verify Content-Digest
var calculatedContentDigest = CalculateContentDigest(body);
if (calculatedContentDigest != contentDigestHeader)
{
return new VerificationResult.Failure("Content-Digest mismatch");
}
// 2. Build signature base
var signatureBase = BuildSignatureBase(calculatedContentDigest);
// 3. Calculate expected signature
var calculatedSignature = CalculateSignature(signatureBase, webhookSecret);
// 4. Compare signatures
if (calculatedSignature != signatureHeader)
{
return new VerificationResult.Failure("Signature mismatch");
}
return new VerificationResult.Success();
}
catch (Exception e)
{
return new VerificationResult.Failure($"Error: {e.Message}");
}
}
private string CalculateContentDigest(string payload)
{
using (var sha256 = SHA256.Create())
{
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(payload));
var encoded = Convert.ToBase64String(hash);
return $"sha-256=:{encoded}:";
}
}
private string BuildSignatureBase(string contentDigest)
{
return $"\"content-digest\": {contentDigest}\n" +
$"\"@signature-params\": (\"content-digest\");alg=\"hmac-sha256\"";
}
private string CalculateSignature(string signatureBase, string secret)
{
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)))
{
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signatureBase));
var encoded = Convert.ToBase64String(hash);
return $"sig=:{encoded}:";
}
}
public abstract class VerificationResult
{
public class Success : VerificationResult { }
public class Failure : VerificationResult
{
public string Reason { get; }
public Failure(string reason) { Reason = reason; }
}
}
}
import hashlib
import hmac
import base64
import re
from typing import Optional
class Rfc9421SignatureVerifier:
def __init__(self, webhook_secret: str):
self.webhook_secret = webhook_secret
def verify_signature(self, body: str, content_digest_header: str,
signature_header: str) -> 'VerificationResult':
try:
# 1. Verify Content-Digest
calculated_content_digest = self._calculate_content_digest(body)
if calculated_content_digest != content_digest_header:
return VerificationResult.Failure("Content-Digest mismatch")
# 2. Build signature base
signature_base = self._build_signature_base(calculated_content_digest)
# 3. Calculate expected signature
calculated_signature = self._calculate_signature(signature_base, self.webhook_secret)
# 5. Compare signatures
if calculated_signature != signature_header:
return VerificationResult.Failure("Signature mismatch")
return VerificationResult.Success()
except Exception as e:
return VerificationResult.Failure(f"Error: {str(e)}")
def _calculate_content_digest(self, payload: str) -> str:
hash_obj = hashlib.sha256(payload.encode('utf-8'))
encoded = base64.b64encode(hash_obj.digest()).decode('utf-8')
return f"sha-256=:{encoded}:"
def _build_signature_base(self, content_digest: str) -> str:
return (
f'"content-digest": {content_digest}\n'
f'"@signature-params": ("content-digest");alg="hmac-sha256"'
)
def _calculate_signature(self, signature_base: str, secret: str) -> str:
hmac_obj = hmac.new(
secret.encode('utf-8'),
signature_base.encode('utf-8'),
hashlib.sha256
)
encoded = base64.b64encode(hmac_obj.digest()).decode('utf-8')
return f"sig=:{encoded}:"
class VerificationResult:
class Success:
pass
class Failure:
def __init__(self, reason: str):
self.reason = reason
verifier = Rfc9421SignatureVerifier(webhook_secret='[JOB_OFFER_KEY]')
resultado = verifier.verify_signature(
body='[POSTED_JSON_DOBY]',
content_digest_header='[POSTED_CONTENT_DIGEST_HEADER]',
signature_header='[POSTED_SIGNATURE_HEADER]'
)
print(resultado)
CV Payload Data Model
The JSON payload sent in the webhook POST request contains the complete CV data of the candidate who applied. This data model is identical to the one returned by the getOfferCurriculumsV3 operation.
JSON Structure
The webhook POST body will contain a JSON object with the following main fields:
| Field | Type | Description |
|---|---|---|
cvCode |
String |
The curriculum's identifier (maximum length: 100) |
cvLastUpdate |
Date |
The date of the last CV update in RFC_3339 format including milliseconds |
cvLink |
String |
Temporal link to access the candidate CV file in PDF. Duration of 2 days |
cvtext |
String |
The original curriculum in plain text format |
cvVisibilityEndDate |
Date |
The date until the CV will be available in RFC_3339 format |
candidatoCode |
String |
The candidate's identifier (maximum length: 100) |
personaldata |
Object |
Contains candidate's personal information (see PersonalData structure) |
experiences |
Array |
Array of professional experience items (see Experience structure) |
education |
Object |
Contains regulated studies information (see Education structure) |
skills |
Object |
Contains expertise and language skills (see Skills structure) |
futurejob |
Object |
Contains availability and motivations for job change (see FutureJob structure) |
killerquestions |
Array |
Multiple choice questions with candidate's answers |
openquestions |
Array |
Open answer questions with candidate's responses |
cvlabels |
Array |
Array of string labels assigned to this CV |
candidateComments |
Array |
Array of string comments about the candidate |
Complete JSON Example
Below is a complete example of the JSON payload that will be sent to your webhook URL. For detailed field descriptions, please refer to the getOfferCurriculumsV3 documentation.
{
"application": {
"coverLetter": "To the selection team: I am writing to express my strong interest in the offered position. My backend development experience aligns with the requirements, and I am motivated to join your team. Best regards.",
"date": "2025-10-08T13:41:59.000Z",
"id": "app-987654321",
"cvVisibilityEndDate": "2026-10-08T13:41:59.000Z",
"openQuestions": [
{
"id": "21375476490",
"question": "How would you describe yourself?",
"answer": "I consider myself a decisive person, with a great capacity for teamwork and always willing to learn new technologies."
}
],
"killerQuestions": [
{
"id": "21375476478",
"question": "More than 5 years of experience?",
"answer": "Yes",
"answerId": "21375476480",
"options": [
{
"id": "21375476480",
"value": "Yes"
}
]
}
]
},
"candidateId": "c1b9a7a0-0b7c-4b1d-8f9a-6e2c1a8b0d4e",
"curriculumId": "d2e8f1b0-1c8d-4e2f-9a0b-7f3d2b9c1e5f",
"cvtext": "Professional with 5 years of experience in software development, specializing in backend solutions with Java and Spring. I am looking for a new challenge to add value and continue growing professionally in an agile environment.",
"educations": [
{
"courseCode": "ing-inf",
"courseName": "Bachelor's Degree in Computer Science",
"currentlyEnrolled": false,
"educationLevel": 110,
"finishingDate": "2019-06-15T22:00:00.000Z",
"id": 9876543210,
"institutionName": "Digital University of Madrid",
"startingDate": "2015-09-01T22:00:00.000Z"
}
],
"email": "elena.gomez.dev@infojobs.net",
"experiences": [
{
"category": "it-telecommunications",
"company": "Global Tech Solutions LLC",
"currentlyWorking": false,
"description": "Development and maintenance of REST APIs for clients in the banking sector. Optimization of database queries and participation in architecture definition.",
"id": "9876543211",
"level": "employee",
"managerLevel": null,
"salaryMax": "45.000",
"salaryMin": "40.000",
"salaryPeriod": "annual-gross",
"startingDate": "2019-07-01T22:00:00.000Z",
"finishingDate": "2024-09-30T22:00:00.000Z",
"subcategories": [
"programming"
],
"title": "Backend Software Developer",
"industry": 30,
"staffInCharge": 1,
"skills": [
{
"id": "172788204",
"name": "Spring Framework",
"level": "advanced"
}
]
}
],
"futureJob": {
"currentlyWorking": false,
"employmentStatus": 1,
"yearsOfExperience": 5,
"lastJobSearch": 2,
"preferredJob": "Backend Developer",
"contractTypes": [
"1"
],
"workDay": "1",
"availabilityToChangeHomeAddress": 40,
"availabilityToTravel": 50,
"salaryPeriod": "3",
"salaryMin": 60,
"preferredSalary": 48000
},
"lastUpdate": "2025-10-01T10:30:00.000Z",
"personalData": {
"address": "Technology Street, 15",
"birthDate": "1995-03-20T23:00:00.000Z",
"city": "Madrid",
"country": "spain",
"driverLicenses": [
2
],
"freelance": false,
"gender": 20,
"internationalPhone": "+34655112233",
"landLinePhone": "910112233",
"mobilePhone": "655112233",
"name": "Elena",
"nationalIdentityCard": "00112233B",
"nationalIdentityCardType": 1,
"nationalities": [
1
],
"photo": "https://url.to/fake/photo.jpg",
"province": "madrid",
"surname1": "Gómez",
"surname2": "Ruiz",
"vehicleOwner": false,
"zipCode": "28010",
"preferredContactPhone": "mobile-phone",
"workPermits": [
3
],
"webPages": [
{
"url": "https://www.mypersonalpage.com",
"type": "Web"
}
]
},
"skills": {
"expertise": [
{
"id": "172788503",
"level": "advanced",
"name": "Microservices"
}
],
"language": [
{
"comments": "Certified B2 Level",
"name": 2,
"reading": 122,
"speaking": 122,
"writing": 122
}
]
},
"labels": [
"Reviewed by HR"
],
"comments": [
"Candidate contacted, pending technical interview."
],
"curriculumFileLink": "https://storage.provider.com/cvs/d2e8f1b0-1c8d-4e2f.pdf"
}
Note: For complete field definitions and detailed descriptions of each object structure (PersonalData, Experience, Education, Skills, FutureJob), please refer to the getOfferCurriculumsV3 documentation which provides comprehensive information about all fields in the CV data model.
