Webhooks

Webhooks are used to communicate changes to a transaction or other entities within the Hurdlr API. If you subscribe to webhooks, then your server will be alerted every time an entity is modified or a notable lifecycle event occurs.

1. How it works

By subscribing to receive "ENTITY_UPDATE" webhooks, your server can asynchronously fetch updated data at the exact moment it becomes available. When that new data is available, the Hurdlr API will make a POST call to a URL of your choosing with the data needed for your server to update appropriately. This approach ensures your users always have up-to-date data while removing unnecessary polls/refreshes from your server, saving you valuable resources.

By subscribing to receive "LIFECYCLE_EVENT" webhooks, you can trigger timely lifecycle notifications (e.g. push notifications, emails, and/or other alerts) based on timely user actions. For example, if a user's client views their invoice, the Hurdlr API will make a POST call to a URL of your choosing with the data needed for your server to alert your user that their client viewed the invoice.

2. Subscribing to receive webhooks

Hurdlr's API team is here to help! Simply email [email protected] with the subject line "Subscribe to Webhooks", and provide the following info:

a. Your API client_Id
b. Webhook URL for the Sandbox environment
c. Webhook URL for the Production environment

Our API team will respond back within 5 business days with your Sandbox webhook_secret and your Production webhook_secret and will fire off sample webhooks to your Sandbox Webhook URL, for your team to develop/test with.

3. ENTITY_UPDATE

An "ENTITY_UPDATE" webhook will contain a JSON body like the following:

{
  "userId": "fake_userId",
  "type": "ENTITY_UPDATE",
  "entities": ["bankTransfer", "expense", "revenue", "taxPayment"]
}

The possible entities and recommended actions on how to update each given entity are listed below:

Webhook TypeEntityRecommended action
ENTITY_UPDATEattachmentGET /attachments
ENTITY_UPDATEbankAccountGET /bankAccounts
ENTITY_UPDATEbankTransferGET /bankTransfers
ENTITY_UPDATEbusinessGET /businesses
ENTITY_UPDATEclientGET /clients
ENTITY_UPDATEexpenseGET /expenses
ENTITY_UPDATEexpenseCategoryGET /expenses/categories
ENTITY_UPDATEexpenseRuleGET /expenses/rules
ENTITY_UPDATEglAccountGET /glAccounts
ENTITY_UPDATEglEntryGET /glEntries
ENTITY_UPDATEintegrationGET /integrations
ENTITY_UPDATEinvoiceGET /invoices
ENTITY_UPDATEinvoiceSetupGET /invoiceSetup
ENTITY_UPDATEpaymentGET /payments
ENTITY_UPDATEpersonalExpenseCategoryGET /expenses/personalCategories
ENTITY_UPDATEplaidItemGET /plaidItems
ENTITY_UPDATErevenueGET /revenues
ENTITY_UPDATErevenueRuleGET /revenueRules
ENTITY_UPDATEtaskGET /tasks
ENTITY_UPDATEtaxEstimatesGET /taxEstimates
ENTITY_UPDATEtaxPaymentGET /taxPayments
ENTITY_UPDATEtimeGET /times
ENTITY_UPDATEuserTaxSetupGET /userTaxSetup
ENTITY_UPDATEvendorGET /vendors

4. LIFECYCLE_EVENT

A "LIFECYCLE_EVENT" webhook will contain a JSON body like the following:

{
  "userId": "fake_userId",
  "type": "LIFECYCLE_EVENT",
  "event": "TRANSACTION_PULL_COMPLETED",
  "eventData": {
    "integrationName": "PLAID"
  }
}

📘

Custom lifecycle events

The Hurdlr API team can customize events to power your ideal user experience. Don't hesitate to email us at [email protected] if you are looking for something that you don't see below.

Some of the possible events and recommended messaging to notify your users are listed below:

Webhook TypeEventExample message
LIFECYCLE_EVENTBANK_TRANSFER_ADDED1 new bank transfer from *3333 needs to be reviewed.
LIFECYCLE_EVENTBANK_TRANSFER_AUTO_TAGGED2 new bank transfers were automatically matched.
LIFECYCLE_EVENTBANK_TRANSFER_UPDATEDN/A (useful for user analytics)
LIFECYCLE_EVENTEXPENSE_ADDED4 new expenses from *3333 need to be tagged.
LIFECYCLE_EVENTEXPENSE_AUTO_TAGGED2 new expenses from *3333 were auto-tagged.
LIFECYCLE_EVENTEXPENSE_RULE_ACCEPTEDN/A (useful for user analytics)
LIFECYCLE_EVENTEXPENSE_UPDATEDN/A (useful for user analytics)
LIFECYCLE_EVENTINTEGRATION_ADDEDN/A (useful for triggering next set of onboarding steps/messages)
LIFECYCLE_EVENTINTEGRATION_ERRORRe-link your Citi account
LIFECYCLE_EVENTINVOICE_CREATEDN/A (useful for user analytics)
LIFECYCLE_EVENTINVOICE_PAIDBill Tanner has paid Invoice HUR-121.
LIFECYCLE_EVENTINVOICE_PAYMENT_METHOD_STOREDBill Tanner has added a card on file for future invoices.
LIFECYCLE_EVENTINVOICE_SETUP_COMPLETEDN/A (useful for user analytics)
LIFECYCLE_EVENTINVOICE_VIEWEDBill Tanner has opened Invoice HUR-121.
LIFECYCLE_EVENTJOURNAL_ENTRY_ADDEDN/A (useful for user analytics)
LIFECYCLE_EVENTPAYMENT_PROCESSING_SETUP_COMPLETEDN/A (useful for user analytics)
LIFECYCLE_EVENTRECEIPT_ATTACHEDN/A (useful for user analytics)
LIFECYCLE_EVENTREVENUE_ADDED3 new income transactions from *3332 need to be tagged.
LIFECYCLE_EVENTREVENUE_AUTO_TAGGED1 new income transaction from *3332 was auto-tagged.
LIFECYCLE_EVENTREVENUE_RULE_ACCEPTEDN/A (useful for user analytics)
LIFECYCLE_EVENTREVENUE_UPDATEDN/A (useful for user analytics)
LIFECYCLE_EVENTTAX_PAYMENT_ADDED1 new tax payment from *3333 needs to be reviewed.
LIFECYCLE_EVENTTAX_PAYMENT_UPDATEDN/A (useful for user analytics)
LIFECYCLE_EVENTTAX_SETUP_COMPLETEDN/A (useful for user analytics)
LIFECYCLE_EVENTTRANSACTION_PULL_COMPLETEDHistorical transactions are ready to be tagged.

👍

Additional use cases

The above lifecycle events can be used in varying ways depending on your product's use case. For example, if your product manages corporate/employee spend, it may benefit your user experience to make the EXPENSE_ADDED event trigger a push notification prompting the user to add a receipt image. The eventData included with the EXPENSE_ADDED event contains the information necessary to route the user to the exact transaction that requires a receipt.

5. Webhook Verification

Hurdlr signs every webhook, so that you have the option to verify the webhooks you receive. This helps you protect against a bad actor flooding your server with fake webhooks.

Verifying Webhooks
Hurdlr follows the JSON Web Token (JWT) standard and includes its JWTs in the Hurdlr-Verification HTTP header of the webhook.

To verify a Hurdlr JWT, follow these steps:

  1. Extract the Hurdlr-Verification HTTP header from the Hurdlr webhook

  2. Select a JWT library of your choice

  3. Pass in HMAC-SHA256 as the signing algorithm to your selected JWT library and then use the library to verify the value of the Hurdlr-Verification header against your webhook_secret, obtained in 2. Subscribing to receive webhooks

import 'dotenv/config';
import express from "express";
import { jwtVerify } from "jose";

const app = express();

// Your webhook_secret
const webhook_secret = process.env.SECRET;

app.post("/webhook", express.raw({type: "application/json"}), async (req, res) => {
  const jwt = req.get('Hurdlr-Verification');

  let payload;
  try {
    // Verify the JWT using the HMAC-SHA256 signing algorithm
    const jwtVerifyResult = await jwtVerify(jwt, webhook_secret, {algorithms: ["HS256"]});
    payload = jwtVerifyResult.payload;
  } catch (err) {
    return res.status(400).send(`JWT failed to verify: ${err.message}`);
  }

  // ...
});

Note: If your library does not automatically decode the JWT, Base64Url decode the JWT payload and deserialize it into a JSON object.

  1. Ensure that the webhook is not expired. Verify that the difference between the iat field of the payload and the current NumericDate timestamp is within your tolerance. Hurdlr recommends a default tolerance of 5 minutes.
app.post("/webhook", express.raw({type: "application/json"}), async (req, res) => {
  // ...

  // Validate the iat, i.e. make sure the webhook has not expired
  // Hurdlr recommends a default tolerance of five minutes
  const TIME_TOLERANCE = (60 * 5);
  let timestamp = payload.iat;
  const now = Math.floor(Date.now() / 1000);
  if (now - timestamp >= TIME_TOLERANCE) {
      return res.status(400).send("Webhook expired")
  }

  // ...
});
  1. Ensure that the webhook was not modified by anyone other than Hurdlr. Verify that the request_body_sha256 field of the payload is equal to the SHA-256 hash of the webhook’s request body.
import { timingSafeEqual } from "node:crypto";
const { subtle } = globalThis.crypto;

app.post("/webhook", express.raw({type: "application/json"}), async (req, res) => {
  // ...

  // Validate the webhook body hash
  const enc = new TextEncoder();
  const bodyDigest = await subtle.digest("SHA-256", req.body);
  const webhookHash = enc.encode(btoa(String.fromCharCode(...new Uint8Array(bodyDigest))));
  const claimHash = enc.encode(payload.request_body_sha256 as string);
  if (!timingSafeEqual(webhookHash, claimHash)) {
      return res.status(400).send("Webhook payload modified")
  }


  // Verification succeeded
  // You can process the webhook here or call the next handler in your request-response cycle
  res.send("Webhook verification succeeded");
});

Once these steps are completed successfully, you can be confident that the webhook came from Hurdlr and that it is safe to process.


What’s Next