Webhooks
Best practices for setting up your app to receive webhooks
This reference is intended for developers using Akahu's enduring account connectivity. Information provided may not be relevant for those using one-off account connectivity.
Webhooks are the best way to make your application responsive to new or changing data.
Webhooks are not available for Personal Apps.
Configuring webhooks
Before your application can receive webhooks, you must register a URL that Akahu will send webhook requests to. If your Akahu application has more than one environment, you may register a different webhook URL for each environment. Reach out to our team on Slack or via email if you'd like to configure or update the webhook URL for your application.
Your webhook URL must be a valid https uri that can accept POST requests. An HTTP 200 status code must be returned by your endpoint within 5 seconds for Akahu to recognise the webhook as delivered.
Subscribing to webhooks
Your application must subscribe to each webhook that you'd like to receive on a per user basis. Generally the best time to initialise your webhook subscriptions is directly after completing the OAuth token exchange on the user's successful completion of the OAuth connection flow.
Verifying a webhook
Webhooks are signed by a private RSA key held by Akahu.
The signature can be found in the X-Akahu-Signature
header (base 64 encoded).
The key ID of the signing key is sent in the X-Akahu-Signing-Key
header.
Using the SDK
If you use a JavaScript backend, the easiest way to verify a webhook is using the Akahu SDK's AkahuClient.webhooks.validateWebhook
method. An example of this can be found in the project readme.
Getting the signing key
In order to verify the signature, you must first retrieve the public key of the RSA keypair used to sign the webhook payload. This can be done by making a GET
request to https://api.akahu.io/v1/keys/{keyID}
. The response payload will contain the public signing key in PEM format.
{
"success": true,
"item": "-----BEGIN RSA PUBLIC KEY-----\n..."
}
Caching
You may cache the signing key (preferably for 24 hours), however whenever the key ID changes, your application must fetch the new key from Akahu, and wipe previously cached keys. This is to ensure that if key rotation happens, your application will not continue to allow webhooks signed with an expired key.
Verification
Once you have the public signing key, compute the RSA-SHA256
signature of the webhook body with PKCS1
padding and ensure it matches the base-64 encoded X-Akahu-Signature
header. Examples using Node.js, Java, C#, and Python are supplied below.
const crypto = require("crypto");
// GET /keys/{keyId}
const publicKey = "-----BEGIN RSA PUBLIC KEY-----\n ...";
// request body as a string (before any parsing)
const body = "{\"webhook_type\":\"PAYMENT\", ...";
// "x-akahu-signature" header
const signature = "KdcB0al ...";
const verify = crypto.createVerify("sha256");
verify.update(body);
verify.end();
const isValid = verify.verify(
publicKey,
signature,
"base64"
);
if (isValid) {
console.log("This webhook is from Akahu!");
} else {
console.log("Invalid webhook caller!");
}
// Code snippet contributed by Christian Mitchell (https://bankroll.co.nz)
// requires BounceCastle https://www.bouncycastle.org/java.html
package com.akahu;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.security.Signature;
import java.util.Base64;
public class Main {
private static RSAPublicKey getRSAPublicKeyFromString(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
String formattedPublicKey = publicKey
.replaceAll("\\n", "")
.replace("-----BEGIN RSA PUBLIC KEY-----", "")
.replace("-----END RSA PUBLIC KEY-----", "");
org.bouncycastle.asn1.pkcs.RSAPublicKey rsaPublicKey = org.bouncycastle.asn1.pkcs.RSAPublicKey.getInstance(Base64.getDecoder().decode(formattedPublicKey));
BigInteger modulus = rsaPublicKey.getModulus();
BigInteger publicExponent = rsaPublicKey.getPublicExponent();
RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, publicExponent);
return (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(keySpec);
}
private static boolean validateSignature(String requestSignature, String requestBody, String publicKey) throws Exception {
RSAPublicKey masterPublicKey = getRSAPublicKeyFromString(publicKey);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(masterPublicKey);
signature.update(requestBody.getBytes());
return signature.verify(Base64.getDecoder().decode(requestSignature.getBytes()));
}
public static void main(String[] args) throws Exception {
// GET /keys/{keyId}
String publicKey = "-----BEGIN RSA PUBLIC KEY-----\n ...";
// request body as a string (before any parsing)
String body = "{\"webhook_type\":\"PAYMENT\", ...";
// "x-akahu-signature" header
String signature = "KdcB0al...";
boolean result = validateSignature(signature, body, publicKey);
if (result) {
System.out.println("This webhook is from Akahu!");
} else {
System.out.println("Invalid webhook caller!");
}
}
}
using System;
using System.Security.Cryptography;
using System.Text;
namespace akahu
{
class Program
{
public static void Main(string[] args)
{
// GET /keys/{keyId}
var publicKey = "-----BEGIN RSA PUBLIC KEY-----\n ...";
// request body as a string (before any parsing)
var body = "{\"webhook_type\":\"PAYMENT\", ...";
// "x-akahu-signature" header
var signature = "KdcB0al...";
var formattedPublicKey = publicKey
.Replace("\\n", "")
.Replace("-----BEGIN RSA PUBLIC KEY-----", "")
.Replace("-----END RSA PUBLIC KEY-----", "");
int bytesRead = 0;
var rsa = RSA.Create();
var pubKeyBytes = Convert.FromBase64String(formattedPublicKey);
rsa.ImportRSAPublicKey(pubKeyBytes, out bytesRead);
var result = rsa.VerifyData(Encoding.UTF8.GetBytes(body), Convert.FromBase64String(signature),
HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
if (result) {
Console.WriteLine("This webhook is from Akahu!");
} else {
Console.WriteLine("Invalid webhook caller!");
}
}
}
}
import json
import base64
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.exceptions import InvalidSignature
def verify_signature(public_key: str, signature: str, request_body: bytes) -> None:
"""Verify that the request body has been signed by Akahu.
Arguments:
public_key -- The PEM formatted public key retrieved from the Akahu API
signature -- The base64 encoded value from the "X-Akahu-Signature" header
request_body -- The raw bytes of the body sent by Akahu
"""
try:
public_key = serialization.load_pem_public_key(public_key.encode('utf-8'))
res = public_key.verify(
base64.b64decode(signature),
request_body,
padding.PKCS1v15(),
hashes.SHA256()
)
print("This webhook is from Akahu!")
except InvalidSignature:
print("Invalid webhook caller!")
To aid in testing this logic, use the below example data to ensure that you accept the valid signature, but reject the invalid one.
{"webhook_type":"ACCOUNT","webhook_code":"UPDATE","state":"example state","item_id":"acc_1111111111111111111111111","updated_fields":["balance"]}
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA1YWQaS5H27EvO3JNOH9nrl9SSSQspFWvoYy/jk9Z/4UhsXPg9S8s
cXKPSVsZb78DXQs8EZDQBHWlVU1VKxtP7fL8EW0bcer0HIuwxKIYMP9IHdmbzOOg
LJC8l2YNn7FqUKE1ltJgLct4UqyTF11jQdKHfhBV9DXtUP9vaFNfFzK1zEwKGggD
sVkwFyna7UoW37l5ynV0BPTaVXZ6sVWoyvxjorcLqjUBCgIcGyHXkAxElsPSBRbE
kydSvePKhe06tn6Ng+PPPJUIKzKMdB3cjKmi5Gsf7JIKRFDoY35oZsoYIRwsgujS
9uFIlDoe0N44XuyXBtLnO2DrJ2yVKkUl/QIDAQAB
-----END RSA PUBLIC KEY-----
FFcDepzALfLqD2Ljua+A1l3eZXHgUpTLWhGQC9OfYeWZX09JwF41F+T/lnKS/P8wP9Ox5eKFU8zhcnjLZ6qJUHgKtUbWUnepynM9bWi6WrkG36sbgsKeg0F0VTkM7SDFy93Vx0rNoJSCt/u87fNpOvEwIn7S7zoVlp5LfwXyispBVM3WpfMs/SDebj2CY3Ir/jqAUmNSTON0rn8+m4My6UKPBAwQCmlHzN4+1zjIJjvWc5Ez78mJUyEfx1qmM1VW2gbWYT3HuVjmGuNrPYQxuIHW6n7q31cKsa/OEVWixxzcUH3MtZvn/LeTMpKg2FmNNfVYUTkd67VxWDj179gm2A==
g+A/e8ud9eDpQNva8RxE7h0Y+8HWIeR+Q6Lefv5R4D8HuPdpBtLgPzgkWPxmQo9mKHYm5iq3apGB95Gu/gFuO8XkVYYx80b0jR8rX6QWWfhBR7MkWIFD1paaKMwXJLfWiqP/3FbMSC7rrE/iOipuZaXRYWW6393jAgtinwzv4OsNYGWNFSeXiTkgcsMDH842t7YvX5GeeT5iT/iQxlflBpkXjmcmAaG2ba2YM/5iU7JjIrvwtZis2Vr196sA+lZKmsp8YnlZ9r++cfaPdAl48GUyHdBxDWt8SM09X7pfdbnMJccExszdR1Mx+aBXRVJs/+3fd2tC5ostDwplrH1CmQ==
Retry logic
Akahu will attempt to deliver your webhook event for up to three days. An attempt will be sent approximately every hour.
If initial delivery fails and you delete your webhook before a retry attempt is made, no retry attempts will be made.
Disable logic
Akahu may disable webhooks if your endpoint fails to return a 200 HTTP status code for multiple events over multiple days.
Event handling
Handling webhook events correctly is critical to ensuring that your apps business logic works correctly.
Akahu can't guarantee delivery of events in the order in which they are generated. For example, making a payment might generate these 2 webhooks.
- payment: status = 'READY'
- payment: status = 'SENT'
If the 'READY' webhook fails due to network connectivity or other reasons outside of our control, it may be retried and delivered after the 'SENT' webhook. Therefore you may want to fetch the whole object from the Akahu API using the item_id
to get up to date data.
What a webhook looks like
Akahu has a hierarchical webhook type structure.
At a high level there is webhook_type
which is the type of object that triggers the webhook (for example a USER
, ACCOUNT
orTRANSACTION
). Within each webhook_type
there are defined webhook_code
s which are the type of event that triggered the webhook (eg. UPDATE
, DELETE
).
All webhooks have the following basic structure:
{
"webhook_type": "<string>",
"webhook_code": "<string>",
"state": "<string>", // If supplied when creating the webhook
}
The types of webhook supported by Akahu are defined below:
TOKEN
Code | Description | Additional Fields |
---|---|---|
DELETE | This User Access Token has been revoked. | item_id The User Access Token. |
ACCOUNT
Code | Description | Additional Fields |
---|---|---|
CREATE | A new account has been connected. | item_id The account ID. |
UPDATE | An account has been updated. The fields that have changed are included in the payload so that recipients can determine if the update is relevant (e.g. an app to help with tax might not care about auto_payments). | item_id The account ID. updated_fields An array of strings. |
DELETE | An account has been deleted. | item_id The account ID. |
WEBHOOK_CANCELLED | This webhook has been cancelled, for example if the user revokes your app. | - |
TRANSACTION
Code | Description | Additional Fields |
---|---|---|
INITIAL_UPDATE | Akahu has retrieved historic transactions for an account after initial connection. NOTE: This webhook will only be sent for accounts newly connected to Akahu. If the user has previously connected accounts to Akahu for use by another app, your app will only ever receive DEFAULT_UPDATE webhooks for those accounts. | item_id The account ID. new_transactions The number of new transactions. new_transaction_ids An array of transaction IDs for the new transactions. |
DEFAULT_UPDATE | Akahu has retrieved new transactions or updated existing transactions. | item_id The account ID. new_transactions The number of new or updated transactions. new_transaction_ids An array of transaction IDs for the new or updated transactions. |
DELETE | One or more transactions have been deleted. | item_id The account ID. removed_transactions An array of transaction IDs. |
WEBHOOK_CANCELLED | This webhook has been cancelled, for example if the user revokes your app. | - |
TRANSFER
Code | Description | Additional Fields |
---|---|---|
UPDATE | A transfer has changed its status. | item_id The transfer ID. status The new status. status_text A description of the status, if present. |
RECEIVED | The funds have arrived at the destination account. | item_id The transfer ID. received_at The time that Akahu identified the funds as received. |
WEBHOOK_CANCELLED | This webhook has been cancelled, for example if the user revokes your app. | - |
PAYMENT
Code | Description | Additional Fields |
---|---|---|
UPDATE | A payment has changed its status. | item_id The payment ID. status The new status. status_code If applicable, the payment Status Code as described here. status_text If applicable, the payment Status Text as described here. |
RECEIVED | The funds have arrived at the destination account. This webhook will only be sent under certain conditions, so we do not recommended implementing core functionality that relies on it. See our Making a Payment guide for more information. | item_id The payment ID. received_at The time that Akahu identified the funds as received. |
WEBHOOK_CANCELLED | This webhook has been cancelled, for example if the user revokes your app. | - |
Updated 9 days ago