Accepting webhook requests
How do you accept and handle webhook events.
Configuring a Callback URL
The webhook callback URL used for an event subscription needs to be configured on the your system to accept an HTTPS POST
request in JSON format. After verifying the request signature provided in the headers, the preconfigured endpoint needs to return a 200
(OK) or 201
(created) HTTP response code to the Webhooks API request made using the bond.tech
domain. All webhook requests come from this domain which must be whitelisted on your system for further authentication.
Webhook authentication
Verify the webhook signature before handling the request.
Webhook event examples
{
"event": "kyc.verification.success",
"customer_id": "931e2341-c3eb-4681-97d4-f6e09d90da14",
"occurred_at": "2021-10-20T10:27:20.154286+00:00"
}
{
"event": "kyc.verification.failure",
"customer_id": "931e2341-c3eb-4681-97d4-f6e09d90da14",
"occurred_at": "2021-10-20T10:35:34.781911+00:00"
}
Signature Verification
The request from the Webhooks API to the developer's system also contains the Bond-Signature
header used for verifying the request. The request should only be processed if the signature can be verified. This header takes the form of Bond-Signature: t={timestamp},v1={version 1 digest},v2={new version 2 digest}
and contains:
- timestamp
t
formed at the time of the request - signature digest of fixed version
v1
- signature digest of the new version
v2
The following is an example of this header.
{
'Bond-Signature':
't=1598980451,v1=3095c22f29d051e548cffd90c899369985f6e2b6,v2=536e828135322f29d051e548cffd90c69985f'
}
The signature digest is generated using keyed-hashing for message authentication HMAC, using the SHA-256 hash algorithm. For details, see the implementation of the hmac module in Python's standard library or others in the code snippets below. You should calculate and verify the signature digest before processing the request.
The signature digest is calculated by encoding the webhook secret
provided during event subscription, the timestamp t
found in the header, and the event payload
, all as byte strings using UTF-8 encoding. The procedure for doing this is as follows:
- Encode the webhook
secret
. - Encode the concatenation of the timestamp
t
as a string, the character.
(period), and:v1
: the event JSON payload (the request body) as a stringv2
: the raw request BODY with no modifications
- Compute an HMAC using SHA-256 from the encoded webhook secret and encoded string from step 2.
- Compare this calculated signature digest with
v1
orv2
from the header, depending on whether you used JSON or the raw requestBODY
.
If the computed digest matches the provided digest, then the request can be processed.
For code examples of signature verification, see Signature verification examples. Note that the v1
signature is prepared using python
style JSON parsing which may result in different strings in other languages and parsing strategies. We now recommend using a v2
digest based on the raw BODY
which is unambiguous across languages and server frameworks.
Event Handling
The request from the Bond platform to the your system contains the event subscription as the body. This provides all the relevant information concerning this event. The event enum label is also shown, as seen in the example below.
{
"event": "event.label",
"customer_id": "customer_uuid",
"verification": "verification_status",
"occurred_at": "timestamp",
}
Events should be processed and recorded following your business logic. In rare cases, the event may be received more than once, so event processing should be idempotent. One way to accomplish this, is to not process any events that have already been received and logged.
Handling duplicate webhooks
Use the combination of
occurred_at
andevent
to match a webhook that your server receives with one that you have already processed to identify a possible duplicate.
Error handling
If a 200
or 201
response code is not received, the Webhooks API retries the request using exponential backoff for up to three days. This adds up to approximately eight retry attempts until a successful response is returned.
If you see that your server is not responding with 200
or 201
for webhooks, check which events are not being accepted by your server.
Signature verification examples
import (
"strings"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func validate_signature_v2(body string, timestamp int, secret string, signature string) bool {
digest := hmac.New(sha256.New, []byte(secret))
digest.Write([]byte(fmt.Sprintf("%d.%s", timestamp, body)))
sigmac, _ := hex.DecodeString(signature)
return hmac.Equal(digest.Sum(nil), sigmac)
}
# retrieving values from header stored in "sigheader"
values := make(map[string]string)
for _, kvp := range strings.Split(sigheader, ",") {
data := strings.Split(kvp, "=")
values[data[0]] = data[1]
} # values has keys t, v1, v2
function validate_signature_v2(body, timestamp, secret, signature) {
const text = `${timestamp}.${body}`;
const digest = Buffer.from(
crypto.createHmac('sha256', secret).update(text).digest('hex'), "utf8"
);
return crypto.timingSafeEqual(digest, Buffer.from(signature, 'utf8'));
}
// retrieving values from header stored in "sigheader"
let values = Object.fromEntries(
sigheader.split(",").map((kvp) => (kvp.split("=")))
); // values has keys t, v1, v2
def validate_signature_v2(
body: Union[str, bytes], timestamp: int, secret: str, signature: str
) -> bool:
to_digest = f"{timestamp}.".encode(encoding="UTF-8")
if isinstance(body, str):
to_digest += body.encode(encoding="UTF-8")
else:
to_digest += body
digest = hmac.new(
secret.encode(encoding="UTF-8"),
to_digest,
"sha256",
).hexdigest()
return hmac.compare_digest(digest, signature)
# retrieving values from header stored in "sigheader"
values = {kvp[0]: kvp[1] for kvp in [kvp.split("=") for kvp in sigheader.split(",")]}
# values has keys t, v1, v2
def validate_signature_v2(body, timestamp, secret, signature)
digest = OpenSSL::Digest.new('sha256')
message = timestamp + "." + body
return OpenSSL::HMAC.hexdigest(digest, secret, message) == signature
# timing attack safe?
end
# retrieving values from header stored in "sigheader"
values = {}
sigheader.split(",").map!{|kvp| kvp.split("=") }.each{|k, v| values[k]=v}
# values has keys t, v1, v2
Updated over 2 years ago