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 (Ingress)
  2. HMAC Signatures - For Stash to authenticate requests to your backend (Egress)

API Key Types

Stash supports two types of API keys depending on the direction of the request flow:

  • Ingress API Keys — Used when your backend calls Stash endpoints (e.g., GetPaymentEvent, checkout link generation)
  • Egress API Keys — Used when Stash calls your ConfirmPayment endpoint. Stash signs these requests with HMAC using your base64-encoded egress key.

Note: Webhooks use a separate webhook secret for HMAC signing, not the egress API key. See the Webhook Listener guide for webhook authentication details.

You can create and manage both key types in Stash StudioProject SettingsAPI Secrets. Make sure to use the correct key type for each integration pattern.

API Key Authentication (Your Backend → Stash)

When API Keys Are Required

Ingress API keys are required for all server-side Stash Pay operations when your backend calls Stash:

  • Checkout Link Generation - Creating multi-item checkout links
  • Quick Pay URL Generation - Creating single-item Quick Pay URLs
  • Payment Status Queries - Checking payment status via GetPaymentEvent
  • Payments Service Operations - Authorizing and capturing payments

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, it authenticates using HMAC signatures. Your backend must verify these signatures to ensure the requests are legitimate.

Important: ConfirmPayment and webhooks use different secrets for HMAC signing. Make sure to use the correct secret for each endpoint type.

When HMAC Verification Is Required

HMAC signature verification is required for endpoints where Stash calls your backend:

Endpoint TypeSecret UsedWhere to Find It
ConfirmPaymentBase64-encoded Egress API KeyStash StudioProject SettingsAPI Secrets
Webhook EndpointsWebhook SecretStash StudioProject SettingsWebhooks
  • ConfirmPayment Endpoint — Stash calls this before finalizing a charge, giving your backend a chance to validate inventory, apply locks, and approve or reject the transaction. Uses your base64-encoded egress API key for HMAC signing.
  • Webhook Endpoints — Stash sends PURCHASE_SUCCEEDED and other events to your configured webhook URL. Uses your webhook secret for HMAC signing (see Webhook Listener for details).

See the High-Level Flow Options guide to understand when to use ConfirmPayment vs webhooks vs GetPaymentEvent.

How HMAC Authentication Works for ConfirmPayment

For ConfirmPayment requests:

  1. Stash retrieves your egress API key and base64-encodes it
  2. Stash generates an HMAC-SHA256 signature of the request body using BASE64(egress_api_key) as the secret
  3. Stash sends the signature in the stash-hmac-signature header
  4. Your backend verifies the signature matches the request body using the same base64-encoded egress key

Verifying HMAC Signatures for ConfirmPayment

The following examples show how to verify HMAC signatures for ConfirmPayment requests using your base64-encoded egress API key.

For webhook signature verification, see the Webhook Listener guide, which uses a different secret (your webhook secret).

Node.js Example

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

function verifyConfirmPaymentSignature(requestBody, signature, egressApiKey) {
  // Decode the base64 signature from the header
  const receivedSignature = Buffer.from(signature, 'base64');
  
  // Base64-encode the egress API key (this is the secret used for signing)
  const base64EncodedKey = Buffer.from(egressApiKey).toString('base64');
  
  // Generate expected signature using the base64-encoded key
  const expectedSignature = crypto
    .createHmac('sha256', base64EncodedKey)
    .update(requestBody, 'utf8')
    .digest();
  
  // Compare signatures using constant-time comparison
  return crypto.timingSafeEqual(receivedSignature, expectedSignature);
}

// Express.js middleware example for ConfirmPayment
function verifyConfirmPaymentHmac(req, res, next) {
  const signature = req.headers['stash-hmac-signature'];
  const egressApiKey = process.env.STASH_EGRESS_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 (!verifyConfirmPaymentSignature(body, signature, egressApiKey)) {
    return res.status(401).json({ error: 'Invalid HMAC signature' });
  }
  
  next();
}

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

Python Example

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

def verify_confirm_payment_signature(request_body, signature, egress_api_key):
    """Verify HMAC signature from Stash for ConfirmPayment requests"""
    # Decode the base64 signature from the header
    received_signature = base64.b64decode(signature)
    
    # Base64-encode the egress API key (this is the secret used for signing)
    base64_encoded_key = base64.b64encode(egress_api_key.encode('utf-8'))
    
    # Generate expected signature using the base64-encoded key
    expected_signature = hmac.new(
        base64_encoded_key,
        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')
    egress_api_key = os.environ['STASH_EGRESS_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_confirm_payment_signature(request_body, signature, egress_api_key):
        return jsonify({'error': 'Invalid HMAC signature'}), 401
    
    # Process payment confirmation
    data = request.get_json()
    # ...
    
    return jsonify({'success': True})

Go Example

Go - ConfirmPayment HMAC Verification
package main

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

func verifyConfirmPaymentSignature(body []byte, signature string, egressApiKey string) bool {
    // Decode the base64 signature from the header
    receivedSig, err := base64.StdEncoding.DecodeString(signature)
    if err != nil {
        return false
    }
    
    // Base64-encode the egress API key (this is the secret used for signing)
    base64EncodedKey := base64.StdEncoding.EncodeToString([]byte(egressApiKey))
    
    // Generate expected signature using the base64-encoded key
    mac := hmac.New(sha256.New, []byte(base64EncodedKey))
    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 using base64-encoded egress API key
    egressApiKey := os.Getenv("STASH_EGRESS_API_KEY")
    if !verifyConfirmPaymentSignature(body, signature, egressApiKey) {
        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 the correct secret for each endpoint type:
    • ConfirmPayment: Base64-encoded egress API key
    • Webhooks: Webhook secret
  • 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
  • Mix up secrets between ConfirmPayment and webhooks

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: Using the wrong secret for HMAC verification

Solution: Use the correct secret for each endpoint type:

  • ConfirmPayment: Use your base64-encoded egress API key (from Stash StudioProject SettingsAPI Secrets)
  • Webhooks: Use your webhook secret (from Stash StudioProject SettingsWebhooks)

Common Mistake: Not base64-encoding the egress API key for ConfirmPayment

Solution: The egress API key must be base64-encoded before using it as the HMAC secret. The signature format is HMAC-SHA256(request_body, BASE64(egress_api_key)).

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: Confusing ingress and egress API keys, or mixing up secrets for different endpoint types

Solution:

  • Ingress API keys authenticate your backend → Stash requests (e.g., GetPaymentEvent, checkout link generation)
  • Egress API keys (base64-encoded) are used by Stash to sign Stash → your backend ConfirmPayment requests via HMAC
  • Webhook secrets are used by Stash to sign Stash → your backend webhook requests via HMAC
  • Create and manage API keys in Stash StudioAPI Secrets
  • Create and manage webhook secrets in Stash StudioWebhooks

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 Secret Type: Ensure you're using the correct secret:
    • ConfirmPayment: Base64-encoded egress API key
    • Webhooks: Webhook secret
  2. Check Base64 Encoding: For ConfirmPayment, ensure you're base64-encoding the egress API key before using it as the HMAC secret
  3. Verify Body: Ensure you're signing/verifying the exact request body (including whitespace)
  4. Check Encoding: Ensure body is UTF-8 encoded
  5. Test Mode: In test mode, verifying auth headers is optional

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?