Stash Webhooks

Webhook Listener

Learn how to create a webhook listener on your backend to receive incoming Stash webhook requests, verify their authenticity using HMAC SHA-256 signatures, and respond appropriately to process events securely.

Begin by creating a webhook listener on your backend. A webhook listener is a server-side program that receives incoming Stash webhook requests at a designated URL, verifies their authenticity (checking the signature), and responds appropriately (if needed) to the Stash.

Example webhook

{
  "type": "PURCHASE_SUCCEEDED",
  "purchaseSucceeded": {
    "timeMillis": 1753993257000,
    "orderId": "8ZVBabLrnCMm9zPdu9QpfCWzNaA",
    "currency": "usd",
    "userId": "user_id",
    "items": [
      {
        "id": "item_id",
        "quantity": 1,
        "price": "199"
      }
    ],
    "tax": "0",
    "total": "199",
    "regionCode": "US",
    "source": "StashPay"
  }
}

Note: The source field indicates which product generated the webhook. It will be "StashPay" for Stash Pay events or "Cart" for Stash Webshop events. See the webhook event list for all available events and their product associations.

Each webhook payload includes a type field that indicates the event type enum (ex: PURCHASE_SUCCEEDED). The payload also contains a corresponding object with event-specific details. For a complete overview of all available webhook event types and their payload structures, refer to the webhook event list.

Signature verification

To ensure secure data transmission, you must verify that each webhook was actually sent from Stash and has not been tampered with in transit. Stash uses an HMAC SHA-256 signature to sign every webhook request. You should generate your own signature using your project's secret key and compare it to the signature provided in the Stash-Hmac-Signature header of the incoming request. If the signatures match, you can trust the webhook is authentic.

Verification steps:

Retrieve the signature

Extract the signature from the Stash-Hmac-Signature header of the incoming Stash webhook request. The header format is:

Stash-Hmac-Signature myp5FsYU7FpC1R6Dk4MUMOdB+boKgeIrffqFBaim0Wo=

Obtain the request body

Read the raw JSON payload of the webhook request exactly as received (do not parse and re-serialize, as this may change whitespace or formatting).

Generate your own signature

  • Concatenate the raw request body (as a string) with your Stash webhook secret key (do not add any separators).
  • Compute the HMAC SHA-256 hash of this concatenated string, using your secret key as the HMAC key.
  • Encode the resulting hash as a lowercase hexadecimal string.

Example in pseudocode:

signature = HMAC_SHA256(secret_key_base64, raw_request_body)

Compare signatures

Compare your generated signature to the value from the Stash-Hmac-Signature header. If they match, the webhook is valid and can be processed safely.

Note:

  • Please keep in mind that the webhook secret you obtain from the Stash Studio is Base64 encoded.
  • Always use the exact raw request body as received for signature calculation.
  • Never expose your webhook secret key in client-side code or logs.
  • If the signatures do not match, reject the request and do not process the webhook.

For additional security, you may also want to validate the request's timestamp or IP address, depending on your backend's requirements.

Implementation Examples

Node.js/JavaScript

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const computedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('base64');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(computedSignature)
  );
}

// Express.js example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['stash-hmac-signature'];
  const isValid = verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET);
  
  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  const event = JSON.parse(req.body);
  // Process webhook event...
  
  res.status(200).json({ received: true });
});

Python

import hmac
import hashlib
import base64
import json

def verify_webhook_signature(payload, signature, secret):
    computed_signature = base64.b64encode(
        hmac.new(
            secret.encode('utf-8'),
            payload.encode('utf-8'),
            hashlib.sha256
        ).digest()
    ).decode('utf-8')
    
    return hmac.compare_digest(signature, computed_signature)

# Flask example
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('stash-hmac-signature')
    payload = request.get_data(as_text=True)
    
    is_valid = verify_webhook_signature(payload, signature, os.environ['WEBHOOK_SECRET'])
    
    if not is_valid:
        return jsonify({'error': 'Invalid signature'}), 401
    
    event = json.loads(payload)
    # Process webhook event...
    
    return jsonify({'received': True}), 200

Go

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "io"
    "net/http"
)

func verifyWebhookSignature(payload []byte, signature string, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    expectedSignature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
    
    return hmac.Equal([]byte(signature), []byte(expectedSignature))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    signature := r.Header.Get("stash-hmac-signature")
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Failed to read body", http.StatusBadRequest)
        return
    }
    
    isValid := verifyWebhookSignature(body, signature, os.Getenv("WEBHOOK_SECRET"))
    
    if !isValid {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }
    
    // Process webhook event...
    
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"received": true}`))
}

Security Best Practices

  1. Always verify signatures in production: Even if optional, signature verification should be enabled for production endpoints
  2. Use timing-safe comparison: Use constant-time comparison functions (e.g., crypto.timingSafeEqual, hmac.compare_digest, hmac.Equal) to prevent timing attacks
  3. Store secrets securely: Never commit webhook secrets to version control; use environment variables or secret management services
  4. Rotate secrets periodically: Periodically rotate webhook secrets and update endpoints

Disabling Signature Verification

If you choose not to verify signatures (not recommended for production), you can simply ignore the Stash-Hmac-Signature header. However, this leaves your endpoint vulnerable to fake webhook events. Always enable signature verification in production environments.

How is this guide?