Building
Onboarding
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:
application.approved
application.documents_needed
application.manual_review
application.rejected
card.status.closed
card.status.frozen
card.status.locked
card.status.open
card.transaction.approved
card.transaction.rejected
transfers.ach.out.success
transfers.ach.out.rejected
transfers.ach.in.success
transfers.ach.in.rejected
transfers.wire.out.processing
transfers.wire.out.success
transfers.wire.out.rejected
transfers.on-us.processing
transfers.on-us.success
transfers.on-us.rejected
Creating a Subscription
Call Create Subscription with the following data in the request body:
- list of event types to subscribe to
- URL to invoke when an event is created
shared_secret
of your choice (16 character minimum)- 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