Verifying requests from Harbor
The harbor-signature
header included in each signed webhook event contains a timestamp and signature. The timestamp is prefixed by t=
, and the signature is prefixed by a scheme. The scheme begins with v
.
Example harbor-signature
Header
harbor-signature
Headerharbor-signature:
t=1689066169,
v1=e997b87453fb8923ae9c02faf45fe5fff60148d7a244896e0b359c96aa4825a0
Note: For clarity, newlines are added in the example, but the
harbor-signature
header in an actual request is a single line.
Harbor generates signatures using HMAC with SHA-256. While we will provide official libraries for signature verification in the future, you can create a custom solution by following these steps:
Steps to Verify the Webhook Signature
Step 1: Extract the Timestamp and Signature from the Header
Split the harbor-signature
header using the ,
character to separate elements. Then split each element using the =
character to extract the prefix and value pair.
- The value for the prefix
t
corresponds to the timestamp. - The value for the prefix
v1
corresponds to the signature.
Step 2: Prepare the signed_payload
String
signed_payload
StringTo create the signed_payload
string, concatenate the following:
- The timestamp (as a string).
- The character
.
. - The actual JSON payload (i.e., the request body).
Step 3: Compute the Expected Signature
Using the HMAC-SHA256 hash function:
- Use the signing secret as the key.
- Use the
signed_payload
string as the message.
Step 4: Compare the Signatures
Compare the computed signature with the one provided in the header (v1
). If they match, the webhook signature is valid.
Node.js Code Example
Below is an example of how to implement this process in Node.js:
const crypto = require('crypto');
// Simulate the incoming harbor-signature header
const requestHeader = 't=1689066169,v1=e997b87453fb8923ae9c02faf45fe5fff60148d7a244896e0b359c96aa4825a0';
// Split the header into parts
const elements = requestHeader.split(',');
const signatureData = elements.reduce((acc, item) => {
const [key, value] = item.split('=');
acc[key] = value;
return acc;
}, {});
// Extract the timestamp and signature
const timestamp = signatureData['t'];
const signature = signatureData['v1'];
// The JSON payload (request body)
const requestBody = '{
"status": "completed",
"source": {
"asset": "USDC",
"amount": "1000.00",
},
"destination": {
"asset": "USD",
"account_number": "123456789012",
"routing_number": "021000021",
"bank_name": "Test Bank USA",
"bank_address": "123 Test Street, New York, NY 10001, USA",
"account_holder_name": "John Doe",
"amount": "980.00",
},
"application_transfer_uuid": "{{YOUR_APPLICATION_TRANSFER_UUID}}",
"transfer_instructions": {
"chain": "ethereum",
"address": "0x6990E7E90ab50C12111f99b84183D3fE298Bb3e4"
},
"commission": {
"rate": "0.01",
"price": 1
},
"receipt": {
"initial_amount": "1000.00",
"commission_fee": "20.00",
"harbor_fee": "5.00",
"final_amount": "975.00",
"tracking_number": '92711234310210', // This field will be filled if the transfer status is `bank_in_process`
},
"created_at": "2025-02-07T13:05:32+00:00",
"updated_at": "2025-02-07T13:05:32+00:00"
}';
// Harbor signing secret
const signingSecret = 'whs_xxxxxxx';
// Create the signed payload string
const signedPayload = `${timestamp}.${requestBody}`;
// Generate the expected HMAC signature
const expectedSignature = crypto
.createHmac('sha256', signingSecret)
.update(signedPayload)
.digest('hex');
// Verify if the signature matches
if (expectedSignature === signature) {
console.log('Signature verification passed.');
} else {
console.log('Signature verification failed.');
}
When executing a Transfer for an e-commerce platform or similar use case, we strongly recommend verifying the harbor-signature to ensure the request originates from Harbor. This step helps maintain security before delivering goods to the consumer.
Updated 26 days ago