Webhooks are a type of API that enables server-to-server communication. They allow real-time notifications to be sent to a designated URL whenever specific events occur. By subscribing to webhooks, developers can significantly reduce the number of API calls made and enable their applications to respond to critical events in real-time.

To subscribe to a webhook, a developer must provide the URL for the server that will receive the notifications. Once the subscription is established, the specified URL will receive HTTP requests with a payload containing information about the event that triggered the webhook.

In the case of CrediLinq Support, developers can subscribe to a variety of webhook events, such as customer updates or drawdown initiations. Each event has its own unique payload structure, which includes relevant information about the event.

It is important to note that webhook subscriptions can be configured to include custom data or authentication credentials to verify the sender's identity. Developers can use this information to ensure that the received requests are legitimate and take appropriate actions accordingly.

Creating a Webhook

Depending on the business use case, CrediLinq can configure your account so that all notifications will be sent to the respectively configured webhook. Just send us an email on support mentioning your

  • id
  • redirect_url (webhook_url)
  • an event as per the below-specified table

List of Available Webhook Events

EventsDescription
DRAWDOWN_INITIATEDDrawdown/Loan is initiated
DRAWDOWN_APPROVEDDrawdown/Loan is approved
DRAWDOWN_DISBURSEDDrawdown/Loan is disbursed
DRAWDOWN_PAYMENT_VERIFIEDDrawdown/Loan payment is verified
DATA_PROCESSING_COMPLETEDCustomer Data Processing Completed
DATA_PROCESSING_FAILEDCustomer Data Processing Failed
CUSTOMER_KYC_ACCEPTEDCustomer's KYC is accepted
CUSTOMER_KYC_REJECTEDCustomer's KYC is rejected
CUSTOMER_APP_ACCEPTEDCustomer's KYC Application is accepted,
and final credit limits are assigned
CUSTOMER_APP_REJECTEDCustomer's KYC Application is rejected
DIRECTOR_SIGNATURE_UPDATEDOnce Director signs Loan Agreement
LOO_AGREEMENT_SIGNEDAll Directors completed their signature
on Loan Agreement
CREDIT_LINE_UPDATEDOnce Customer's credit line will changed

Sample Payloads

DRAWDOWN_INITIATED
{
  "loanId": "2071b12e-a831-4a53-****-******",
  "loanReferenceNo": "BNPL*******",
  "customerReferenceNo": "7171b12e-sgb2-****-******",
  "loanTermFrequency": 30,
  "loanType": "BNPL",
  "loanStatus": "pending-for-approval",
  "disbursalAmount": 500,
  "totalDueAmount": 505,
  "loanDueDate": "2023-05-23T18:30:00.000Z",
  "loanDueDateUTC": "2023-05-23T18:30:00.000Z",
  "charges": 1,
  "chargesApplied": 5,
  "customerApprovedCredit": "75000",
  "customerAvailableCredit": "48000",
  "currency": "SGD",
  "remark": "Loan for xyz ltd, construction purpose",
  "metadata": "",
  "createdAt": "2023-04-24T08:15:40.152Z",
  "updatedAt": "2023-04-24T08:15:40.152Z",
  "event": "DRAWDOWN_INITIATED"
}
DRAWDOWN_APPROVED
{
  "loanId": "2071b12e-a831-4a53-****-******",
  "loanReferenceNo": "BNPL*******",
  "customerReferenceNo": "7171b12e-sgb2-****-******",
  "loanTermFrequency": 30,
  "loanType": "BNPL",
  "loanStatus": "waiting-for-disbursal",
  "disbursalAmount": 500,
  "totalDueAmount": 505,
  "loanDueDate": "2023-05-23T18:30:00.000Z",
  "loanDueDateUTC": "2023-05-23T18:30:00.000Z",
  "charges": 1,
  "chargesApplied": 5,
  "customerApprovedCredit": "75000",
  "customerAvailableCredit": "48000",
  "currency": "SGD",
  "remark": "Loan for xyz ltd, construction purpose",
  "metadata": "",
  "createdAt": "2023-04-24T08:15:40.152Z",
  "updatedAt": "2023-04-24T08:15:40.152Z",
  "event": "DRAWDOWN_APPROVED"
}
DRAWDOWN_DISBURSED
{
  "loanId": "2071b12e-a831-4a53-****-******",
  "loanReferenceNo": "BNPL*******",
  "customerReferenceNo": "7171b12e-sgb2-****-******",
  "loanTermFrequency": 30,
  "loanType": "BNPL",
  "loanStatus": "waiting-for-disbursal",
  "disbursalAmount": 500,
  "totalDueAmount": 505,
  "loanDueDate": "2023-05-23T18:30:00.000Z",
  "loanDueDateUTC": "2023-05-23T18:30:00.000Z",
  "charges": 1,
  "chargesApplied": 5,
  "customerApprovedCredit": "75000",
  "customerAvailableCredit": "48000",
  "currency": "SGD",
  "remark": "Loan for xyz ltd, construction purpose",
  "metadata": "",
  "createdAt": "2023-04-24T08:15:40.152Z",
  "updatedAt": "2023-04-24T08:15:40.152Z",
  "event": "DRAWDOWN_DISBURSED"
}
DRAWDOWN_PAYMENT_VERIFIED
{
  "paymentId": "443b12e-a831-4a53-****-******",
  "remark": "Verified",
  "paymentDate": "2023-05-23T18:30:00.000Z",
  "paymentAmount": 47000,
  "currency": "SGD",
  "paymentMethod": "Bank Transfer",
  "paymentReferenceNo": "5362356213",
  "accountNumber": "601825128001",
  "accountName": "OVERSEA-CHINESE BANKING CORPORATION LIMITED",
  "bankName": "",
  "bankSwiftCode": "OCBCSGSG",
  "paymentStatus": 2,
  "loanId": "2071b12e-a831-4a53-****-******",
  "customerReferenceNo": "7171b12e-sgb2-****-******",
  "metadata": "",
  "createdAt": "2023-04-24T08:15:40.152Z",
  "updatedAt": "2023-04-24T08:15:40.152Z",
  "event":"DRAWDOWN_PAYMENT_VERIFIED"
}
DATA_PROCESSING_COMPLETED
{
  "applicationId": "e050cd2b-3c23-****-****-*******",
  "dataProcessingId": "c2f2fa3b-2268-****-****-*******",
  "customerReferenceNo": "7171b12e-sgb2-****-****-*******",
  "creditEligibility":"Yes/No",
  "inPrincipleApprovedLine": 689878,
  "availableLine": 471710,
  "currency": "SGD",
  "rateOfInterest": 4,
  "segment": "High Risk-1",
  "status": "success",
  "creditScore": 2.3,
  "event": "DATA_PROCESSING_COMPLETED"
}
DATA_PROCESSING_FAILED
{
  "applicationId": "e050cd2b-3c23-****-****-*******",
  "dataProcessingId": "c2f2fa3b-2268-****-****-*******",
  "customerReferenceNo": "7171b12e-sgb2-****-****-*******",
  "status": "failed",
  "reasons": ["error-1", "error-2"],
  "failedStage": "ETL",
  "event": "DATA_PROCESSING_FAILED"
}
CUSTOMER_KYC_ACCEPTED
{
  "applicationId": "e050cd2b-3c23-****-****-*******",
  "customerReferenceNo": "7171b12e-sgb2-****-****-***",
  "reason": "Verified & All OK",
  "event": "CUSTOMER_KYC_ACCEPTED"
}
CUSTOMER_KYC_REJECTED
{
  "applicationId": "e050cd2b-3c23-****-****-*******",
  "customerReferenceNo": "7171b12e-sgb2-****-****-***",
  "reason": "KYC Documents are not correct",
  "event": "CUSTOMER_KYC_REJECTED"
}
CUSTOMER_APP_ACCEPTED
{
  "applicationId": "e050cd2b-3c23-****-****-*******",
  "customerReferenceNo": "7171b12e-sgb2-****-****-***",
  "approvedLine": 75000,
  "availableLine": 50000,
  "rateOfInterest":1.4,
  "currency": "SGD",
  "receivableAccountDetails": [
      {
        "receivableType": "Bank Transfer",
        "accountDetails": {
          "accountName": "John Smith",
          "accountNumber": "88464922983****",
          "bankName": "BANK MANDIRI",
          "bankSwiftCode": "BMRIIDJA"
        }
      }
    ],
  "event": "CUSTOMER_APP_ACCEPTED"
}
CUSTOMER_APP_REJECTED
{
  "applicationId": "e050cd2b-3c23-****-****-*******",
  "customerReferenceNo": "7171b12e-sgb2-****-****-***",
  "reason": "KYC Documents are not correct",
  "event": "CUSTOMER_APP_REJECTED"
}
DIRECTOR_SIGNATURE_UPDATED
{
  "applicationId": "e050cd2b-3c23-****-****-*******",
  "customerReferenceNo": "7171b12e-sgb2-****-****-*******",
  "agreementStatus": "partially-signed",
  "directors": [
    {
      "directorId": "fda6cefc-2257-4748-8f23-*******",
      "directorName": "Director 1",
      "directorEmail": "[email protected]",
      "signatureStatus": true
    },
    {
      "directorId": "b936f698-035a-4422-be7f-********",
      "directorName": "Director 2",
      "directorEmail": "[email protected]",
      "signatureStatus": false
    },
    {
      "directorId": "021dd48b-e540-4c7d-9971-*********",
      "directorName": "System Director",
      "directorEmail": "[email protected]",
      "signatureStatus": false
    }
  ],
  "event": "DIRECTOR_SIGNATURE_UPDATED"
}
LOO_AGREEMENT_SIGNED
{
  "applicationId": "e050cd2b-3c23-****-****-*******",
  "customerReferenceNo": "7171b12e-sgb2-****-****-*******",
  "agreementStatus": "signed",
  "directors": [
    {
      "directorId": "fda6cefc-2257-4748-8f23-*******",
      "directorName": "Director 1",
      "directorEmail": "[email protected]",
      "signatureStatus": true
    },
    {
      "directorId": "b936f698-035a-4422-be7f-********",
      "directorName": "Director 2",
      "directorEmail": "[email protected]",
      "signatureStatus": true
    },
    {
      "directorId": "021dd48b-e540-4c7d-9971-*********",
      "directorName": "System Director",
      "directorEmail": "[email protected]",
      "signatureStatus": true
    }
  ],
  "event": "LOO_AGREEMENT_SIGNED"
}
CREDIT_LINE_UPDATED
  1. Increase credit line
    {
        "referenceNumber": "onb-test--cust-******",
        "existingAvailableCredit": 771660,
        "existingShadowCreditLine": 772660,
        "existingApprovedLine": 871981,
        "revisedAvailableCredit": 772660,
        "revisedShadowCreditLine": 773660,
        "revisedApprovedLine": 871981,
        "creditChanges": 1000,
        "currency": "USD",
        "action": "increase",
        "event": "CREDIT_LINE_UPDATED",
        "updatedAt": "2023-11-22T12:53:31.429Z"
    }
    
  2. Decrease credit line
    {
        "referenceNumber": "onb-test--cust--******",
        "existingAvailableCredit": 772660,
        "existingShadowCreditLine": 773660,
        "existingApprovedLine": 871981,
        "revisedAvailableCredit": 771660,
        "revisedShadowCreditLine": 772660,
        "revisedApprovedLine": 871981,
        "creditChanges": 1000,
        "currency": "USD",
        "action": "decrease",
        "event": "CREDIT_LINE_UPDATED",
        "updatedAt": "2023-11-22T12:59:29.975Z"
    }
    
  3. Blocked credit line
    {
        "referenceNumber": "onb-test--cust--******",
        "existingAvailableCredit": 735042,
        "existingShadowCreditLine": 736042,
        "existingApprovedLine": 800000,
        "revisedAvailableCredit": 735042,
        "revisedShadowCreditLine": 736042,
        "revisedApprovedLine": 800000,
        "creditChanges": 0,
        "currency": "USD",
        "action": "blocked",
        "event": "CREDIT_LINE_UPDATED",
        "updatedAt": "2023-11-22T12:52:25.925Z"
    }
    
  4. Active credit line
    {
        "referenceNumber": "test-******",
        "existingAvailableCredit": 14000,
        "existingShadowCreditLine": 14000,
        "existingApprovedLine": 15000,
        "revisedAvailableCredit": 14000,
        "revisedShadowCreditLine": 14000,
        "revisedApprovedLine": 15000,
        "creditChanges": 0,
        "currency": "USD",
        "action": "active",
        "event": "CREDIT_LINE_UPDATED",
        "updatedAt": "2023-11-22T12:53:07.365Z"
    }
    
  5. No Changes in credit line
    {
        "referenceNumber": "onb-test--cust-******",
        "existingAvailableCredit": 371315,
        "existingShadowCreditLine": 375815,
        "existingApprovedLine": 514166,
        "revisedAvailableCredit": 371315,
        "revisedShadowCreditLine": 375815,
        "revisedApprovedLine": 514166,
        "creditChanges": 0,
        "currency": "USD",
        "action": "no-changes",
        "event": "CREDIT_LINE_UPDATED"
    }
    

Anatomy of a Webhook Request

A webhook sends an HTTP request to a specified URL in response to activity in Zendesk Support. These requests use specific headers, HTTP methods, formats, and payloads.

Request headers

All webhook requests include the following HTTP headers:

x-credilinq-webhook-user-id: 1234
x-credilinq-webhook-signature: FiqRE3SXTPQpPulBV78SusvyziIishZNc1VwNZYqZrHQ=
x-credilinq-webhook-timestamp: 1679457369
x-credilinq-webhook-invocation-id: c3c4065e-2e2e-41e1-83a1-a24d232c5015,

You can use the x-credilinq-webhook-signature and x-credilinq-webhook-timestamp headers to verify that a request came from Credilinq. See Verifying webhook authenticity.

Credilinq-Webhooks

Verifying Webhooks

Webhooks provide an additional security measure to verify that the webhook is genuine and has come from Credilinq. This can be useful if you want to ensure only Credilinq webhooks are being made to your endpoint and ensure the information is genuine and as the system expects. These signatures will also help prevent against replay attacks. Verifying the signing secret is optional.

Webhook requests will contain two headers which can be used to verify the request's authenticity:

  • x-credilinq-webhook-signature - the main signature
  • x-credilinq-webhook-timestamp - the timestamp used to verify the signature

This is used in conjunction with the payload of the request (if there is one).

Verifying the signature

Sign the body and signature timestamp with the webhook secret key using SHA256, then base64 encoding the resulting digest.

Represented simply: base64(HMACSHA256(TIMESTAMP + BODY))

To verify the signature, create the same SHA256 HMAC signature and then compare it to the webhook payload to ensure they match. If they match, you can be sure that the webhook came from Credilinq. If they don't, it may be a request from another source.

Not all webhook requests will have a body (GETs, DELETEs), so ensure that this scenario is accounted for in any verification code. Depending on the language, this may be an empty string or null. Consult your language's documentation for details.

NodeJS verification example

The example below shows how a signature can be verified using NodeJS and Express. In a production environment, this check should form part of the check to accept or reject the webhook request.

const express = require("express");
const crypto = require("crypto");
require("body-parser-xml")(express);

// Signing secret will be your "client_secret" shared with you
const SIGNING_SECRET = "dGhpc19zZWNyZXRfaXNfZm9yX3Rlc3Rpbmdfb25seQ==";

// Always sha256
const SIGNING_SECRET_ALGORITHM = "sha256";

const PORT = 3000;

const app = express();

function isValidSignature(signature, body, timestamp) {
  let hmac = crypto.createHmac(SIGNING_SECRET_ALGORITHM, SIGNING_SECRET);
  let sig = hmac.update(timestamp + '.' + body).digest("hex");

  return (
    Buffer.compare(
      Buffer.from(signature),
      Buffer.from(sig.toString("hex"))
    ) === 0
  );
}


app.use(express.json());

app.post("/hook", (req, res) => {
  // Fields from the webhook request, this will change every time
  const signature = req.headers["x-credilinq-webhook-signature"];
  const timestamp = req.headers["x-credilinq-webhook-timestamp"];
  const body = req.body;

  console.log(
    isValidSignature(signature, JSON.stringify(body), timestamp)
      ? "HMAC signature is valid"
      : "HMAC signature is invalid"
  );
  res.status(200).send("Success");
});

app.listen(PORT, () => console.log(`🚀 Server running on port ${PORT}`));

from flask import Flask, request
import hashlib
import hmac
import base64

app = Flask(__name__)

SIGNING_SECRET = "dGhpc19zZWNyZXRfaXNfZm9yX3Rlc3Rpbmdfb25seQ=="
SIGNING_SECRET_ALGORITHM = "sha256"
PORT = 3000

def is_valid_signature(signature, body, timestamp):
    key = base64.b64decode(SIGNING_SECRET)
    msg = timestamp.encode() + b'.' + body.encode()
    signature = base64.b64decode(signature)
    computed_signature = hmac.new(key, msg, hashlib.sha256).digest()
    return hmac.compare_digest(signature, computed_signature)

@app.route('/hook', methods=['POST'])
def webhook():
    signature = request.headers.get('x-credilinq-webhook-signature')
    timestamp = request.headers.get('x-credilinq-webhook-timestamp')
    body = request.data.decode('utf-8')

    if is_valid_signature(signature, body, timestamp):
        print("HMAC signature is valid")
    else:
        print("HMAC signature is invalid")

    return "Success", 200

if __name__ == '__main__':
    app.run(port=PORT)
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

public class Startup
{
    private const string SIGNING_SECRET = "dGhpc19zZWNyZXRfaXNfZm9yX3Rlc3Rpbmdfb25seQ==";
    private const string SIGNING_SECRET_ALGORITHM = "sha256";
    private const int PORT = 3000;

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRouting();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapPost("/hook", HandleWebhook);
        });

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync("Not Found");
        });
    }

    private async Task HandleWebhook(HttpContext context)
    {
        var request = context.Request;
        var response = context.Response;

        var signature = request.Headers["x-credilinq-webhook-signature"].FirstOrDefault();
        var timestamp = request.Headers["x-credilinq-webhook-timestamp"].FirstOrDefault();

        using (var reader = new StreamReader(request.Body, Encoding.UTF8))
        {
            var body = await reader.ReadToEndAsync();

            if (IsValidSignature(signature, body, timestamp))
            {
                Console.WriteLine("HMAC signature is valid");
            }
            else
            {
                Console.WriteLine("HMAC signature is invalid");
            }
        }

        await response.WriteAsync("Success");
    }

    private bool IsValidSignature(string signature, string body, string timestamp)
    {
        var signingSecret = Convert.FromBase64String(SIGNING_SECRET);
        var hmac = new HMACSHA256(signingSecret);

        var data = Encoding.UTF8.GetBytes(timestamp + "." + body);
        var computedSignature = hmac.ComputeHash(data);

        var receivedSignature = Convert.FromBase64String(signature);

        return receivedSignature.SequenceEqual(computedSignature);
    }

    public static void Main(string[] args)
    {
        var host = new WebHostBuilder()
            .UseKestrel()
            .UseStartup<Startup>()
            .UseUrls($"http://localhost:{PORT}/")
            .Build();

        host.Run();
    }
}