Create, Configure a Webhook

Webhook Connectors Tutorial: from Webhook creation to start sending Events.

Webhooks (also called a web callback or HTTP push API) are the best way to notify third-party tools with real-time information from HrFlow.ai. The HrFlow.ai Webhooks define a callback URL where Events are delivered as they happen in HrFlow.ai.

πŸ“˜

Prerequisites

Step 1: Go to the Connectors Marketplace

1600

Left Sidebar > Connections > Connectors Marketplace > Destinations

Step 2: Choose Webhook

1600

Left Sidebar > Connections > Connectors Marketplace > Destinations > Webhook

After opening the modal, click on the button Β«InstallΒ».

Step 3: Choose an Event Type

πŸ“˜

Webhook Format

The webhook posts data to the URL you provided in the configuration. The body is encoded in JSON, which is indicated by the application/json content-type, with UTF-8 encoding (as stated in RFC 4627 (http://www.ietf.org/rfc/rfc4627.txt).

1600

Left Sidebar > Connections > Connectors Marketplace > Destinations > Webhook > Settings

1. Profile's Events

1.1 Profile's Event Types

Event TypeDescriptionVolume
profile.parsing.successSent when a Profile is parsed successfully for the 1st time.High volume
profile.parsing.updateSent when an existing Profile is parsed again successfully.Medium volume
profile.parsing.errorSent when parsing a Profile has failed and should be retried. The Profile Parsing cannot be retrieved.Low volume
profile.storing.successSent when a Profile is saved successfully for the 1st time.High volume
profile.storing.updateSent when an existing Profile is updated successfully.Medium volume
profile.storing.errorSent when saving a Profile has failed and should be retried. The Profile cannot be retrieved.Low volume
profile.searching.successSent when a Profile is saved successfully in the Searching API Index for the 1st time and is ready to be queried.High volume
profile.searching.updateSent when an existing Profile is updated successfully in the Searching API Index and is ready to be queried.Medium volume
profile.searching.errorSent when saving a Profile in the Searching API Index has failed and should be retried. The Profile cannot be queried.Low volume
profile.scoring.successSent when a Profile is saved successfully in the Scoring API Index for the 1st time and is ready to be scored.High volume
profile.scoring.updateSent when an existing Profile is updated successfully in the Scoring API Index and is ready to be scored.Medium volume
profile.scoring.errorSent when saving a Profile in the Scoring API Index has failed and should be retried. The Profile cannot be scored.Low volume

1.2 Profile's Event Payloads

{status} = success, update or error

# Headers
content-type=application/x-www-form-urlencoded

# Form values
type:profile.parsing.success
origin=api
message=profile parsing succeed
profile={"key": "d821393853fc32b08c93b8d38590817c72048ec4", "source": {"key": "d900ec70c67d43c71027f9bc63ec3b5b3e16c1d8"}}

# Raw content
type=profile.parsing.success&origin=api&message=profile+parsing+succeed&profile=%7B%22key%22%3A+%22d821393853fc32b08c93b8d38590817c72048ec4%22%2C+%22source%22%3A+%7B%22key%22%3A+%22d900ec70c67d43c71027f9bc63ec3b5b3e16c1d8%22%7D%7D
# Headers
content-type=application/x-www-form-urlencoded

# Form values
type:profile.storing.success
origin=api
message=profile storing succeed
profile={"key": "d821393853fc32b08c93b8d38590817c72048ec4", "source": {"key": "d900ec70c67d43c71027f9bc63ec3b5b3e16c1d8"}}

# Raw content
type=profile.storing.success&origin=api&message=profile+storing+succeed&profile=%7B%22key%22%3A+%22d821393853fc32b08c93b8d38590817c72048ec4%22%2C+%22source%22%3A+%7B%22key%22%3A+%22d900ec70c67d43c71027f9bc63ec3b5b3e16c1d8%22%7D%7D
# Headers
content-type=application/x-www-form-urlencoded

# Form values
type:profile.searching.success
origin=api
message=profile searching succeed
profile={"key": "d821393853fc32b08c93b8d38590817c72048ec4", "source": {"key": "d900ec70c67d43c71027f9bc63ec3b5b3e16c1d8"}}

# Raw content
type=profile.searching.success&origin=api&message=profile+searching+succeed&profile=%7B%22key%22%3A+%22d821393853fc32b08c93b8d38590817c72048ec4%22%2C+%22source%22%3A+%7B%22key%22%3A+%22d900ec70c67d43c71027f9bc63ec3b5b3e16c1d8%22%7D%7D
# Headers
content-type=application/x-www-form-urlencoded

# Form values
type:profile.scoring.success
origin=api
message=profile scoring succeed
profile={"key": "d821393853fc32b08c93b8d38590817c72048ec4", "source": {"key": "d900ec70c67d43c71027f9bc63ec3b5b3e16c1d8"}}

# Raw content
type=profile.scoring.success&origin=api&message=profile+scoring+succeed&profile=%7B%22key%22%3A+%22d821393853fc32b08c93b8d38590817c72048ec4%22%2C+%22source%22%3A+%7B%22key%22%3A+%22d900ec70c67d43c71027f9bc63ec3b5b3e16c1d8%22%7D%7D

2.1 Job's Event Types

Event TypeDescriptionVolume
job.storing.successSent when a Job is saved successfully for the 1st time.High volume
job.storing.updateSent when an existing Job is updated successfully.High volume
job.storing.errorSent when saving a Job has failed and should be retried. The Job cannot be retrieved.Low volume
job.searching.successSent when a Job is saved successfully in the Searching API Index for the 1st time and is ready to be queried.High volume
job.searching.updateSent when an existing Job is updated successfully in the Searching API Index and is ready to be queried.High volume
job.searching.errorSent when saving a Job in the Searching API Index has failed and should be retried. The Job cannot be queried.Low volume
job.scoring.successSent when a Job is saved successfully in the Scoring API Index for the 1st time and is ready to be scored.High volume
job.scoring.updateSent when an existing Job is updated successfully in the Scoring API Index and is ready to be scored.High volume
job.scoring.errorSent when saving a Job in the Scoring API Index has failed and should be retried. The Job cannot be scored.Low volume

2.2 Job's Event Payloads

{status} = success, update or error

# Headers
content-type=application/x-www-form-urlencoded

# Form values
type:job.parsing.success
origin=api
message=job parsing succeed
job={"key": "d821393853fc32b08c93b8d38590817c72048ec4", "board": {"key": "d900ec70c67d43c71027f9bc63ec3b5b3e16c1d8"}}

# Raw content
type=job.parsing.success&origin=api&message=job+parsing+succeed&job=%7B%22key%22%3A+%22d821393853fc32b08c93b8d38590817c72048ec4%22%2C+%22source%22%3A+%7B%22key%22%3A+%22d900ec70c67d43c71027f9bc63ec3b5b3e16c1d8%22%7D%7D
# Headers
content-type=application/x-www-form-urlencoded

# Form values
type:job.storing.success
origin=api
message=job storing succeed
job={"key": "d821393853fc32b08c93b8d38590817c72048ec4", "board": {"key": "d900ec70c67d43c71027f9bc63ec3b5b3e16c1d8"}}

# Raw content
type=job.storing.success&origin=api&message=job+storing+succeed&job=%7B%22key%22%3A+%22d821393853fc32b08c93b8d38590817c72048ec4%22%2C+%22source%22%3A+%7B%22key%22%3A+%22d900ec70c67d43c71027f9bc63ec3b5b3e16c1d8%22%7D%7D
# Headers
content-type=application/x-www-form-urlencoded

# Form values
type:job.searching.success
origin=api
message=job searching succeed
job={"key": "d821393853fc32b08c93b8d38590817c72048ec4", "board": {"key": "d900ec70c67d43c71027f9bc63ec3b5b3e16c1d8"}}

# Raw content
type=job.searching.success&origin=api&message=job+searching+succeed&job=%7B%22key%22%3A+%22d821393853fc32b08c93b8d38590817c72048ec4%22%2C+%22source%22%3A+%7B%22key%22%3A+%22d900ec70c67d43c71027f9bc63ec3b5b3e16c1d8%22%7D%7D
# Headers
content-type=application/x-www-form-urlencoded

# Form values
type:job.scoring.success
origin=api
message=job scoring succeed
job={"key": "d821393853fc32b08c93b8d38590817c72048ec4", "board": {"key": "d900ec70c67d43c71027f9bc63ec3b5b3e16c1d8"}}

# Raw content
type=job.scoring.success&origin=api&message=job+scoring+succeed&job=%7B%22key%22%3A+%22d821393853fc32b08c93b8d38590817c72048ec4%22%2C+%22source%22%3A+%7B%22key%22%3A+%22d900ec70c67d43c71027f9bc63ec3b5b3e16c1d8%22%7D%7D

succeed

Step 3: Specify an Callback URL

For this tutorial, let's create a fake Webhook URL on an online website (such as RequestCatcher or Webhook dot site) and then send an Event of the type profile.parsing.success.

1600

Go to the website RequestCatcher dot com and create a fake Webhook URL

The Webhook URL is ready to receive requests.

1600

Webhook URL

Now let us:

  • Pick the Event of the type profile.parsing.success from the Dropdown (don't hesitate to try others)
  • Choose a recognizable Webhook name that you can easily remember, and succeedother users can easily recognize.
  • Copy the Webhook URL in the Settings and click on the button Β«SaveΒ».

You can also simulate the webhook request by clicking on the button Β«CheckΒ» before saving.
You can click on the button Β«Add more webhooksΒ» to send more Events.

1600

Left Sidebar > Connections > Connectors Marketplace > Destinations > Webhook > Settings

The RequestCatcher website shows you that HrFlow.ai has saved your event has successfully.
From now, Every time you save a profile in HrFlow.ai, a request will be sent to this Webhook URL.
Don't forget to delete unused Webhook Events to let your workspace clean.

1600

Webhook URL receiving your 1st event

Advanced Topics:

1. Securing Webhooks with Signature

When a Webhook Event is sent, an HTTP POST request is made to your specified Webhook URL. This POST request will contain some parameters, including the HTTP-HRFLOW-SIGNATURE header parameter, which you can use for authorization.

The HTTP-HRFLOW-SIGNATURE is base64url encoded and signed with an HMAC version of your WEBHOOK SECRET KEY with the SHA-256 digest.

πŸ“˜

HMAC algorithm with SHA-256 digest

See examples in different languages https://www.jokecamp.com/blog/examples-of-creating-base64-hashes-using-hmac-sha256-in-different-languages .

What this means is that when it is POSTed to your WEBHOOK SECRET KEY, you will need to parse and verify it before it can be used. This is performed in three steps:

  • Split the signed request into two parts delineated by a '.' character (eg. 238fsdfsd.oijdoifjsidf899)
  • Decode the first part - the encoded signature - from base64url
  • Decode the second part - the payload - from base64url and then decode the resultant JSON object

These steps are possible in any modern programming language.

Examples:

<?php

function parse_signed_request($signed_request, $secret) {
  list($encoded_sig, $payload) = explode('.', $signed_request, 2); 

  // decode the data
  $sig = base64_url_decode($encoded_sig);
  $data = json_decode(base64_url_decode($payload), true);

  // confirm the signature
  $expected_sig = hash_hmac('sha256', $payload, $secret, $raw = true);
  if ($sig !== $expected_sig) {
    error_log('Bad Signed JSON signature!');
    return null;
  }

  return $data;
}

function base64_url_decode($input) {
  return base64_decode(strtr($input, '-_', '+/'));
}
import hmac
import hashlib
import json
import urllib

def check_signature(secret, signature, body_str):
    body_dict = urllib.parse.parse_qs(body_str) # = {'team_name': ['yourteam'], 'type': ['profile.storing.success']}
    body_dict = {key:value[0] for key, value in body_dict.items()} # = {'team_name': 'yourteam', 'type': 'profile.storing.success'}
    
    # WARNING: the separators parameter is important to ensure there are no spaces in the encoded string
    # encoded_body = '{"team_name":"yourteam","type":"profile.storing.success"}' 
    encoded_body = json.dumps(body_dict, separators=(',', ':'))
    
    hasher = hmac.new(secret.encode('utf8'), encoded_body.encode('utf8'), hashlib.sha256)
    dig = hasher.hexdigest()
    
    return hmac.compare_digest(dig, signature)

# WEBHOOK_SECRET = "wsk_...."
signature = "91b3d1667cf420806512fb61b69f04af6fe9b71505e8c3acedeee4cc9a71017b" # in the header
body_str = "team_name=yourteam&type=profile.storing.success" # in the body for example

print(check_signature(WEBHOOK_SECRET, signature, body_str)) # True
require 'openssl'

def check_signature(secret_key, request_signature, request_body)

  digest = OpenSSL::Digest.new('sha256')

  hmac = OpenSSL::HMAC.new(secret_key, digest)
  hmac.update(request_body)
  hmac.to_s == request_signature
end

# req_sig = request.headers['HTTP-HRFLOW-SIGNATURE']
# req_body = request.body.read
# secret_key = ENV['HRFLOW_WEBHOOK_KEY']

req_sig = '9d101d2bf630748679226b767d2031634c520390ff0e926afc09bc65a05bfdb2'
req_body = '4567'
secret_key = '1234'

puts check_signature(secret_key, req_sig, req_body)

2. Handling a Webhook Request with a Workflow CATCH

Handling an event is very simple. You can specify callback function to handle every event incoming on your webhooks' endpoint. Each event must have its own handling function.

πŸ“˜

Requierments

Requirements

  • The REQUEST header
  • The X-API-KEY with Read & Write permissions
  • The WEBHOOK SECRET KEY
  • An endpoint receiving post requests

In the following example, we will use a Workflow CATCH URL as an endpoint to catch Webhook events.
You will just have to:

  • Go to Connections > Connectors Marketplace > Workflows > Catch and create a Workflow with the type CATCH .
  • Go to Connections > Connectors Marketplace > Destinations > Webhook > Settings and add a Webhook with an event type, an event name and copy/paste your Workflow CATCH URL

The following example shows you how you can:

  • Receive a Webhook Event
  • Decode the Webook Event
  • Verify the signature of the sender
  • Execute a business logic
from hrflow import Hrflow
import mailchimp_transactional as MailchimpTransactional
from mailchimp_transactional.api_client import ApiClientError


def get_profile_event(_request: dict) -> dict:
    """
    Reconstruct webhook body
    @param _request: POST request
    @return: webhook event
    """
    return {
        "type": _request.get("type"),
        "origin": _request.get("type"),
        "message": _request.get("message"),
        "profile": _request.get("profile")
    }


def verify_webhook(signature: str, event: dict, secret: str) -> null:
    """
    Verify Webhook Signature
    @param signature: webhook integrity signature
    @param event: webhook data event
    @param secret: webhook secret key
    """
    import json
    import hmac
    import hashlib
    message = json.dumps(event, separators=(",", ":")).encode()
    hasher = hmac.new(secret.encode(), message, hashlib.sha256)
    dig = hasher.hexdigest()
    assert hmac.compare_digest(dig, signature)


def workflow(_request: dict, settings: dict) -> None:
    """
    WORKFLOW to send an email to a profile after catching a wehook parsing success event
    @rtype: null
    @param _request: dictionary that contains the body and the headers of the request
    @param settings: dictionary of settings params of the workflow
    """
    if not _request.get("profile"):
        return
    webhook_event = get_profile_event(_request)
    assert webhook_event["type"] == "profile.parsing.success"  # This workflow is only to create a new profile
    verify_webhook(_request["HTTP-HRFLOW-SIGNATURE"], webhook_event, settings["HRFLOW_WEBHOOK_SECRET"])
    profile_key = webhook_event["profile"]["key"]
    source_key = webhook_event["profile"]["source"]["key"]
    # Retrieve Profile from HrFlow.ai
    hrflow_client = Hrflow(api_secret=settings["HRFLOW_API_SECRET"], api_user=settings["HRFLOW_API_USER"])
    profile = hrflow_client.profile.storing.get(source_key=source_key, key=profile_key).get("data")
    assert profile is not None
    # send Email with Mailchimp
    mailchimp = MailchimpTransactional.Client(settings["MANDRILL_API_TOKEN"])
    message = {
        "from_email": settings["HRFLOW_API_USER"],
        "subject": "Thank you for your application",
        "text": "Your application is well received. Our team will reach out to you if there are any opportunities "
                "matching your profile. Best, the Team",
        "to": [
            {
                "email": profile["info"]["email"],
                "type": "to"
            }
        ]
    }
    try:
        response = mailchimp.messages.send({"message": message})
        print("AcPI called successfully: {}".format(response))
    except ApiClientError as error:
        print("An excception occurred: {}".format(error.text))