Webhooks

Webhooks are an incredibly useful and resource-light method to implement event reactions. Webhooks provide a mechanism whereby a server-side application can notifiy a client application when a new event occurs in which a client may be interested.

Webhooks operate on the concept of 'event reaction' and thus avoid the need for constant polling of the server by the client. Leveraging webhooks, client applications are able to subscribe for push notifications when specific actions occur within Victor.

Webhook Configuration

When configuring a webhook on Victor, the callback response will be sent to the provided URL. The webhook can be created and configured either through the Victor Platform UI or via API.

Webhook Events

After configuration of the webhook URL, the account will be subcribed to receive notifications for the following transactional events that can occur both internal and external to Victor:

Transaction Type Description Status
Outbound ACH ACH transaction originating within Victor and being sent to an account at another financial institution Cancelled, Failed, OnHold, Pending, Processing, Sent, Success
Outbound Wire Wire transaction originating within Victor and being sent to an account at another financial institution Failed, Pending, Processing, Sent, Success
Book Internal Transfer Transaction originating within Victor and being sent to an account at the same financial instution, but owned by the same accountholder Pending, Success
Book External Transfer Transaction originating within Victor and being sent to an account at the same financial instution, but owned by a different accountholder Pending, Success
Inbound ACH Transaction ACH Transaction originating outside of Victor and being sent to an account within Victor Pending, Success
Inbound Wire Transaction Wire Transaction originating outside of Victor and being sent to an account within Victor Pending, Success
ACH Return ACH Reversal. In this instance, the original transaction is first failed and then the ACH reversal transaction is created. Two(2) webhook events are generated. Pending, Success
Wire Return A wire is reversed and a webhook event is generated. Note that this works slightly differently than the ACH Return, due to the differences between ACH and Wires. Pending, Success
RTP Send [COMING SOON!] A webhook is sent with the transaction information when an RTP Send is initiated from the account. Pending, Sent, Success, Declined, Failed
RTP Refund [COMING SOON!] A webhook is sent when an RTP Refund is processed. This doesn't happen often. Pending, Sent, Success, Declined, Failed
RTP Receive (Inbound RTP) [COMING SOON!] A webhook is sent when an inbound RTP payment has been received on the account. Sent, Success, Declined, Failed

💡

Info

When creating the webhook, the json parameter can be used to specify that the response should be returned as a JSON object. By default, webhooks payloads are URL-encoded.

As of 26 AUG 2022, URL-encoded webhooks are being deprecated. Continued development will cease and URL-encoded webhooks will no longer be available as of 1 DEC 2022.

Webhook Signatures

To ensure that data sent using the webhook was not manipulated or malformed along the way to the configured endpoint, each request is signed. The following section covers required steps that are required to verify signatures.

Whenever a webhook is configured, we generate ECDSA keys and share the Public Key in the response. Before publishing events to the configured endpoint, we timestamp and sign the request and create a signature that can be verified using the Public Key.

The signature is being provided with the request using the HTTP Authorization header. Below is an example of the Authorization header that contains webhook signing information:

Authorization: SHA-256, SignedHeaders=content-type;host;x-vfi-timestamp, Signature=MIGHAkE7lIv01Vf2hjGDjXCAKF60KGPfUKgER13rKzKPY0qkNBah8aZhuIhg8c5NrsxzUu1GAkhONde2fww9j/QBMkJ4NgJCAPBYou7T67/

The Authorization header is composed of the following components (split into multiple lines for readability):

Authorization: <hashing algorithm>, 
SignedHeaders=<headers that are part of the signing string>, 
Signature=<base64 encoded signature>

From the example:

  1. SHA-256 - the algorithm that was used to hash the request payload and calculate the signature
  2. SignedHeaders=content-type;host;x-vfi-timestamp - headers that were signed that form the integral part of the request
    3.Signature=MIGHAkE7lIv01Vf2hjGDjXCAKF60KGPfUKgER13rKzKPY0qkNBah8aZhuIhg8c5NrsxzUu1GAkhONde2fww9j/QBMkJ4NgJCAPBYou7T67/ - Base64 encoded representation of the signature.

In order to correctly validate the integrity of the webhook, the following steps need to be taken:

  1. A string representation of the request needs to be generated using an algorithm described below.
  2. A signing string needs to be generated from the hashing algorithm, the shared timestamp and a hex encoded hash of the string generated in step 1.
  3. The signing string will become the payload for the verification, and will be used alongside the signature to be verified using the public key provided when the webhook was set up.

Creating the string form of a request

The following pseudocode defines the structure of the string form of the request used for signing:

RequestString=
  HTTPMethod + '\n' +
  RequestUriPathFragment + '\n' +
  QueryStringSortedByParameterName + '\n' +
  ListOfNormalizedHeaders + '\n' +
  SignedHeaders + '\n' +
  Lowercase(HexEncode(Hash(RequestPayload))
  • HTTPMethod - the HTTP method being used for the request in upper case e.g.: POST
  • RequestUriPathFragment - the path component according to RFC3986
  • QueryStringSortedByParameterName - the query string parameters, sorted using natural order with URL encoded values. If multiple params with the same name are provided, they are then sorted according to their values using natural ordering. Example:
?queryParam1=1&queryParam2=split%20text&queryParam2=abc&QueryParam=test

Would become:

QueryParam=test&queryParam1=1&queryParam2=abc&queryParam2=split%20text
  • ListOfNormalizedHeaders - a list of normalized header names with values that are split using a newline character. Header names are first normalized to a lowercase format, and they are sorted by the normalized header name. Value of the header needs to be trimmed i.e. leading and trailing spaces must be removed.

Example:

Host: api.victorfi.com
Content-Type: application/json; charset=utf-8
X-Vfi-Timestamp: 2022-05-02T15:18:01Z

would become:

content-type:application/json; charset=utf-8
host:api.victorfi.com
x-vfi-timestamp:2022-05-02T15:18:01Z
  • SignedHeaders - is a ; separated, sorted list of normalized header names that were used to build the ListOfNormalizedHeaders element in the previous step. The sorting should be done using natural ordering.

Example:

content-type;host;x-vfi-timestamp
  • Lowercase(HexEncode(Hash(RequestPayload)) - this operation consists of 3 steps:
    1. Hash - hashing of the raw request payload using a hashing algorithm defined in the Authentication header
    2. HexEncode - hex encoding of the bytes generated by the Hash function in step i.
    3. Lowercase - changing all characters to lowercase.

Example:

ba7c649d5def497c2c74b824787217d2b5219397ce8fa666a645f0e2fa1b7145

Below is an example of a full string representation of a request that combines all elements described above:

POST
/webhooks
QueryParam=test&queryParam1=1&queryParam2=abc&queryParam2=split%20text
content-type:application/json; charset=utf-8
host:api.victorfi.com
x-vfi-timestamp:2022-05-02T15:18:01Z
content-type;host;x-vfi-timestamp
ba7c649d5def497c2c74b824787217d2b5219397ce8fa666a645f0e2fa1b7145

Creating the signing string

The string that is used to generate the signature can be built using the following pseudocode:

StringToSign =
    BodyHashingAlgorithm + \n +
    IsoDateTimeStringTruncatedToSeconds + \n +
    Lowercase(Hash(RequestString))
  • BodyHashingAlgorithm - the algorithm used for hashing of the request body and the string representation of the request, e.g. SHA-256
  • IsoDateTimeStringTruncatedToSeconds - the timestamp of the notification in ISO 8601 format, e.g. 2022-05-02T13:00:41Z
  • Lowercase(HexEncode(Hash(RequestString))) - the request string represented as a lowercase hex encoded hash using the algorithm defined in the BodyHashingAlgorithm line.

Example:

SHA-256
2022-05-02T15:18:01Z
05f63076feb7ee2490f2c0c2d5d9d3b5b069c7f3e4e20ed084403798058901e2

The StringToSign is then used to generate a SHA256withECDSA signature which is Base64 encoded. This signature is then put into the Authorization header in the Signature component.

To verify the integrity of the payload, the receiver needs to build the RequestString to be able to build the StringToSign that becomes the payload for the verification method.

Alongside the Authorization header, the following headers will be sent in the request:

  • X-Vfi-Timestamp - the ISO8601 UTC timestamp truncated to seconds, that is needed to build the StringToSign
  • X-Vfi-SignedHeaders - a list of headers that are being signed, should have the same value as the SignedHeaders component of the Authorization header

The following Java snippet shows an example of verification of a webhook signature:

String  buildRequestString(HttpServletRequest request) {
  String authHeader = request.getHeader("Authorization");
  String signedHeaders = getSignedHeaders(authHeader);
  String timestamp = request.getHeader("X-Vfi-Timestamp");
  // readSignedHeaders from Authorization header
  //  create a String representation of the request following the pseudocode
  // RequestString=
  //  HTTPMethod + '\n' +
  //  RequestUriPathFragment + '\n' +
  //  QueryStringSortedByParameterName + '\n' +
  //  ListOfNormalizedHeaders + '\n' +
  //  SignedHeaders + '\n' +
  //  Lowercase(HexEncode(Hash(RequestPayload))
}


String buildStringToSign(String hashingAlgorithm, String timestamp, String requestString) {
        return String.join(LINE_SEPARATOR, hashingAlgorithm, timestamp, lowercase(hexEncode(hash(requestString, hashingAlgorithm))));
};

String lowercase(String someString) {
  return someString.toLowerCase(Locale.ROOT);
}

byte[] hash(String stringToHash, String hashingAlgorithm) { ... }

String hexEncode(byte[] bytes) { ... }

String getAlgorithm(String authHeaderContent) { ... }

List<String> getSignedHeaders(String authHeaderContent) { ... }

String getSignature(String authHeaderContent) { ... }

void verify(HttpServletRequest request) throws Exception {
        String authHeader = request.getHeader("Authorization");
        String hashingAlgorithm = getAlgorithm(authHeader);
        String timestamp = request.getHeader("X-Vfi-Timestamp");
        String requestString = buildRequestString(request);
        String signature = getSignature(authHeader);
        String signingString = buildStringToSign(hashingAlgorithm, timestamp, requestString);
        Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA"));
        EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode("your public key goes here"));

        KeyFactory keyFactory = KeyFactory.getInstance("EC");
        PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
        ecdsaVerify.initVerify(publicKey);
        ecdsaVerify.update(signingString.getBytes("UTF-8"));
        boolean result = ecdsaVerify.verify(Base64.getDecoder().decode(signature)); //Must return true
}

🚧

Character encoding

Whenever encoding needs to be specified when doing an operation like hashing or getting bytes from strings, the encoding that should be used is UTF-8.

Sample Webhook Payloads (URL-encoded - DEPRECATED 26 AUG 2022)

Outbound ACH (DEPRECATED)

amount=1.00&fee_amount=0.00&account_name=USD+Account&
account_number=9999999999&external_account_number=123456789&
created_at=1639507804049&id=7FFB2IJ03F&transaction_type=ach_transfer&
status=Pending

Outbound Wire (DEPRECATED)

id=Q2HV2YAG0B&amount=0.01&fee_amount=0.00&status=Sent&
created_at=1654118973089&account_name=Test+RTP&
account_number=33400052&external_account_number=1802000&
description=AAAAAAA&transaction_type=wire_transfer&
wireIMAD=20220601MMQFMCLY000001&wireAmt=0.01&
wireInitDt=Jun+01%2C2022+00%3A00%3A00&wireBenfId=1801000&
wireBenfRef=Q2HV2YAG0B&wireOrignName=Test+Automation&
wireOrignToBenfInfoRec=20220601MMQFMCLY000001

Book Internal (DEPRECATED)

to_account_number=66600000200&to_account_name=TestAccountFour&
amount=1.00&created_at=1631556555133&
description=Test+Automation+Book+Internal&transaction_type=book_internal&
from_account_name=TestAccountOne&completed_at=1631556555215&
fee_amount=0.00&from_account_number=66600000197&
id=H5XWX7U6IA&status=Success

Book External (DEPRECATED)

to_account_number=66600000191&to_account_name=new+uat&amount=1.00&
created_at=1631556552182&description=Test+Automation+Book+External&
transaction_type=book_external&from_account_name=TestAccountOne&
completed_at=1631556552263&fee_amount=0.00&
from_account_number=66600000197&id=9HBAMQ3F0H&status=Success

Inbound ACH (DEPRECATED)

completed_at=1648669814953&amount=6.00&account_name=USD+Account&
account_number=999999999&external_account_number=1903327&
fee_amount=0.00&created_at=1648669814943&id=VSJDDFR734&
transaction_type=ach_inbound_credit&status=Success

Inbound Wire (DEPRECATED)

id=3O5XN2GPRT&amount=10.06&fee_amount=0.00&status=Success&
created_at=1652894466719&account_name=Global+Technologies&
account_number=9999999999999&transaction_type=wire_inbound&
wireIMAD=20220518B6B7HU3R010612&wireOMAD=20220518MMQFMPLY000368&
wireAmt=10.06&wireInitDt=May+18%2C2022+00%3A00%3A00&
wireBenfId=5999346544235302&wireBenfRef=388288884&
wireOrignName=BOB+SMITH&wireOrignToBenfInfoRec=20220518B6B7HU3R010612&
completed_at=1652894466731

ACH Return (original transaction first transitions to failed status) (DEPRECATED)

return_description=Insufficient%20funds&completed_at=1640278802894&
amount=1.00&fee_amount=0.00&account_name=USD+Account&
account_number=9999999999&external_account_number=123456789&
created_at=1640114617519&id=SJ8ECZ9Q98&transaction_type=ach_transfer&
return_code=R01&status=Failed&description=null

ACH Reversal Transaction (DEPRECATED)

completed_at=1640208602712&amount=1.00&fee_amount=0.00&account_name=USD+Account&
account_number=9999999999&created_at=1640208602665&
id=GVP1USQRFS&transaction_type=ach_reversal&status=Success&parent_reference_id=SJ8ECZ9Q98&
return_code=R01&return_description=Insufficient%20funds&description=null

Wire Return (DEPRECATED)

completed_at=1640208602712&amount=1.00&fee_amount=0.00&account_name=USD+Account&
account_number=9999999999&external_account_number=123456789&created_at=1640208602665&
id=GVP1USQRFS&transaction_type=wire_reversal&status=Success

Sample Webhook Payloads (JSON)

Outbound ACH

{
    "amount":"1.00",
    "fee_amount":"0.00",
    "account_name": "USD Account",
    "account_number": "9999999999",
    "external_account_number": "123456789",
    "created_at":"1639507804049*",
    "id":"7FFB2IJ03F",
    "transaction_type":"ach_transfer",
    "status":"Pending"
}

Outbound Wire

{
    "id":"Q2HV2YAG0B",
    "amount":"0.01",
    "fee_amount":"0.00",
    "status":"Sent",
    "created_at":"1654118973089",
    "account_name":"Test+RTP",
     "account_number":"33400052",
     "external_account_number":"1802000",
     "description":"AAAAAAA",
     "transaction_type":"wire_transfer",
     "wireIMAD":"20220601MMQFMCLY000001",
     "wireAmt":"0.01",
     "wireInitDt":"May 18,2022 00:00:00",
     "wireBenfId":"1801000",
     "wireBenfRef":"Q2HV2YAG0B",
     "wireOrignName":"Test Automation",
     "wireOrignToBenfInfoRec":"20220601MMQFMCLY000001"
}

Book Internal

{
    "to_account_number":"2570007",
    "to_account_name":"TestAccountThree",
    "amount":"1.00",
    "created_at":"1639507823089",
    "description":"AAAAAAAA",
    "transaction_type":"book_internal",
    "from_account_name":"TestAccountOne",
    "completed_at":"1639507823147",
    "fee_amount":"0.00",
    "from_account_number":"66600000197",
    "id":"5OJBRT809Q",
    "status":"Success"
}

Book External

{
    "to_account_number":"66600000191",
    "to_account_name":"new uat",
    "amount":"1.00",
    "fee_amount":"0.00",
    "from_account_number":"66600000197",
    "description":"AAAAAAAA",
    "id":"L1Q5HMRIDE",
    "transaction_type":"book_external",
    "from_account_name":"TestAccountOne",
    "status":"Pending"
}

Inbound ACH

{
   "completed_at":"1648669814953",
   "amount":"6.00",
   "account_name": "USD Account",
   "account_number": "999999999",
   "external_account_number": "1903327",
   "fee_amount":"0.00",
   "created_at":"1648669814943",
   "id":"VSJDDFR734",
   "transaction_type":"ach_inbound_credit",
   "status":"Success"
}

Inbound Wire

{
    "id":"NQB0ASK5CM",
    "amount":"10.34",
    "fee_amount":"0.00",
    "status":"Success",
    "created_at":"1652899614554",
    "account_name":"Global Technologies",
    "account_number":"************5302",
    "description":null,
    "transaction_type":"wire_inbound",
    "wireIMAD":"20220518B6B7HU2R012993",
    "wireOMAD":"20220518MMQFMPLY000484",
    "wireAmt":"10.34",
    "wireInitDt":"May 18,2022 00:00:00",
    "wireBenfId":"5999346544235302",
    "wireBenfRef":"388305486",
    "wireOrignName":"BOB SMITH",
    "completed_at":"1652899614567"
}

ACH Return (original transaction first transitions to failed status if in Pending or Sent state)

{
    "return_description":"Insufficient funds",
    "completed_at":"1640278802894",
    "amount":"1.00",
    "fee_amount":"0.00",
    "account_name": "USD Account",
    "account_number": "9999999999",
    "external_account_number": "123456789",
    "created_at":"1640114617519",
    "id":"SJ8ECZ9Q98",
    "transaction_type":"ach_transfer",
    "return_code":"R01",
    "status":"Failed",
    "description":"some description",
    "parent_reference_id":null
}

ACH Return Transaction

{
    "completed_at":"1640208602712",
    "amount":"1.00",
    "fee_amount":"0.00",
    "account_name": "USD Account",
    "account_number": "9999999999",
    "created_at":"1640208602665",
    "id":"GVP1USQRFS",
    "transaction_type":"ach_reversal",
    "status":"Success",
    "parent_reference_id":"SJ8ECZ9Q98",
    "return_code":"R01",
    "return_description":"Insufficient funds",
    "description":"some description"
}

Wire Return

{
    "completed_at":"1640208602712",
    "amount":"1.00",
    "fee_amount":"0.00",
    "account_name": "USD Account",
    "account_number": "9999999999",
    "external_account_number": "123456789",
    "created_at":"1640208602665",
    "id":"GVP1USQRFS",
    "transaction_type":"wire_reversal",
    "status":"Success"
}

RTP Send [COMING SOON!]

{
    "id":"Q2HV2YAG0B",
    "amount":"0.01",
    "fee_amount":"0.00",
    "status":"Sent",
    "created_at":"1654118973089",
    "account_name":"Test+RTP",
    "account_number":"33400052",
    "external_account_number":"1802000",
    "description":"AAAAAAA",
    "transaction_type":"rtp_send",
    "completed_at":"1654118983089"
}

RTP Refund [COMING SOON!]

{
    "id":"Q2HV2YAG0B",
    "amount":"0.01",
    "fee_amount":"0.00",
    "status":"Sent",
    "created_at":"1654118973089",
    "account_name":"Test+RTP",
    "account_number":"33400052",
    "external_account_number":"1802000",
    "description":"AAAAAAA",
    "transaction_type":"rtp_refund",
    "completed_at":"1654118983089"
}

RTP Inbound [COMING SOON!]

{  
    "id":"Q2HV2YAG0B",  
    "amount":"0.01",  
    "fee_amount":"0.00",  
    "status":"Sent",  
    "created_at":"1654118973089",  
    "account_name":"Test+RTP",  
    "account_number":"33400052",  
    "external_account_number":"1802000",  
    "description":"AAAAAAA",  
    "transaction_type":"rtp_inbound",  
    "completed_at":"1654118983089"  
}