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 (Ingress)
- 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
ConfirmPaymentendpoint. 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 Studio → Project Settings → API 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-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, 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 Type | Secret Used | Where to Find It |
|---|---|---|
ConfirmPayment | Base64-encoded Egress API Key | Stash Studio → Project Settings → API Secrets |
| Webhook Endpoints | Webhook Secret | Stash Studio → Project Settings → Webhooks |
ConfirmPaymentEndpoint — 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_SUCCEEDEDand 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:
- Stash retrieves your egress API key and base64-encodes it
- Stash generates an HMAC-SHA256 signature of the request body using
BASE64(egress_api_key)as the secret - Stash sends the signature in the
stash-hmac-signatureheader - 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
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
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
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, 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
- Mix up secrets between
ConfirmPaymentand 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 Studio → Project Settings → API Secrets)- Webhooks: Use your webhook secret (from Stash Studio → Project Settings → Webhooks)
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
ConfirmPaymentrequests via HMAC - Webhook secrets are used by Stash to sign Stash → your backend webhook requests via HMAC
- Create and manage API keys in Stash Studio → API Secrets
- Create and manage webhook secrets in Stash Studio → Webhooks
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 Secret Type: Ensure you're using the correct secret:
ConfirmPayment: Base64-encoded egress API key- Webhooks: Webhook secret
- Check Base64 Encoding: For
ConfirmPayment, ensure you're base64-encoding the egress API key before using it as the HMAC secret - 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, verifying auth headers is optional
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
- High-Level Flow Options - Understand
ConfirmPayment, webhooks, andGetPaymentEventpatterns - API Keys Guide
- Stash Pay Integration
- Project Settings
How is this guide?