Receiving webhooks
Your system receives webhooks on an HTTPS endpoint that you register in Merchant Hub. This page covers what the endpoint must support, what the requests look like, how retries work, and how to verify that a request comes from Walley.
Expose an HTTPS endpointβ
To receive webhooks you are required to have an HTTPS endpoint.
Endpoint requirements
- Must accept HTTP POST requests
- Must accept a JSON request body
- Must support HTTPS with TLS 1.2 or 1.3
Endpoint recommendations
- Require basic authentication with a username and password
- Support HTTP2
- Validate the Walley-Signature header with the HMAC key
Respond to webhooksβ
When you receive a webhook request you are required to respond with an HTTP status code in the 2xx series.
Structureβ
The request body is formatted as JSON. Type holds the name of the event and Timestamp is when the event occurred. Payload is different for each event.
{
"Type": string,
"Timestamp": date,
"Payload": object
}
Retriesβ
Webhooks are delivered at least once, so your endpoint must support being called multiple times with the same event without side effects. We retry delivery with increasing backoff time until we receive an HTTP status code in the 2xx series.
The webhook is sent instantly, and if the endpoint does not answer with a 2xx status, a retry will be made after:
- 5 seconds
- 10 seconds
- 3 minutes
- 1 hour
- 4 hours
- 8 hours
- 16 hours
- 24 hours
If the webhook endpoint did not answer with a 2xx status after the last retry (after about 53 hours), we will handle this manually and contact you before retrying again.
Headersβ
These headers are included in every webhook request:
| Key | Description |
|---|---|
| User-Agent | Standard HTTP header. The value will be something like Walleybot 0.1 (+https://dev.walleypay.com). |
| Walley-CorrelationId | The id of the request that triggered the webhook request. If you have issues with any request, provide this value in the support ticket. |
| Walley-Timestamp | UNIX timestamp when this event occurred. Used for HMAC validation, see Verifying the request with HMAC. |
| Walley-Signature | Computed signature from Walley. Used for HMAC validation, see Verifying the request with HMAC. |
| Walley-Live | True or False. Used to know if the request to the webhook is made from Walley's production or a test triggered from the Merchant Hub. True indicates production data. |
Verifying the request with HMACβ
When you set up a webhook, you'll get an HMAC key for it. You can use this key to verify that the requests you receive originate from Walley, using the following method:
- Retrieve the request header
Walley-Timestamp - Concatenate the version number, the timestamp, and the body of the request to form a basestring. Use a semicolon as the delimiter between the three elements. The version number right now is always
v0. Example:v0;{Timestamp};{Body} - Hash the string described above with the HMAC SHA256 implementation of your system's platform, using the HMAC secret for the webhook.
- Compare the computed signature with the request header
Walley-Signature
Examplesβ
- C#
- Node.js
- PHP
static string ComputeHash(string secret, string timestamp, string payload)
{
byte[] bytes = Encoding.UTF8.GetBytes(secret);
HMACSHA256 hmac = new HMACSHA256(bytes);
bytes = Encoding.UTF8.GetBytes($"v0;{timestamp};{payload}");
return Convert.ToHexString(hmac.ComputeHash(bytes)).ToLower();
}
static bool IsHashValid(string secret, string timestamp, string payload, string verify)
{
string hash = ComputeHash(secret, timestamp, payload);
ReadOnlySpan<byte> hashBytes = Convert.FromHexString(hash);
ReadOnlySpan<byte> verifyBytes = Convert.FromHexString(verify);
return CryptographicOperations.FixedTimeEquals(hashBytes, verifyBytes);
}
const crypto = require('crypto');
const computeHash = (secret, timestamp, payload) => {
const hmac = crypto.createHmac('sha256', secret);
hmac.write(`v0;${timestamp};${payload}`);
hmac.end();
return hmac.read().toString('hex');
};
const isHashValid = (secret, timestamp, payload, verify) => {
const computed = Buffer.from(computeHash(secret, timestamp, payload), 'hex');
const received = Buffer.from(verify, 'hex');
return received.length === computed.length && crypto.timingSafeEqual(received, computed);
};
function computeHash($secret, $timestamp, $payload) {
$hmac = hash_hmac('sha256', "v0;$timestamp;$payload", $secret, true);
return bin2hex($hmac);
}
function isHashValid($secret, $timestamp, $payload, $verify) {
$hash = computeHash($secret, $timestamp, $payload);
return hash_equals($hash, strtolower($verify));
}