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, Failed |
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, Failed |
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 | In this instance, the original transaction is first failed (if the transaction is in a pending or sent state) and then the ACH return transaction is created. Two (2) webhook events are generated. If the original transaction was already successful, then only one (1) webhook event is generated and the original transaction remains in a successful state. | Pending, Sent, 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 | A webhook is sent with the transaction information when an RTP Send is initiated from the account. | DECLINED, FAILED, PENDING, SENT, SUCCESS, PENDING_APPROVAL |
RTP Refund | A webhook is sent when an RTP Refund is processed. This doesn't happen often. | Pending, Sent, Success, Declined, Failed |
RTP Receive (Inbound RTP) | A webhook is sent when an inbound RTP payment has been received on the account. | Sent, Success, Declined, Failed |
ACH Reversal | A webhook is sent when an ACH Reversal request is made. | CANCELLED, FAILED, ONHOLD, PENDING, PROCESSING, SENT, SUCCESS |
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:
SHA-256
- the algorithm that was used to hash the request payload and calculate the signatureSignedHeaders=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:
- A string representation of the request needs to be generated using an algorithm described below.
- 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.
- 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 RFC3986QueryStringSortedByParameterName
- 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 theListOfNormalizedHeaders
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:Hash
- hashing of the raw request payload using a hashing algorithm defined in theAuthentication
headerHexEncode
- hex encoding of the bytes generated by theHash
function in step i.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 theBodyHashingAlgorithm
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 theStringToSign
X-Vfi-SignedHeaders
- a list of headers that are being signed, should have the same value as theSignedHeaders
component of theAuthorization
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
Outbound ACH
{
"amount":"1.00",
"account_name": "USD Account",
"account_number": "9999999999",
"client_reference_id": "Ueh484udnlp",
"external_account_number": "123456789",
"created_at":"1639507804049",
"id":"7FFB2IJ03F",
"company_name": null,
"company_id": null,
"sec_code": null,
"effective_entry_date": null,
"fed_trace_id": null,
"victor_trace_id": null,
"transaction_type":"ach_transfer",
"ledger_inherent_balance": "100.00", //only present for ledger accounts
"status":"Pending"
}
Outbound Wire
{
"id":"ZRPO62N2PG",
"amount":"1.99",
"status":"Success",
"created_at":"1678118671617",
"account_name":"Apple Computer", // Originating Account Name
"account_number":"******94", // Originating Account Number
"external_account_number":"*****00", // Destination Account Number
"description":"null",
"transaction_type":"wire_transfer",
"wireIMAD":"20230306GMQFMC01000013",
"wireOMAD":"20230306GMQFMC0100000803061125FT01",
"wireInitDt":"2023-03-06T10:24:26.23",
"wireBenfId":"4567001468583218",
"wireBenfRef":null,
"wireOrignName":null,
"wireOrignToBenfInfoRec":null,
"wireOriginAddress":null,
"wireOriginAddress2":null,
"wireOriginAddress3":null,
"originatorAccountNumber":"*****00", // Originating Account Number
"originatorToBeneficiary1":null,
"originatorToBeneficiary2":null,
"originatorToBeneficiary3":null,
"originatorToBeneficiary4":null,
"sendingBankBic":"51504597",
"sendingBankName":null,
"instructingBankBic":null,
"instructingBankName":null,
"client_reference_id": "dJEhdn343pil",
"originatingBankBic":null,
"originatingBankName":null,
"ledger_inherent_balance": "100.00", //only present for ledger accounts
"completed_at":"1678120204065"
}
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",
"from_account_number":"66600000197",
"id":"5OJBRT809Q",
"ledger_inherent_balance": "100.00", //only present for ledger accounts
"status":"Success"
}
Book External
{
"to_account_number":"66600000191",
"to_account_name":"new uat",
"amount":"1.00",
"from_account_number":"66600000197",
"description":"AAAAAAAA",
"id":"L1Q5HMRIDE",
"transaction_type":"book_external",
"from_account_name":"TestAccountOne",
"ledger_inherent_balance": "100.00", //only present for ledger accounts
"status":"Pending"
}
Inbound ACH
{
"completed_at":"1648669814953",
"amount":"6.00",
"account_name": "USD Account", // Originating Account Name
"account_number": "*******99", // Originating Account Number
"external_account_number": "*****27", // Destination Account Number
"originator_name": "Originator data",
"created_at":"1648669814943",
"id":"VSJDDFR734",
"transaction_type":"ach_inbound_credit",
"status":"Success",
"description":null, // This field is currently null, but will include EFT description in the future
"originator_name":"QA MQ All",
"receiver_name":null,
"effective_entry_date":null,
"entry_description":null,
"individual_id":null,
"sec_code":null,
"company_discretionary_data":null,
"company_id": null,
"company_name": null,
"originating_aba":null,
"transaction_code":null,
"receiver_account_number":null,
"fed_trace_id": null,
"ledger_inherent_balance": "100.00", //only present for ledger accounts
"victor_trace_id": null,
}
Inbound Wire
{
"id":"X2SJFVZ2OX",
"amount":"11900000.00",
"status":"Success",
"created_at":"1678379409586",
"account_name":"Apple Computer", // Destination Account Name
"account_number":"**************02", // Destination Victor Account Number
"external_account_number":null, // Destination JH Account Number
"description":null,
"transaction_type":"wire_inbound",
"wireIMAD":"20230309I1B7033R007606",
"wireOMAD":"20230309GMQFMP010136125",
"wireInitDt":"2023-03-09T10:25:33",
"wireBenfId":"5001001257162802",
"wireBenfRef":"3194",
"wireOrignName":"SOME FICTIONAL COMPANY", // Originating Account Name
"wireOrignToBenfInfoRec":null,
"wireOriginAddress":"PARENT ACCOUNT",
"wireOriginAddress2":"6122 CENTER DR",
"wireOriginAddress3":"LOS ANGELES CA US",
"originatorAccountNumber":"********02", // Originating Account Number
"originatorToBeneficiary1":"FUNDING TO COMPANY A",
"originatorToBeneficiary2":null,
"originatorToBeneficiary3":null,
"originatorToBeneficiary4":null,
"sendingBankBic":"121000248",
"sendingBankName":"WELLS FARGO SF",
"instructingBankBic":null,
"instructingBankName":null,
"originatingBankBic":null,
"originatingBankName":null,
"ledger_inherent_balance": "100.00", //only present for ledger accounts
"completed_at":"1678379410460"
}
ACH Return
{
"return_description":"Insufficient funds",
"completed_at":"1640278802894",
"amount":"1.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",
"company_name": null,
"company_id": null,
"sec_code": null,
"effective_entry_date": null,
"fed_trace_id": null,
"victor_trace_id": null,
"description":"some description",
"ledger_inherent_balance": "100.00", //only present for ledger accounts
"parent_reference_id":null
}
ACH Return Transaction
{
"completed_at":"1640208602712",
"amount":"1.00",
"account_name": "USD Account",
"account_number": "9999999999",
"created_at":"1640208602665",
"id":"GVP1USQRFS",
"transaction_type":"ach_return",
"status":"Success",
"parent_reference_id":"SJ8ECZ9Q98",
"return_code":"R01",
"return_description":"Insufficient funds",
"company_name": null,
"company_id": null,
"sec_code": null,
"effective_entry_date": null,
"fed_trace_id": null,
"victor_trace_id": null,
"ledger_inherent_balance": "100.00", //only present for ledger accounts
"description":"some description"
}
Wire Return
{
"completed_at":"1640208602712",
"amount":"1.00",
"account_name": "USD Account",
"account_number": "9999999999",
"external_account_number": "123456789",
"created_at":"1640208602665",
"id":"GVP1USQRFS",
"transaction_type":"wire_reversal",
"ledger_inherent_balance": "100.00", //only present for ledger accounts
"status":"Success"
}
RTP Send
{
"id":"Q2HV2YAG0B",
"amount":"0.01",
"status":"Sent",
"created_at":"1654118973089",
"account_name":"Test-RTP",
"account_number":"33400052", //Victor Account Number
"external_account_number":"1802000", // Bank Account Number for Sending Account
"description":"RTP Send description",
"transaction_type":"rtp_send",
"ledger_inherent_balance": "100.00", //only present for ledger accounts
"completed_at":"1654118983089"
}
RTP Refund
{
"id":"Q2HV2YAG0B",
"amount":"0.01",
"status":"Sent",
"created_at":"1654118973089",
"account_name":"Test-RTP",
"account_number":"33400052",
"external_account_number":"1802000",
"description":"RTP Refund description",
"transaction_type":"rtp_refund",
"ledger_inherent_balance": "100.00", //only present for ledger accounts
"completed_at":"1654118983089"
}
RTP Inbound
{
"id":"Q2HV2YAG0B",
"amount":"0.01",
"status":"Sent",
"created_at":"1654118973089",
"originator_name":"John Doe",
"originatorAccountNumber":"123456789",
"originator_aba":"051504597",
"account_name":"Test-RTP",
"account_number":"33400052", //Receiving Victor Account Number
"external_account_number":"1802000", //Bank Acount Number for Receiving Account
"description":"Inbound RTP description",
"transaction_type":"rtp_inbound",
"ledger_inherent_balance": "100.00", //only present for ledger accounts
"completed_at":"1654118983089"
}
RTP Request for Payment (RfP) Inbound
{
"victor_payment_request_id": "2EOVVV66RW", // DEPRECATED. Use "id" instead.
"id": "2EOVVV66RW",
"direction": "inbound",
"amount": 0.79,
"original_amount": 0.89,
"status": "Accepted",
"created_at": 1712256800610,
"client_reference_id": "Ueh484udnlp",
"completed_at": 1712256805610,
"account_name": "USD Account",
"account_number": "9999999999", //Victor Account Number
"external_account_number": "123456789", //Bank Acount Number for Receiving Account
"description": "Inbound RFP. Will you accept?",
"due_date": "2025-04-04",
"ledger_inherent_balance": "100.00", //only present for ledger accounts
"victor_payment_request_parent_id": "GVP1USQRFS" //Victor Transaction ID from Outboud (Payer)
}
RTP Request for Payment (RfP) Outbound
{
"victor_payment_request_id": "GVP1USQRFS", // DEPRECATED. Use "id" instead.
"id": "GVP1USQRFS",
"amount": 0.79,
"original_amount": 0.89,
"direction": "outbound",
"status": "Accepted",
"created_at": 1712256800610,
"client_reference_id": "Ueh484udnlp",
"completed_at": 1712256805610,
"account_name": "USD Account",
"account_number": "9999999999", //Victor Account Number
"external_account_number": "123456789", // Bank Account Number for Sending Account
"description": "Outbound RFP. Will you accept?",
"due_date": "2025-04-04",
"ledger_inherent_balance": "100.00", //only present for ledger accounts
"victor_payment_request_child_id": "2EOVVV66RW" //Victor Transaction ID from Inbound (Payee)
}
ACH NOC (Notice of Change)
{
"id":"SO7UV9BCT5",
"effective_date":"201222",
"change_code":"C01",
"return_trace_number":"912211",
"corrected_data":"321270742", // For change codes with multiple updates, the corrected_data will return with space as the delimiter. So for a C03 you'd receive the routing number account number.
"amount":"0.17",
"company_name":"Victor Test",
"company_id":"1119699589",
"company_entry_description":"Sent Test",
"dfi_account_number":"8965159976",
"individual_name":"John Smith",
"original_trace_number":"55211",
"original_receiving_dfi_id":"3110127",
"transaction_type":"ach_noc",
"noc_description":"Incorrect DFI Account Number",
"fed_trace_id": null,
"victor_trace_id": null,
"ledger_inherent_balance": "100.00", //only present for ledger accounts
"victor_counterparty_id": "WRX149NC42"
}
ACH Reversal
{
"account_number": "77700001884",
"amount": "0.50",
"company_id": "1119699589",
"created_at": "1684849530647",
"external_account_number": "1903293",
"description": "incorrect",
"transaction_type": "ach_debit_reversal", //Alternatively, this value can be "ach_reversal" for an ACH Reversal
"parent_reference_id": "LQ3B1CTF4F",
"completed_at": null,
"company_name": "QA Demo",
"account_name": "Primary Transactional Account",
"client_reference_id": null,
"fed_trace_id": null,
"victor_trace_id": null,
"id": "SHHA4VNB33",
"ledger_inherent_balance": "100.00", //only present for ledger accounts
"status": "Sent"
}
Updated about 1 month ago