Authentication

Learn how to authenticate requests when integrating Stash Pay. This guide covers API key authentication for server-to-server requests and HMAC signature verification for secure backend communication.

Stash Pay uses two authentication methods:

  1. API Keys - For authenticating your backend's requests to Stash
  2. HMAC Signatures - For Stash to authenticate requests to your backend

API Key Authentication (Your Backend → Stash)

When API Keys Are Required

API keys are required for all server-side Stash Pay operations:

  • Checkout Link Generation - Creating multi-item checkout links
  • Quick Pay URL Generation - Creating single-item Quick Pay URLs
  • Payment Status Queries - Checking payment status by order ID
  • Payments Service Operations - Authorizing and capturing payments (standalone Stash Pay)

How to Use API Keys

Include your API key in the X-Stash-Api-Key header:

X-Stash-Api-Key: your-api-key-secret-here

Make sure API key requests are made from your server, not the client, so the API key remains private.

Creating API Keys

Go to Stash StudioProject SettingsAPI Secrets.

Create a New API Secret

Click "Create API Secret" or "Add API Secret".

Name Your Key

Enter a descriptive name (e.g., "Stash Pay Production", "Stash Pay Test Environment").

Copy the Secret

Click "Create" and copy the secret value immediately - it's only shown once.

For detailed information on creating, managing, and securing API keys, see the API Keys guide.

Code Examples

Node.js - Generate Checkout Link
const axios = require('axios');

async function generateCheckoutLink(shopHandle, items, user) {
  const response = await axios.post(
    'https://api.stash.gg/api/v1/sdk/checkout-links/generate',
    {
      shop_handle: shopHandle,
      currency: 'USD',
      external_user: {
        id: user.id,
        validated_email: user.email,
        display_name: user.displayName
      },
      items: items.map(item => ({
        id: item.id,
        name: item.name,
        price_per_item: item.price.toString(),
        quantity: item.quantity,
        image_url: item.imageUrl
      }))
    },
    {
      headers: {
        'X-Stash-Api-Key': process.env.STASH_API_KEY,
        'Content-Type': 'application/json'
      }
    }
  );
  return response.data.url;
}

Generate Quick Pay URL

Node.js - Generate Quick Pay URL
async function generateQuickPayUrl(shopHandle, itemId, userId, transactionId) {
  const response = await axios.post(
    'https://api.stash.gg/api/v1/sdk/quick-pay/generate',
    {
      shop_handle: shopHandle,
      item: {
        id: itemId
      },
      user: {
        id: userId
      },
      transaction_id: transactionId,
      currency: 'USD'
    },
    {
      headers: {
        'X-Stash-Api-Key': process.env.STASH_API_KEY,
        'Content-Type': 'application/json'
      }
    }
  );
  return response.data.url;
}

Query Payment Status

Node.js - Query Payment Status
async function getPaymentEvent(shopHandle, orderId) {
  const response = await axios.get(
    `https://api.stash.gg/api/v1/sdk/payments/event/${orderId}`,
    {
      headers: {
        'X-Stash-Api-Key': process.env.STASH_API_KEY
      }
    }
  );
  return response.data;
}

HMAC Authentication (Stash → Your Backend)

Overview

When Stash Pay needs to call your backend (e.g., to confirm a payment), it authenticates using HMAC signatures. Your backend must verify these signatures to ensure requests are legitimate.

How HMAC Authentication Works

  1. Stash retrieves your egress API key (the same key you use for outgoing requests)
  2. Stash generates an HMAC signature of the request body using your API key
  3. Stash sends the signature in the stash-hmac-signature header
  4. Your backend verifies the signature matches the request body

The egress API key is the same as your ingress API key - there's no separate key type. Stash uses your API key to sign requests to your backend.

Verifying HMAC Signatures

Node.js Example

Node.js - HMAC Verification
const crypto = require('crypto');

function verifyHmacSignature(requestBody, signature, apiKey) {
  // Decode the base64 signature
  const receivedSignature = Buffer.from(signature, 'base64');
  
  // Generate expected signature
  const expectedSignature = crypto
    .createHmac('sha256', apiKey)
    .update(requestBody, 'utf8')
    .digest();
  
  // Compare signatures using constant-time comparison
  return crypto.timingSafeEqual(receivedSignature, expectedSignature);
}

// Express.js middleware example
function verifyStashHmac(req, res, next) {
  const signature = req.headers['stash-hmac-signature'];
  const apiKey = process.env.STASH_API_KEY;
  
  if (!signature) {
    return res.status(401).json({ error: 'Missing HMAC signature' });
  }
  
  // Get raw body (you may need body-parser with verify option)
  const body = JSON.stringify(req.body);
  
  if (!verifyHmacSignature(body, signature, apiKey)) {
    return res.status(401).json({ error: 'Invalid HMAC signature' });
  }
  
  next();
}

// Use middleware
app.post('/stash/confirm-payment', verifyStashHmac, (req, res) => {
  // Process payment confirmation
  // ...
});

Python Example

Python - HMAC Verification
import hmac
import hashlib
import base64
import os
from flask import Flask, request, jsonify

def verify_hmac_signature(request_body, signature, api_key):
    """Verify HMAC signature from Stash"""
    # Decode the base64 signature
    received_signature = base64.b64decode(signature)
    
    # Generate expected signature
    expected_signature = hmac.new(
        api_key.encode('utf-8'),
        request_body.encode('utf-8'),
        hashlib.sha256
    ).digest()
    
    # Compare signatures using constant-time comparison
    return hmac.compare_digest(received_signature, expected_signature)

app = Flask(__name__)

@app.route('/stash/confirm-payment', methods=['POST'])
def confirm_payment():
    signature = request.headers.get('stash-hmac-signature')
    api_key = os.environ['STASH_API_KEY']
    
    if not signature:
        return jsonify({'error': 'Missing HMAC signature'}), 401
    
    # Get raw request body
    request_body = request.get_data(as_text=True)
    
    if not verify_hmac_signature(request_body, signature, api_key):
        return jsonify({'error': 'Invalid HMAC signature'}), 401
    
    # Process payment confirmation
    data = request.get_json()
    # ...
    
    return jsonify({'success': True})

Go Example

Go - HMAC Verification
package main

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

func verifyHMACSignature(body []byte, signature string, apiKey string) bool {
    // Decode the base64 signature
    receivedSig, err := base64.StdEncoding.DecodeString(signature)
    if err != nil {
        return false
    }
    
    // Generate expected signature
    mac := hmac.New(sha256.New, []byte(apiKey))
    mac.Write(body)
    expectedSig := mac.Sum(nil)
    
    // Compare signatures using constant-time comparison
    return hmac.Equal(receivedSig, expectedSig)
}

func confirmPaymentHandler(w http.ResponseWriter, r *http.Request) {
    // Get HMAC signature from header
    signature := r.Header.Get("stash-hmac-signature")
    if signature == "" {
        http.Error(w, "Missing HMAC signature", http.StatusUnauthorized)
        return
    }
    
    // Read request body
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Failed to read body", http.StatusBadRequest)
        return
    }
    
    // Verify signature
    apiKey := os.Getenv("STASH_API_KEY")
    if !verifyHMACSignature(body, signature, apiKey) {
        http.Error(w, "Invalid HMAC signature", http.StatusUnauthorized)
        return
    }
    
    // Process payment confirmation
    // ...
}

Security Best Practices for HMAC Verification

DO:

  • Always verify signatures before processing requests
  • Use constant-time comparison (timingSafeEqual, hmac.compare_digest, or hmac.Equal) to prevent timing attacks
  • Verify the signature before parsing JSON to prevent injection attacks
  • Log verification failures for security monitoring

DON'T:

  • Trust requests without valid HMAC signatures
  • Use regular string comparison for signature verification
  • Parse JSON before verifying the signature

Test Mode

In test mode, Stash also sends the X-Stash-Api-Key header for convenience. However, you should still verify HMAC signatures in all environments, including test.

Common Pitfalls

HMAC Signature Verification Issues

Common Mistake: Not verifying HMAC signatures because test mode includes API key header

Solution: Always verify HMAC signatures in all environments, even in test mode. The API key header in test mode is for convenience only.

Common Mistake: Modifying request body before verifying signature (parsing JSON, reformatting, etc.)

Solution: Verify the signature using the exact raw request body as received. Parse JSON only after verification succeeds.

Common Mistake: Using regular string comparison for signature verification

Solution: Always use constant-time comparison (timingSafeEqual, hmac.compare_digest, or hmac.Equal) to prevent timing attacks.

API Key vs HMAC Confusion

Common Mistake: Thinking API keys are used for both directions (your backend → Stash AND Stash → your backend)

Solution:

  • API keys authenticate your backend → Stash requests
  • HMAC signatures authenticate Stash → your backend requests
  • Both use the same API key, but serve different purposes

Body Encoding Issues

Common Mistake: Verifying signature with incorrectly encoded body

Solution: Ensure the request body is UTF-8 encoded and matches exactly what Stash sent (including whitespace, newlines, etc.)

Test vs Production Differences

Common Mistake: Assuming test mode behavior applies to production

Solution: Test mode includes X-Stash-Api-Key header for convenience, but production only uses HMAC signatures. Always verify HMAC in both environments.

Error Handling

Authentication Errors

401 Unauthenticated

Error: invalid auth or X-Stash-Api-Key header is required

Solution: Ensure the X-Stash-Api-Key header is included and not empty.

403 Permission Denied

Error: invalid key or PermissionDenied

Causes:

  • API key is invalid or deleted
  • API key doesn't belong to the shop in the request

Solution: Verify the API key in Studio and ensure it matches the shop.

HMAC Verification Failures

If HMAC signature verification fails:

  1. Check API Key: Ensure you're using the correct API key
  2. Verify Body: Ensure you're signing/verifying the exact request body (including whitespace)
  3. Check Encoding: Ensure body is UTF-8 encoded
  4. Test Mode: In test mode, verify both HMAC and API key header if present

Common Integration Patterns

// 1. User adds items to cart in game
// 2. Game backend calls Stash to generate checkout link
const checkoutUrl = await generateCheckoutLink(shopHandle, items, user);

// 3. Redirect user to checkoutUrl
// 4. User completes payment
// 5. Stash calls your backend with HMAC-authenticated request

Pattern 2: Quick Pay Flow

// 1. User clicks "Buy" on item
// 2. Game backend calls Stash to generate Quick Pay URL
const quickPayUrl = await generateQuickPayUrl(shopHandle, itemId, userId, transactionId);

// 3. Open Quick Pay URL
// 4. User completes payment
// 5. Stash calls your backend with HMAC-authenticated confirmation

Security Checklist

  • API keys stored in environment variables (never in code)
  • Different API keys for test and production
  • HMAC signature verification implemented on backend
  • Constant-time comparison used for HMAC verification
  • API keys rotated regularly (every 90 days)
  • Failed authentication attempts logged
  • API keys never exposed in client-side code

How is this guide?