Webhooks

How to set up your app for webhooks and best practices

Webhooks are the best way to make your application responsive to new or changing data.

πŸ“˜

Webhooks are only available for full apps.

When you sign up for a full app you will be asked for your webhook url. Your webhook url must be a valid https uri that can accept POST requests. A HTTP 200 status code must be returned by your endpoint within 5 seconds for Akahu to recognise the webhook as delivered. If you want to add or change your app's webhook url in the future, reach out to Akahu support.

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 and ensure it matches the X-Akahu-Signature header. Examples using Node.js, Java and C# 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!");
      }
    }
  }
}

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 = 'INITIATED'
  • payment: status = 'SENT'

If the 'INITATED' 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_codes 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

CodeDescriptionAdditional Fields
DELETEThis User Access Token has been revoked.item_id The User Access Token.

IDENTITY

CodeDescriptionAdditional Fields
CREATE, UPDATE, DELETEA user's identity information has changed. This could be the user's name, an email address, or any other identity information you have access to.item_id The user ID.
WEBHOOK_CANCELLEDThis webhook has been cancelled, for example if the user revokes your app.-

ACCOUNT

CodeDescriptionAdditional Fields
CREATEA new account has been connected.item_id The account ID.
UPDATEAn 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.
DELETEAn account has been deleted.item_id The account ID.
WEBHOOK_CANCELLEDThis webhook has been cancelled, for example if the user revokes your app.-

TRANSACTION

CodeDescriptionAdditional Fields
INITIAL_UPDATEAkahu has retrieved historic transactions for an account after initial connection. By default this is all transactions from the last 90 days. NOTE: This webhook will only be sent for newly connected accounts. If the user has previously connected their accounts to Akahu for use by another app, your app will only ever receive DEFAULT_UPDATE webhooks.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_UPDATEAkahu 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.
DELETEOne or more transactions have been deleted.item_id The account ID. removed_transactions An array of transaction IDs.
WEBHOOK_CANCELLEDThis webhook has been cancelled, for example if the user revokes your app.-

TRANSFER

CodeDescriptionAdditional Fields
UPDATEA transfer has changed its status.item_id The transfer ID. status The new status. status_text A description of the status, if present.
RECEIVEDThe funds have arrived at the destination account.item_id The transfer ID. received_at The time that Akahu identified the funds as received.
WEBHOOK_CANCELLEDThis webhook has been cancelled, for example if the user revokes your app.-

PAYMENT

CodeDescriptionAdditional Fields
UPDATEA payment has changed its status.item_id The payment ID. status The new status. status_text A description of the status, if present.
RECEIVEDThe funds have arrived at the destination account. (This only occurs if the destination account is connected to Akahu)item_id The payment ID. received_at The time that Akahu identified the funds as received.
WEBHOOK_CANCELLEDThis webhook has been cancelled, for example if the user revokes your app.-

Did this page help you?