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:
-
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
urltags; they will be ignored. -
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
| Field | Required | Type | Description |
|---|---|---|---|
response_id | Yes | string | A unique identifier for the Job Application being transmitted, assigned by ZipRecruiter. |
zr_application_id | Yes | string | A unique internal identifier (UUID) assigned by ZipRecruiter for the application. Use this value when reporting events via the Hiring Signals API. |
job_id | Yes | string | The 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. |
email | Yes | string | The Applicant's email address. |
name | Yes | string | The Applicant's full name as entered. |
first_name | Yes | string | The Applicant's first name as entered or parsed. |
last_name | Yes | string | The Applicant's last name as entered or parsed. |
phone | No | string | The Applicant's phone number. Format: +N NNNNNNNNNN. |
resume | No | string | The Applicant's attached resume, in PDF format. Base-64 encoded stream without whitespace. |
attributes | No | object | Key/value pairs for custom behavior. A JSON object with custom string fields and string values (no nesting). See also <partner_attributes> in Feed Import. |
answers | No | array | The 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. |
profile | No | object | The jobseeker's profile data, parsed from resume or entered from profile page. Omitted if no profile data exists. See Profile Fields below. |
post_url | No | string | Optional 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:
| Field | Required | Type | Description |
|---|---|---|---|
mobile | No | string | Mobile phone number. |
executive_summary | No | string | Professional summary. |
objective | No | string | Career objective. |
text_resume | No | string | Plain text version of the resume. |
city | No | string | City. |
state | No | string | State. |
postal_code | No | string | Postal/ZIP code. |
job_records | No | array | Employment history records. |
education_records | No | array | Education history records. |
achievement_records | No | array | Achievement records. |
license_certification_records | No | array | License and certification records. |
association_records | No | array | Professional 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.
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, orcandidate_idandjob_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
| Field | Required | Type | Description |
|---|---|---|---|
application_id | No | string | Partner's unique identifier for the job application, if any. |
job_id | No | string | Partner's unique identifier for the job. |
candidate_id | No | string | Partner's unique identifier for the jobseeker. |
additional_data | No | object | A free-form object containing any additional details about the application. |
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:
-
Verify the signature.
-
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
| Header | Description |
|---|---|
X-ZipRecruiter-Signature | String of characters returned by the signature algorithm. |
X-ZipRecruiter-Signature-Version | A 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-Timestamp | A 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
-
Take the API Webhook payload:
{"response_id" : "1","first_name" : "John", "last_name" : "Doe"} -
Take the timestamp from
X-ZipRecruiter-Signature-Timestamp:2021-02-12T15:49:55.214Z -
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"} -
Create a base64-encoded HMAC SHA256 hash of
timestamp.payloadwith your secret without breaking the encoded string into lines:c//+/Qo4wuJyzFENikeVOabKFXCJ5wLHzlTqkxHgZTk= -
Verify that the calculated signature is identical to the signature in
X-ZipRecruiter-Signaturewith 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))
}
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:
- If you post jobs through the Jobs API, attach questions with the Questions API.
- If you send jobs through an XML Job Feed, include the questions in the feed using the Screener Questions Specification.
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.