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:
- API Keys - For authenticating your backend's requests to Stash
- 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-hereMake sure API key requests are made from your server, not the client, so the API key remains private.
Creating API Keys
Navigate to API Secrets
Go to Stash Studio → Project Settings → API 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
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
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
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
- Stash retrieves your egress API key (the same key you use for outgoing requests)
- Stash generates an HMAC signature of the request body using your API key
- Stash sends the signature in the
stash-hmac-signatureheader - 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
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
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
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, orhmac.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:
- Check API Key: Ensure you're using the correct API key
- Verify Body: Ensure you're signing/verifying the exact request body (including whitespace)
- Check Encoding: Ensure body is UTF-8 encoded
- Test Mode: In test mode, verify both HMAC and API key header if present
Common Integration Patterns
Pattern 1: Generate Checkout Link
// 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 requestPattern 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 confirmationSecurity 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
Related Documentation
How is this guide?