Skip to main content

Apply Webhook

Overview​

ZipRecruiter aims to make the job posting process simple from end to end. This includes automated job posting from your website or Applicant Tracking System (ATS) platform, as well as delivering new job applications back to it.

Requirements​

ZipRecruiter Apply Webhook requires the following:

  1. You must set up a Jobs API integration (optionally XML Feed Import) for automatic job posting, in at least the minimal format. This allows us to post and unpost jobs in line with your ATS, as well as provides us with your ATS's job identifiers so applicants can be delivered back to a specific job in your ATS. This feed does not need url tags; they will be ignored.

  2. You must provide an HTTPS endpoint which accepts a POST request in JSON format. This is a single URL which handles submissions for all of your jobs. You will need to let us know which content type you need when you furnish the URL for this endpoint.

Transport​

The ZipRecruiter Apply Webhook makes an HTTPS POST to the endpoint of your choosing. We do not deliver to non-HTTPS services.

This endpoint must furnish a verifiable SSL certificate signed by a well-known Certificate Authority (such as those trusted by Mozilla). If your web browser can visit the endpoint without generating a security warning, we can support it.

Fields​

All field values are alphanumeric strings unless specified otherwise.

Webhook payload fields
FieldRequiredTypeDescription
response_idYesstringA unique identifier for the Job Application being transmitted, assigned by ZipRecruiter.
zr_application_idYesstringA unique internal identifier (UUID) assigned by ZipRecruiter for the application. Use this value when reporting events via the Hiring Signals API.
job_idYesstringThe external identification for the Job as provided to ZipRecruiter with the Jobs API's job_id or XML Feed's referencenumber field. This is the Job ID your system uses.
emailYesstringThe Applicant's email address.
nameYesstringThe Applicant's full name as entered.
first_nameYesstringThe Applicant's first name as entered or parsed.
last_nameYesstringThe Applicant's last name as entered or parsed.
phoneNostringThe Applicant's phone number. Format: +N NNNNNNNNNN.
resumeNostringThe Applicant's attached resume, in PDF format. Base-64 encoded stream without whitespace.
attributesNoobjectKey/value pairs for custom behavior. A JSON object with custom string fields and string values (no nesting). See also <partner_attributes> in Feed Import.
answersNoarrayThe Applicant's interview answers, as an array of objects, if an interview was added to the job through your job feed. Advanced and optional — omitted if not in use.
profileNoobjectThe jobseeker's profile data, parsed from resume or entered from profile page. Omitted if no profile data exists. See Profile Fields below.
post_urlNostringOptional URL for dynamic candidate submission. If provided in the job feed, this URL overrides the default webhook endpoint for delivering applications for that specific job.

Profile Fields​

Profile fields reference

The profile object contains the following fields:

FieldRequiredTypeDescription
mobileNostringMobile phone number.
executive_summaryNostringProfessional summary.
objectiveNostringCareer objective.
text_resumeNostringPlain text version of the resume.
cityNostringCity.
stateNostringState.
postal_codeNostringPostal/ZIP code.
job_recordsNoarrayEmployment history records.
education_recordsNoarrayEducation history records.
achievement_recordsNoarrayAchievement records.
license_certification_recordsNoarrayLicense and certification records.
association_recordsNoarrayProfessional association records.

Format​

The Content-Type request header will be set to application/json.

Full webhook payload example
POST /job/apply HTTP/1.1
Host: yourhost.example.com
Content-Type: application/json

{
"response_id": "a39bd9a",
"zr_application_id": "2770c22a-ab43-43a1-b541-f9650727b0df",
"job_id": "1000002",
"name": "Tom Foolery",
"first_name": "Tom",
"last_name": "Foolery",
"email": "tf@example.org",
"phone": "+1 5555551942",
"resume": "JVBERi0xLjUKJb/3ov4KMiAwIG9iago8PCAvTGluZWFyaXplZCAxIC9MIDE3ODA3IC9IIFsgNjg3IDEyNiBdIC9PIDYgL0UgMTc1MzIgL04gMSAvVCA...jIxNgolJUVPRgo=",
"post_url": "https://backend.example.com/api/ziprecruiter/12345678?recruiterEmail=tf@example.org",
"attributes": {
"tracking_code": "r2d2c3po"
},
"profile": {
"executive_summary": "Business Analyst with over 5 years of experience supporting business solution software and analyzing business operations.",
"mobile": "+1 555 555-1942",
"job_records": [
{
"start_date_precision": "YearMonthDay",
"position": "Business Analyst",
"description": "Analyzing data",
"start_date": "2012-09-01",
"end_date_precision": null,
"employer": "Toms",
"current": "1",
"end_date": null
},
{
"position": "Business Analyst",
"start_date_precision": "YearMonthDay",
"start_date": "2010-09-01",
"end_date_precision": "YearMonthDay",
"description": "Still Analyzing data",
"current": "0",
"employer": "Fooleries",
"end_date": "2012-05-01"
}
],
"text_resume": "BUSINESS ANALYST SAMPLE RESUME",
"city": "Santa Monica",
"state": "CA",
"postal_code": "90401"
}
}

Response​

A 200 OK or 2xx HTTP response is considered a successful delivery. Other response codes are considered unsuccessful, and are logged and handled, and may be retried automatically or manually. If we receive a 5xx response, the system will automatically retry delivery up to six times over a two-day period before marking the attempt as failed.

warning

Your endpoint must return meaningful HTTP response codes — a response that says 200 OK but has an error message in the body is considered successful.

For successful applications, the response body can include either:

  • application_id, or
  • candidate_id and job_id

This information will be stored on our side to support future API calls with these identifiers (such as the Hiring Signals API).

Response fields
FieldRequiredTypeDescription
application_idNostringPartner's unique identifier for the job application, if any.
job_idNostringPartner's unique identifier for the job.
candidate_idNostringPartner's unique identifier for the jobseeker.
additional_dataNoobjectA free-form object containing any additional details about the application.
Example response
Example Response
{
"candidate_id": "44016-ksrm",
"job_id": "V-8632",
"additional_data": {
"summary": {
"application": {
"status": "success",
"error": null
}
}
}
}

JSON Schema​

The JSON Schema for the Apply Webhook payload can be found at: apply-webhook-schema-v2.json

Signatures​

You may optionally enable signatures for network requests from Apply Webhook.

After signatures are enabled, you must:

  1. Verify the signature.

  2. Check that the current time is within a few seconds of the timestamp in the request.

Benefits​

  • Guarantee that payloads sent from Apply Webhook originate from ZipRecruiter.

  • Guarantee that payloads sent from Apply Webhook were not changed.

  • Prevent replay attacks.

Secret Exchange​

Upon request, we will provide a secret string via a one-time access link to you, leveraging a secure delivery method. This secret will be shared within the same ZipRecruiter org. You should keep this secret safe from potentially malicious actors.

Signature​

Once signatures are enabled and secrets have been exchanged, we will send 3 signature-related headers with every Apply Webhook delivery to your endpoint.

Signature headers
HeaderDescription
X-ZipRecruiter-SignatureString of characters returned by the signature algorithm.
X-ZipRecruiter-Signature-VersionA version followed by an ignorable comment, separated by a semi-colon (e.g. v1; sha256). Versioning indicates our method for computing signatures.
X-ZipRecruiter-Signature-TimestampA date-timestamp in UTC conforming to RFC 3339 (e.g. 2021-02-12T15:49:55.214Z).
Full signature headers example
POST http://localhost/apply
Accept-Encoding: gzip
Authorization: Basic YOUR_API_KEY
User-Agent: Test-WWW-Mechanize/1.50
Content-Length: 22978
Content-Type: application/json
X-ZipRecruiter-Signature: c//+/Qo4wuJyzFENikeVOabKFXCJ5wLHzlTqkxHgZTk=
X-ZipRecruiter-Signature-Timestamp: 2021-02-12T15:49:55.214Z
X-ZipRecruiter-Signature-Version: v1; sha256

{"response_id": "1", "first_name": "John", "last_name": "Doe"}

Verifying the Signature​

  1. Take the API Webhook payload: {"response_id" : "1","first_name" : "John", "last_name" : "Doe"}

  2. Take the timestamp from X-ZipRecruiter-Signature-Timestamp: 2021-02-12T15:49:55.214Z

  3. Concatenate timestamp and payload, separated by a dot (.). The payload must be from the request body without altering it: 2021-02-12T15:49:55.214Z.{"response_id" : "1","first_name" : "John", "last_name" : "Doe"}

  4. Create a base64-encoded HMAC SHA256 hash of timestamp.payload with your secret without breaking the encoded string into lines: c//+/Qo4wuJyzFENikeVOabKFXCJ5wLHzlTqkxHgZTk=

  5. Verify that the calculated signature is identical to the signature in X-ZipRecruiter-Signature with a case-sensitive equality check.

Implementation Sample

Java
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.Mac;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidKeyException;
public class Main {
public static String encode(String key, String data) {
Mac sha256_HMAC = null;
try {
sha256_HMAC = Mac.getInstance("HmacSHA256");
}
catch(NoSuchAlgorithmException e) {
System.out.println("Something is wrong");
}
SecretKeySpec key_spec = new SecretKeySpec(key.getBytes(StandardCharsets.US_ASCII), "HmacSHA256");
try {
sha256_HMAC.init(key_spec);
}
catch(InvalidKeyException e) {
System.out.println("Something is wrong");
}
return Base64.getEncoder().encodeToString(sha256_HMAC.doFinal(data.getBytes(StandardCharsets.US_ASCII)));
}
public static void main(String[] args) {
String message = getMessage();
String secret = "YOUR_SHARED_SECRET";
String signature = encode(secret,message);
String sent_signature = "c//+/Qo4wuJyzFENikeVOabKFXCJ5wLHzlTqkxHgZTk=";
System.out.println(sent_signature.equals(signature));
}
}
Python
import hmac
import hashlib
import base64
message = getMessage()
secret = 'YOUR_SHARED_SECRET'
signature = base64.b64encode(hmac.new(bytes(secret , 'ascii'), msg = bytes(message , 'ascii'), digestmod = hashlib.sha256).digest()).decode()
sent_signature = 'c//+/Qo4wuJyzFENikeVOabKFXCJ5wLHzlTqkxHgZTk=';
print(signature == sent_signature)
Go
import (
b64 "encoding/base64"
"fmt"
"crypto/sha256"
"crypto/hmac"
)
func main() {
message := getMessage()
secret := "YOUR_SHARED_SECRET"
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(message))
signature := b64.StdEncoding.EncodeToString(mac.Sum(nil))
sent_signature := "c//+/Qo4wuJyzFENikeVOabKFXCJ5wLHzlTqkxHgZTk="
fmt.Println(signature == sent_signature)
}

Timestamp Validation​

To prevent replay attacks, check that the timestamp in X-ZipRecruiter-Signature-Timestamp is within a few seconds of the current time. Too large of a window will result in false negatives (not catching a replay attack). Too small of a window will result in false positives (rejecting valid requests).

Java
import java.util.Calendar;
import java.time.Instant;
import java.time.Duration;
public class Main {
public static void main(String[] args) {
Instant sent_time = get_time();
Instant current_time = Calendar.getInstance().toInstant();
Duration duration = Duration.ofSeconds(5);
Instant max_window = sent_time.plus( duration );
System.out.println(current_time.isBefore(max_window));
}
}
Python
from datetime import datetime, timedelta
sent_time = get_time()
current_time = datetime.now()
max_window = sent_time + timedelta(0,5)
print(current_time < max_window)
Go
import (
"fmt"
"time"
)
func main() {
sent_time := get_time()
current_time := time.Now()
max_window := sent_time.Add(time.Second * time.Duration(5))
fmt.Println(current_time.Before(max_window))
}
note

Signatures are only supported for JSON data.

Frequently Asked Questions​

Do you provide a sandbox for testing?

Unfortunately, at the moment we do not have a sandbox for testing the integration. However, we can make arrangements to provide a test import and a few test applications if needed.

Do I need a URL for every job?

No. Please provide only one POST URL for the webhook, and the software receiving that POST can associate applications with jobs by the provided job_id field.

Can I add an interview or screening questions to my jobs?

This is an advanced feature. How you add screening questions depends on how you send your jobs:

Either way, applicant answers are delivered back through this webhook. If you are interested, please contact us. If your ATS platform supports interviews but doesn't support our format, please reach out. We may be able to help on a case-by-case basis.

Do I need an authorization or API key?

No. If the endpoint you provide to us requires special HTTP headers or HTTP Basic Authentication, though, we can support that.

Can I redirect candidates to an external URL?

No. Our integrations require that candidates use the ZipRecruiter Apply process.

This webhook doesn't meet my ATS requirements, can you make adjustments?

Please contact us about this, as we may be able to support anything from minor tweaks to major custom integrations.