Overview

Webhooks are automated messages that are sent by Citadel API as an HTTP POST request to a predefined URL when an event occurs on the platform.

We recommend using webhooks to track TaskTask - A process where a Link is used to pull/write data from/to a payroll provider. status changes while it is being processed. After a successful payroll connection, data and PDF documents that come from the payroll provider need time to be processed. To not delay the user experience or the API responses, you can use webhooks to get updates when the status of a TaskTask - A process where a Link is used to pull/write data from/to a payroll provider. changes.

Subscribing

Visit Citadel Dashboard Development > Webhooks and add a URL for your environment. Each environment has separate URL and needs to be specified independently.

Testing

You can test hooks by running Citadel Bridge in Emulator. In case you don't have an endpoint for webhooks today, we recommend using ngrok to test payload in your local environment or MockBin to test webhooks in your browser.

Payload

There are many fields available when Citadel sends a request to a webhook endpoint and quite a few depend on the event that is being sent. Here's a list of the common fields that will occur in every call regardless of the event:

NameInDescription
X-WEBHOOK-SIGNheaderA hash of the request body created with an {{ definitions.Access_key }}
webhook_idbodyA unique identifier for this specific webhook request
event_typebodyAn identifier of the event the webhook request is sent for
updated_atbodyThe time the event occurred

Events

Task status change

Sample webhook payload for "task-status-updated"

{
    "webhook_id": "609a82aab21e4d9ba2569f35e9e8f26a",
    "event_type": "task-status-updated",
    "updated_at": "2021-04-26T13:02:20.369267+00:00",
    "task_id": "67f2924530564282bbaf6d27655e94a4",
    "link_id": "64f8e374949c4b769706028022626bf1",
    "product": "income",
    "tracking_info": "27266f35-bb54-44c3-8905-070641a0c0aa",
    "status": "login"
}

Event task-status-updated occurs whenever the status of a TaskTask - A process where a Link is used to pull/write data from/to a payroll provider. changes. When you receive a task-status-updated
event with a status of done all the data for the TaskTask - A process where a Link is used to pull/write data from/to a payroll provider. is downloaded and documents have been processed.
You can use the link_id value to locate the access_tokenaccess_token - A private token unique to a single Link. Used to access Link data and initiate any actions using the same Link in your system and retrieve the latest payroll data from the respective endpoint.

Field NameDescription
task_idThe identifier of the TaskTask - A process where a Link is used to pull/write data from/to a payroll provider. associated to the event
link_idThe identifier of the LinkLink - A connection to a payroll provider used to retrieve payroll data. associated to the TaskTask - A process where a Link is used to pull/write data from/to a payroll provider.
productWhich product the TaskTask - A process where a Link is used to pull/write data from/to a payroll provider. is for (employment, income or admin)
tracking_infoAny info passed into the bridge_tokenbridge_token - A short-lived token provided by the Citadel API to authorize the use of Bridge. This token has a 6 hour expiration. for the LinkLink - A connection to a payroll provider used to retrieve payroll data. . Nullable.
statusThe new task status from the Task Lifecycle

Order status change

Sample webhook payload for "order-status-updated"

{
    "webhook_id": "609a82aab21e4d9ba2569f35e9e8f26a",
    "event_type": "order-status-updated",
    "updated_at": "2021-04-26T13:02:20.369267+00:00",
    "order_id": "67f2924530564282bbaf6d27655e94a4",
    "order_number": "100",
    "employer_id": "56f8e374949c4b769706028022626zz1",
    "link_id": "64f8e374949c4b769706028022626bf1",
    "product": "income",
    "status": "completed"
}

Event order-status-updated occurs whenever the status of orders changes. When you receive a order-status-updated event with a status of completed it means the order has been successfully processed and a payroll account(s) is linked.

Field NameDescription
order_idThe identifier of the order
order_numberAny info passed into the order_number for the order. Nullable.
employer_idUnique Employer ID of the order
link_idThe identifier of the LinkLink - A connection to a payroll provider used to retrieve payroll data. associated to the TaskTask - A process where a Link is used to pull/write data from/to a payroll provider. . Nullable.
productWhich product the TaskTask - A process where a Link is used to pull/write data from/to a payroll provider. is for (employment or income)
statusThe new order status following orders lifecycle

Timing

While on the Citadel side webhook requests for Task statuses are sent in the same order the statuses are updated (ie. a full_parse event happens before a done event) there are many factors that can influence the delivery of webhook requests, such as network latency, outages, etc. As a result it's possible to not receive webhook events in the proper order so it's important when implementing your code to receive webhook requests from Citadel that you reference the updated_at field to track which events happened when.

Security

In order for you to be able to verify that data via webhook is coming from Citadel API we implemented webhook signatures. Every webhook request we send contains an X-WEBHOOK-SIGN header which is an HMAC hash of the request body using your Access keyAccess key - The unique key passed into the X-Access-Secret header of every API request to authorize it. as the hashing key and SHA-256 as the hashing function.

Follow the steps below to verify the request is coming from us:

  1. Use a hash library to create an HMAC hash of the raw request body received by Citadel using the Access key you normally place in the X-Access-Secret header as the hashing key. Make sure to use SHA-256 as the hash function and that the final hash is converted to hexidecimal.

  2. Compare the hash created to the value in the X-WEBHOOK-SIGN header sent with the webhook request. If they match you can rest assured that Citadel sent the request and you can continue to process the webhook.

import hashlib
import hmac

def generate_webhook_sign(payload: str, key: str) -> str:
    generated_hash = hmac.new(
        key=key.encode('utf-8'),
        msg=payload.encode('utf-8'),
        digestmod=hashlib.sha256,
    ).hexdigest()
    return f'v1={generated_hash}'

@app.route('/webhook', methods=['POST'])
def webhook():
    return generate_webhook_sign(request.data.decode('UTF-8'), secret)
import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
  "fmt"
  "strings"
)

func generate_webhook_sign(body string, key string) string {

  mac := hmac.New(sha256.New, []byte(key))
  mac.Write([]byte(body))
  return hex.EncodeToString(mac.Sum(nil))
}

func webhook(w http.ResponseWriter, r *http.Request) {
  b, _ := ioutil.ReadAll(r.Body)
  convertedBody := string(b)
  signature := generate_webhook_sign(convertedBody, os.Getenv("API_SECRET"))
  fullSignature := fmt.Sprintf("v1=%s", signature)

  fmt.Fprintf(w, fullSignature)
}
const crypto = require("crypto")

// ensure all request bodies are parsed to JSON. Callback function
// keeps a copy of the raw body for webhooks.
app.use(bodyParser.json({
  verify: (req, res, buf) => {
    req.rawBody = buf
  }
}))

const generate_webhook_sign = (body, key) => {
  return crypto.createHmac("sha256", key)
  .update(body)
  .digest("hex")
}

app.post("/webhook", async (req, res) => {

  const body = req.rawBody.toString()
  const webhook_sign = generate_webhook_sign(body, API_SECRET)

  res.send(`v1=${webhook_sign}`).end()
})
class Webhook
  def self.generate_webhook_sign(body, key)
    digest = OpenSSL::Digest.new('sha256')
    return "v1=" + OpenSSL::HMAC.hexdigest(digest, key, body)
  end

  def self.post(body)
    return self.generate_webhook_sign(body, Citadel.client_secret)
  end
end
using System.Threading.Tasks;
using System.IO;
using Microsoft.AspNetCore.Mvc;
using System.Text;
using System.Security.Cryptography;
using System;

namespace c_sharp.Controllers
{
  [ApiController]
  [Route("webhook")]
  public class WebhookController : ControllerBase
  {

    [HttpPost]
    public async Task<string> Post()
    {
      using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
      {
        string body = await reader.ReadToEndAsync();
        return generateWebhookSign(body, Environment.GetEnvironmentVariable("API_SECRET"));
      }
    }

    private string generateWebhookSign(string body, string key)
    {
      using (HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key)))
      {
        // Compute the hash of the input file.
        byte[] hashValue = hmac.ComputeHash(Encoding.UTF8.GetBytes(body));

        return "v1=" + BitConverter.ToString(hashValue).Replace("-", "").ToLower();

      }
    }
  }
}

HTTP Timeouts

We have strict HTTP request timeouts: 3 second for a connection timeout and 5 second for a read timeout (wait for the response after connection). The receiving API should respect those timeouts, otherwise the webhook events will not be received successfully.


Did this page help you?