Webhooks

As a Jiko Partner API user, you may want to be made aware of events surrounding your account, as they happen in real time. For this, we recommend setting up subscriptions to Jiko Partner API webhooks.

Available Webhook Subscriptions

To view available event types, make an authenticated request to List Event Types:

send_jiko_request "GET" "/api/v1/events/types/"

It should return a 200 OK response with an object that looks something like this:

{
  "offset":0,
  "count": 16,
  "items": [
    {
      "event":"application.approved",
      "object_type":"EventType"
    },
    {
      "event":"application.documents_needed",
      "object_type":"EventType"
    },
    ...
  ],
  "object_type":"List"
}

The list of available event types are found in your response object, by looping through items["event"].

To see example payloads for each event type, see the Trigger Webhook documentation.

The following event types are currently available:

  1. application.approved
  2. application.documents_needed
  3. application.manual_review
  4. application.rejected
  5. card.status.closed
  6. card.status.frozen
  7. card.status.locked
  8. card.status.open
  9. card.transaction.approved
  10. card.transaction.rejected
  11. transfers.ach.out.success
  12. transfers.ach.out.rejected
  13. transfers.ach.in.success
  14. transfers.ach.in.rejected
  15. transfers.wire.out.processing
  16. transfers.wire.out.success
  17. transfers.wire.out.rejected
  18. transfers.on-us.processing
  19. transfers.on-us.success
  20. transfers.on-us.rejected

Creating a Subscription

Call Create Subscription with the following data in the request body:

  1. list of event types to subscribe to
  2. URL to invoke when an event is created
  3. shared_secret of your choice (16 character minimum)
  4. optional description of the subscription

Your request should look something like this:

body='{
 "description": "Webhook subscription for application statuses",
 "events": [
   "application.approved",
   "application.documents_needed",
   "application.manual_review",
   "application.rejected"
 ],
 "url": "https://example.com/webhooks/application-status",
 "shared_secret": "0123456789123456"
}';
send_jiko_request "POST" "/api/v1/subscriptions/" $body

If you get a 201 status code response then you’re up and running and your response should look something like this:

{
  "description": "Webhook subscription for application statuses",
  "events": [
    "application.approved",
    "application.manual_review",
    "application.documents_needed",
    "application.rejected"
  ],
  "url": "https://example.com/webhooks/application-status",
  "id": "55b53873-3abf-4abd-971f-65331ce6dcc4",
  "time_created": "2022-08-09T17:30:52.646971+00:00",
  "updated_at": "2022-08-09T17:30:52.647004+00:00",
  "status": "enabled",
  "object_type": "Subscription"
}

Verifying the sender

There are a few methods for securing webhook deliveries. We calculate a signature of the request using the shared secret and send it with the HTTP request to your webhook URL. Additionally, we try to only include references to objects in the request bodies. These references can then be used to make calls to the Partner API, using valid API credentials.

Calculating the signature

HMAC-SHA256 is used to calculate the signature contained in the x-jiko-signature HTTP request header sent with the webhook request.

The method is as follows:

base64(hmac_sha256(shared_secret, request_body))

where

  • shared_secret is the shared secret specified during the subscription creation.
  • request_body is the entire utf-8 encoded string containg the webhook request body.

In the following two examples, we have a webhook API setup for listening to webhook calls from the Partner API, having already created a subscription with a shared secret:

import base64
import hashlib
import hmac
import json
import os

from fastapi import FastAPI, HTTPException, Request


# we've setup an environment variable containing our shared secret
SHARED_SECRET = os.getenv("SHARED_SECRET", "")


def calculate_signature(request_bytes: bytes) -> str:
    hashed = hmac.new(
        SHARED_SECRET.encode("utf-8"), request_bytes, hashlib.sha256
    ).digest()

    return base64.b64encode(hashed).decode("utf-8")


app = FastAPI()


@app.post("/webhook", status_code=202)
async def receive_webhook(request: Request) -> None:
    body_bytes = await request.body()

    # we calculate the signature using the request body
    signature = calculate_signature(body_bytes)

    if request.headers["x-jiko-signature"] != signature:
        raise HTTPException(401, "Invalid signature!")

    # Okay, this seems to have come from Jiko, so now
    # we can do something with the request data!
    request_json = json.loads(body_bytes)

    if request_json["event_type"] == "application.approved":
        application_id = request_json["payload"]["application_id"]

        # ...

Here's a similar example in Node.js, using Express.js.

const express = require('express')
const bodyParser = require('body-parser')
const crypto = require('crypto')

function calculateSignature(requestJson, sharedSecret) {
  const hashed = crypto
    .createHmac('sha256', sharedSecret)
    .update(requestJson)
    .digest()

  return Buffer.from(hashed).toString('base64')
}

const app = express()
  .use(bodyParser.text({ type: '*/*' }))
  .post('/webhook', function (req, res) {
    const sig = calculateSignature(req.body, process.env.SHARED_SECRET)

    if (sig !== req.headers['x-jiko-signature']) {
      res.status(401).send('Invalid signature!')
      return
    }

    // do stuff with the request body
    const { payload, event_type } = JSON.parse(req.body)

    if (event_type === 'application.approved') {
      console.log(
        "I'm about to fetch this application from the Partner API!",
        payload.application_id
      )
    }

    res.status(202).send()
  })

app.listen(3000, () => console.log('Listening on 3000!'))

Last updated: May 17, 2024