# Authentication This page explains how Spotnana authenticates with your endpoint when delivering webhook events, and what your system needs to support. ## HMAC signature verification Spotnana signs every webhook payload using [HMAC-SHA256 protocol](https://en.wikipedia.org/wiki/HMAC). This allows your endpoint to independently verify that a webhook was genuinely sent by Spotnana and that the payload was not tampered with in transit. **Note:** Partners currently using the legacy OAuth 2.0 client credentials flow will continue to be supported. However, we recommend migrating the events to use HMAC signature and updating your systems to accept this signature. Reach out to your Spotnana account representative to initiate the migration. ### Authentication procedure During onboarding, Spotnana provides you with a unique webhook signing secret. This secret starts with a `whsec_` prefix (e.g., `whsec_a1b2c3d4...`). Keep this secret secure and never expose it in client-side code or public repositories. Every webhook delivery will include three headers that you will use to verify the payload: | Header | Description | | --- | --- | | `x-spotnana-webhook-id` | A unique identifier for this webhook delivery. This ID remains the same if the same webhook is re-sent. | | `x-spotnana-webhook-timestamp` | The Unix timestamp (in seconds) of when the webhook was sent. | | `x-spotnana-webhook-signature` | The Base64-encoded HMAC-SHA256 signature of the payload. May contain multiple signatures during key rotation periods, separated by spaces. | #### Step 1: Prepare your signing secret Remove the `whsec_` prefix from your signing secret, then Base64-decode the remaining string into raw bytes. This decoded value is what you will use for the cryptographic operation in step 3. #### Step 2: Reconstruct the signed string Concatenate the webhook ID, timestamp, and raw request body, each separated by a period: ``` {x-spotnana-webhook-id}.{x-spotnana-webhook-timestamp}.{raw-request-body} ``` Always use the raw, unparsed HTTP request body. If your framework parses the JSON body into an object and you re-stringify it, the formatting may differ slightly and cause verification to fail. #### Step 3: Compute the expected signature Using the HMAC-SHA256 algorithm, hash the reconstructed string from step 2 using the decoded secret bytes from step 1. Encode the result as a Base64 string. #### Step 4: Compare the signatures Split the `x-spotnana-webhook-signature` header by spaces to get an array of signatures. Compare your computed signature against each one using a constant-time comparison function to prevent timing attacks. If any signature matches, the webhook is valid and safe to process. ### Key rotation During key rotation, the `x-spotnana-webhook-signature` header may contain multiple signatures separated by spaces. Your verification logic should check the computed signature against each one and treat the webhook as valid if any match. ### Sample receiver Here's a sample node.js express webhook receiver you can use to receive the webhook messages sent from Spotnana: ```javascript webhook-receiver.js const express = require('express'); const crypto = require('crypto'); const app = express(); // 1. Capture the raw body string specifically for signature verification app.use(express.json({ verify: function (req, res, buf) { req.rawBody = buf.toString('utf8'); } })); app.post('/webhook-receiver', function(req, res) { const webhookId = req.headers['x-spotnana-webhook-id']; const webhookTimestamp = req.headers['x-spotnana-webhook-timestamp']; const webhookSignature = req.headers['x-spotnana-webhook-signature']; if (!webhookId || !webhookTimestamp || !webhookSignature) { return res.status(401).json({ error: "Missing required headers" }); } const secret = process.env.HMAC_SIGNING_SECRET; // 2. Prepare the secret const base64Secret = secret.startsWith('whsec_') ? secret.slice(6) : secret; const decodedSecret = Buffer.from(base64Secret, 'base64'); try { // 3. Reconstruct the signed payload using the raw body const signedContent = `${webhookId}.${webhookTimestamp}.${req.rawBody}`; // 4. Compute the expected signature const computedSignature = crypto .createHmac('sha256', decodedSecret) .update(signedContent, 'utf8') .digest('base64'); // 5. Compare the signatures securely const signatureParts = webhookSignature.trim().split(/\s+/); let isValid = false; for (const receivedSignature of signatureParts) { if (!receivedSignature) continue; try { if (crypto.timingSafeEqual( Buffer.from(computedSignature, 'base64'), Buffer.from(receivedSignature, 'base64') )) { isValid = true; break; } } catch (e) { // timingSafeEqual throws if buffers differ in length; skip malformed signatures continue; } } if (!isValid) { return res.status(401).json({ error: "Invalid signature" }); } // Process the webhook safely here console.log("Webhook verified successfully!"); res.status(200).json({ status: "ok" }); } catch (error) { return res.status(500).json({ error: "Verification error" }); } }); ```