Stayntouch: Webhook Signature Verification Guide

Stayntouch: Webhook Signature Verification Guide

Overview

To ensure the authenticity and security of webhook requests from Stayntouch, all webhook payloads include an HMAC-SHA256 signature that you should validate before processing.

Getting Your Shared Secret

Your Stayntouch Onboarding team will configure webhook signature verification in your PMS and provide you with your unique shared signature secret. This secret is used to verify that webhook requests are genuinely from Stayntouch and haven't been tampered with.

Important: Keep your shared secret secure and never expose it in client-side code or public repositories.

How Signature Verification Works

Every webhook request from Stayntouch contains a signature in the snt-webhook-signature header. To verify the webhook's authenticity:

  1. Use the request body exactly as received (raw payload)

  2. Generate an HMAC-SHA256 hash using your shared secret and the payload

  3. Encode the resulting hash using Base64

  4. Compare the computed Base64-encoded hash with the snt-webhook-signature header value

  5. If the values match, the request is authentic

 

To ensure the authenticity of webhook requests from Stayntouch, validate the HMAC-SHA256 signature included in each request’s header.

Every request contains a signature in the snt-webhook-signature header. Use your shared secret and the request body to verify this signature.

Testing the webhook payload validation

You can use the following shared secret and payload values to verify that your implementation is correct.

Payload (JSON):

{ "id": 203, "object": "event", "event": "edit_reservation", "created_at": "2025-06-13T06:01:42Z", "data": { "id": 1, "object": "reservation", "hotel_id": 1, "confirmation_number": "100000", "arrival_date": "2025-06-12", "arrival_time": "2025-06-13T01:00:00.000Z", "departure_date": "2025-06-12", "departure_time": "2025-06-13T03:00:00.000Z", "status": "reserved" } }

Shared Secret (generated by user): d1dc79f45a5df6e2b39888b1a734202c

 

If your implementation is correct, the signatures that you generate should match the following signature values:

  • signature: CO5s3gPNVbTmnPq3zQM1/7JJCsB4mUDcZras3RZ+Zkw=

  • snt-webhook-signature: CO5s3gPNVbTmnPq3zQM1/7JJCsB4mUDcZras3RZ+Zkw=


Implementation Examples

Below are code examples showing how to implement HMAC signature verification in various programming languages.

Ruby

require 'openssl' require 'base64' require 'active_support/security_utils' def verify_hmac_signature(payload, signature, shared_secret) hmac_hex = OpenSSL::HMAC.hexdigest('sha256', shared_secret, payload) hmac_base64 = Base64.strict_encode64([hmac_hex].pack('H*')) ActiveSupport::SecurityUtils.secure_compare(hmac_base64, signature) end # Controller Example class WebhooksController < ApplicationController skip_before_action :verify_authenticity_token def receive payload = request.raw_post signature = request.headers['snt-webhook-signature'] shared_secret = ENV['STAYNTOUCH_WEBHOOK_SECRET'] if verify_hmac_signature(payload, signature, shared_secret) # Process webhook head :ok else head :unauthorized end end end

Node.js / JavaScript

const crypto = require('crypto'); function verifyHmacSignature(payload, signature, sharedSecret) { const hmac = crypto.createHmac('sha256', sharedSecret) .update(payload) .digest('hex'); const hmacBase64 = Buffer.from(hmac, 'hex').toString('base64'); return signature === hmacBase64; } // Express.js example app.post('/webhook', (req, res) => { const payload = JSON.stringify(req.body); const signature = req.headers['snt-webhook-signature']; const sharedSecret = process.env.STAYNTOUCH_WEBHOOK_SECRET; const isValid = verifyHmacSignature(payload, signature, sharedSecret); if (isValid) { // Process webhook return res.status(200).send('OK'); } return res.status(401).send('Invalid signature'); });

Python

import hmac import hashlib import base64 def verify_hmac_signature(payload: str, signature: str, shared_secret: str) -> bool: # Generate HMAC SHA256 digest hmac_digest = hmac.new( key=shared_secret.encode('utf-8'), msg=payload.encode('utf-8'), digestmod=hashlib.sha256 ).digest() # Convert to Base64 hmac_base64 = base64.b64encode(hmac_digest).decode('utf-8') # Compare securely return hmac.compare_digest(hmac_base64, signature) # Flask example import json @app.route('/webhook', methods=['POST']) def webhook(): # Convert the payload to a JSON string payload = json.dumps(request.json, separators=(',', ':')) signature = request.headers.get('snt-webhook-signature') shared_secret = os.environ.get('STAYNTOUCH_WEBHOOK_SECRET') if verify_hmac_signature(payload, signature, shared_secret): # Process webhook return 'OK', 200 else: return 'Invalid signature', 401

PHP

function verify_hmac_signature($payload, $signature, $sharedSecret) { // Create HMAC SHA256 hash $hash = hash_hmac('sha256', $payload, $sharedSecret, true); // Encode to Base64 $hmacBase64 = base64_encode($hash); // Constant-time comparison to prevent timing attacks return hash_equals($hmacBase64, $signature); } // Usage $payload = json_encode($payload, JSON_UNESCAPED_SLASHES); $signature = $_SERVER['HTTP_SNT_WEBHOOK_SIGNATURE']; $sharedSecret = getenv('STAYNTOUCH_WEBHOOK_SECRET'); if (verify_hmac_signature($payload, signature, $sharedSecret)) { // Process webhook echo "Verified"; } else { http_response_code(401); echo "Invalid signature"; }

Best Practices

  • Store your shared secret securely in environment variables or a secrets manager

  • Always use the raw request body for signature verification (before any parsing or processing)

  • Use constant-time comparison functions to prevent timing attacks

  • Log failed verification attempts for security monitoring

Support

If you have questions about implementing webhook signature verification or need assistance: