Webhook notification
Webhook notification is a REST PUT call made to the backend system of the merchant at the URL specified in merchant's application configuration. It is sent for two final statuses of the payment: COMPLETED
or FAILED
.
Delivery rules
Volume Webhook will be sent until we receive a successful 200
response. In all other cases, we will treat it as a failure, and send it again.
Payload
When implementing data serialization in software applications, it is crucial to configure the serialization process to ignore unknown properties. This practice ensures that any unexpected data fields in the serialized input do not cause errors or application crashes.
{
"paymentId": "d2799a79-bd76-4ba4-93b9-2f90bf2a1933",
"merchantPaymentId": "d2799a79-bd76-4ba4-a3b9-2f90bf2a1934",
"paymentStatus": "COMPLETED",
"errorDescription": null,
"paymentRequest": {
"amount": 0.1,
"currency": "GBP",
"reference": "payment 2022-06-22/123"
},
"paymentMetadata": {
"mydata": "myvalue"
},
"applicationId": "a2799a79-bd76-4ba4-93b9-2f90bf2a1912"
}
field | type | description |
---|---|---|
paymentId | string (UUID) | payment id |
merchantPaymentId | string (UUID) [optional] | payment id, if specified by the merchant when payment is initiated |
paymentStatus | string | payment status : COMPLETED or FAILED |
errorDescription | string [optional] | failure description in case of paymentStatus is FAILED |
paymentRequest:amount | number | payment amount |
paymentRequest:currency | string | payment currency |
paymentRequest:reference | string [optional] | payment reference |
paymentMetadata | string [optional] | payment metadata, if specified by the merchant when payment is initiated |
applicationId | string (UUID) | application id |
Security
Request signature verification
All Volume webhook notifications come equipped with an Authorization
header containing a digital signature of the payload sent in the request. It is signed with a Volume private key hence it can be verified using Volumes public key.
It is highly recommended verifying the signature as it provides two main benefits:
-
as it can be verified only using Volumes public key it proofs that Volume is the origin of the message
-
it proofs that the message payload was not altered, and it is exactly what Volume sent
Implementation
There are multiple options of implementing the signature verifying mechanism, but we recommend going through example implementations provided by Volume. All examples contain a sample web server which can consume a webhook notification and a service responsible for fetching Volumes public key and verifying the signature.
Generic description
Volume webhook security is done by taking the json string which will be sent as the payload of the message, digitally signing it with Volumes private key and sending it as an Authorization
header in form of:
Authorization: {algorithm_used} {signature}
Example:
SHA256withRSA psQ647Ru7aa43dtyj0nAgFcvKq7KuDL6qE9SjNAdMH+hqJ4md7gFqW6kaDIyyixoUjcMArRjr6l1MORvLCPUX8FeCRbG4/Igz48zNDy4n7oOrGrk2CpInHi7QeMof1XQT0GnXo3qqYkaZyxNaHdXS+MHfaFDsyKVBDereKqdPg+gPdoe8Ons1qpTaDdeJe5LQW8d4Gq2IZaEIgQSG9hcWdQEL69Pq9UrP/Agx1XZIToX2RIKZPCh4DNP5oJlXM+7ysBM0g/xvCsYK6ikBGFyNEyDOEHXKvkiqyJNkQiV6Cs4ZWBoswlQ6B+bJ5EvU2Mn2sVMDF3SSVIAFSJFBrfV3g==
To verify the signature, consume the original, unmodified payload sent from Volume and verify it using Volumes public key located here:
- sandbox
https://api.sandbox.volumepay.io/.well-known/signature/pem
- live
https://api.volumepay.io/.well-known/signature/pem
This public key exposed above is formatted in PKCS#8 .pem file format with trimmed pre- and postfixes:
-----BEGIN|END PUBLIC KEY-----
Adding static IP for additional security
To improve the security of your webhook integration, it's advisable to add our static IP address to IP whitelist in your firewall settings or within your application configuration.
Reasons for Adding the Static IP:
-
Identity Verification: The static IP serves as an additional layer of verification, ensuring that incoming webhooks originate from our authorized server.
-
Protection Against Spoofing: By whitelisting our static IP, you minimize the risk of receiving fake or unauthorized webhook requests from malicious sources.
Static IP Addresses for Different Environments:
Sandbox Environment (Testing): For testing purposes, you can use the static IP address:
52.30.246.188
This IP corresponds to our sandbox account and allows you to simulate webhook interactions in a controlled environment.
Live Environment (Production): When deploying your application in a live environment, it is essential to use the static IP address:
52.56.123.234
18.175.86.214
3.11.7.150
This ensures that your production environment only accepts legitimate webhook requests from our verified server. Use it for your firewall rules or application configuration.
IDEMPOTENCY
Webhook consumer must be able to successfully handle more than one webhook call for the same payment.
Payments may be recognised always by the paymentId and by merchantPaymentId, if you use unique merchantPaymentId per payment.
If your webhook consumer implementation will successfully handle a webhook, but we will not receive a response, we will send it again. So your endpoint must be ready to accept this payload multiple times - according to the delivery rules.
WARNING!
Remember not to alter the payload of the message before passing it to the verification function. This also includes automatic changes done by the deserialisation mechanisms. Some Dto implementations can produce a different byte array, hence failing the validation. A good example of a problem which can occur by using an Enum instead of a string is implemented here: ApiController.cs#L65 (opens in a new tab)
A complex Dto with a transfer status deserialised to an Enum produces a different byte representation. There is no one easy solution to this problem, as different languages can produce different results (f.ex. Java allows enum usage and does not change the byte array). This is why we recommend consuming either a byte array or the json string exactly as it was sent by Volume(no additional line break or text formatting) and then deserialising it to Dto programmatically: ApiController.cs#L34 (opens in a new tab)
Examples
1. ASP .NET C#
Github repository: https://github.com/getvolume/VolumeWebhookDotNetConsumer (opens in a new tab)
Key points:
-
Service which is responsible for fetching public key and verifying the signature: SignatureService.cs (opens in a new tab)
-
Controller with sample methods: ApiController.cs (opens in a new tab)
-
Url to the Volume public key: appsettings.json#L8 (opens in a new tab)
-
A Postman collection with sample calls: VolumeWebhookSampe.postman_collection.json (opens in a new tab)
We recommend importing the Postman
collection mentioned above and as it already contains a proper url, port and sample data.
After starting the project a useful Swagger UI
will also be available under:
https://localhost:7142/swagger/index.html (opens in a new tab)
The most important part of the project is the SignatureService. It contains two method which differ only by the first argument:
-
bool ValidateSignature(byte[] data, string signatureHeader);
-
bool ValidateSignature(string json, string signatureHeader);
First takes the original bytes sent by Volume in the message payload. The other takes a JSON string representation of the payload.
Propositions
Naturally a good candidate to implement this kind of verification function is an interceptor or a filter. But this depends on the technology used.
2. Java, Spring
Github repository: https://github.com/getvolume/VolumeWebhookJavaConsumer (opens in a new tab)
Key points:
-
Service which is responsible for fetching public key and verifying the signature: SignatureService.java (opens in a new tab)
-
Controller with sample methods: WebhookController.java (opens in a new tab)
-
Url to the Volume public key: application.properties#L1 (opens in a new tab)
-
A Postman collection with sample calls: VolumeWebhookSampe.postman_collection.json (opens in a new tab)
The most important part of the project is the SignatureService (opens in a new tab). It contains two method which differ only byt the first argument:
-
public boolean verify(byte[] data, String signature)
-
public boolean verify(String jsonString, String signature)
First takes the original bytes sent by Volume in the message payload and the other takes a JSON string representation of the payload.
2. Node.js, Express
Github repository: https://github.com/getvolume/volume-webhook-node-consumer (opens in a new tab)
Key points:
-
Service which is responsible for fetching the public key and verifying the signature: validationService.js (opens in a new tab)
-
Controller with a sample method: app.js (opens in a new tab)
-
Url to the Volume public key: .env (opens in a new tab)
If using the LIVE environment change the public key URL in the .env file, to a correct one according to the implementation paragraph
Testing
In the course of implementing the webhook endpoint, it might be helpful to utilize a CURL statement.
The examples below include a header featuring a digital signature generated using the Volume private key within the SANDBOX environment.
Successful payment scenario
curl --request PUT 'ENDPOINT_URL' \
--header 'Authorization: SHA256withRSA hnHI6qoo7p37NwtBFj332TWC9UUHFiMlwgKsI2XV+L1xKbIK4Vp+3b3bczrdM+8bLXNTRMvJJJ+5zr5uBXBhl9enN3Sfq/4q3gmdq1pGd0Gz0YaRUZxhNG2tkVq7LGtKeeWzg5PxfCy7PeD3D71C+SnUYa7fwT+KzKyPCMqk+uWjLws6pKysinOzh3aYmVhaW9DhH6gZtV2LLGQFHUsqtYClzOkQRxDePhJU8kf8tu8FyTYxJgN4+CZ7vXrD162L0zrcsHXZX1VvVS0GbguHz/JHIFRzqu+o3QpHoidnU+reXPoCQOBV420NaWwVy3Op5o3rFSAZvSwjwAczoQRfnw==' \
--header 'Content-Type: application/json' \
--header 'Accept: */*' \
--data '{"paymentId":"3f2a2b69-6d42-4050-9c4f-7e8849bf683c","merchantPaymentId":"806","paymentStatus":"COMPLETED","errorDescription":null,"paymentRequest":{"amount":24.23,"currency":"GBP","reference":"payment-reference"},"paymentRefundData":null,"paymentMetadata":{"some-data":"some-value"}}'
Failed payment scenario
curl --request PUT 'ENDPOINT_URL' \
--header 'Authorization: SHA256withRSA Th+qWdYLLh436/L0pZOgFgjfNa8jcLE5VTMM4IYEQz1yVkljudt0XMgShhWjqhy2+f0puV+FXfh3PWP3DMAV8FlgYdciyPpLqijy5Ruo2ALz0LPgunT/o6Y+7NAfWCnVDfWT17yqokeN+70QCX/Waq+2Ox8nu8a7bJVj6noiiMUfq5pLKKiQMqb1t7ebznrKGGvt0IUdQzIxfFQz/lT4V5Oar4lQZ3hNhjm/Rmde8ctJ3g2sVuY6Mqt5OiUZzWQl/zqEl5OR4zlzTrxCzlqyep9Yu5E3TRgz1J82gZMeZVFY7yxt/wj5Fre/zK6SWJmpShbqzK5fJ5D++4CS8nY+GA==' \
--header 'Content-Type: application/json' \
--header 'Accept: */*' \
--data '{"paymentId":"183b5eee-0fbf-4863-b55a-7a72af84db1a","merchantPaymentId":"937","paymentStatus":"FAILED","errorDescription":"Payment was rejected","paymentRequest":{"amount":24.23,"currency":"GBP","reference":"payment-reference"},"paymentRefundData":null,"paymentMetadata":{"some-data":"some-value"}}'
parameters | description |
---|---|
ENDPOINT_URL | URL of the endpoint under implementation. |
Authorization | Header containing the digital signature of this exact payload. |
data | Example of a webhook payload sent for a payment. |