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

Kotlin
Java
C#
Python
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.