Signature Verification

Webhook Signatures

Each webhook in COS is signed with a standard HMAC with SHA256 hash. This provides an added layer of protection from replay attacks. Without a signature present in the webhook, an attacker could potentially intercept a valid payload and re-transmit it. However, since the event timestamp is part of the signature, it is not possible to modify either the timestamp or the signature without also invalidating the signature.


Old Timestamps

If you receive an event with an old timestamp it is recommended your application discard or flag it. The recommended tolerance between timestamp and time of delivery will vary by your application's requirements, but should generally be less than 20 minutes. Special consideration for situations where there has been an extended outage between either party is also recommended.

The signature can be found in the event's request header.


All COS signatures are version 1 (v1). Any other schemes besides v1 should ignored.
The signature is a standard HMAC with SHA256 hash.
COS generates the timestamp and signature each time we send an event to your endpoint. If COS retries an event, after a previous failure, we generate a new signature and timestamp for the new delivery attempt.

Validating the Signature

  1. Extract the timestamp and signature from the cos-signature header.
  2. Concatenate the timestamp with the event body with a period in between the two values (as illustrated below).
  3. base64 decode your signing secret (provided by your COS Integration Support team) (as illustrated below).
  4. Create an HMAC object using the SHA256 hash function and the decoded signing secret as the key.
  5. Compute the HMAC of the message created in step #2.
  6. Base64 encode the computed hash value.
  7. Compare the hash generated in steps 3-6 with the signature extracted from the cos-signature header. If the hash and signature match this is a valid request from COS.
  8. Compare the timestamp to the time received. If the difference exceeds your tolerance for accepting messages, you can reject the message or flag it for further review.
unBase64Secret, _ := b64.StdEncoding.DecodeString(secret)

Below is a Node.js script which illustrates how a webhook signature would typically be validated.

const crypto = require('crypto');

function validateSignature(header, payload, signingSecret) {
                let headerParts = header.split(',');
                let timestamp = headerParts[0].replace('t:', '');
                let signature = headerParts[1].trim().replace('v1:', '');

                let secret_buffer = Buffer.from(signingSecret, 'base64');              
                let hmac = new crypto.createHmac('sha256', secret_buffer);

                let hmac_digest = hmac.update(`${timestamp}.${payload}`, 'binary').digest('base64');

                return hmac_digest === signature;

let payload = '{"id":"e7ead744-d6ff-4521-863d-abab0176f849","eventName":"Core.Transaction.Completed","status":0,"partnerId":"d6b4c661-b38a-46a3-8963-a9a40131eacf","createdAt":"2020-04-28T18:45:14.57-04:00","resources":["core/v1/transactions/6aa7e3b2-3c85-4647-aa6f-abab0176e18b"],"details":[{"transactionId":"6aa7e3b2-3c85-4647-aa6f-abab0176e18b","transactionCode":"Account Transfer","debitSubAccount":"2058112745","debitMasterAccount":"2058112745","debitResult":"OK","creditSubAccount":"2101120877","creditMasterAccount":"2101120877","creditResult":"OK","rail":"Internal","railId":"0","amount":"100"}]}';
let signatureHeader = 't:2020-04-28T18:45:15.6360965-04:00, v1:MvGXdx1O1P8+YjWglbmxAxkrAgVlMglSPpCzsR/Ly/w=';
let secret = 'uVdwwB9HIFZ+5/8nmta5PXu6p1kxZcQmXPCNBRhiVNuKNBhIgth8MvmlD7FYoVfHOmcpHO5QYN/3HHnJ+6TO6Q==';

let result = validateSignature(signatureHeader, payload, secret);