Authentication

Learn how to authenticate requests when integrating Stash Launcher. This guide covers API key authentication for build management, channel management, and token service operations.

Stash Launcher uses API keys to authenticate server-side operations for build management and authentication token flows. All server-side Stash Launcher operations require API key authentication.

When API Keys Are Required

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

  • Build Management - Creating, updating, and querying build artifacts
  • Channel Management - Setting which builds are on which channels
  • Token Management - Saving authentication tokens for launcher deeplink flows

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., "Launcher Build Service", "Launcher Production").

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.

Using API Keys

Authentication Header

Include your API key in the X-Stash-Api-Key header for all authenticated requests:

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.

Build Management Operations

Get Build Status

Node.js - Get Build Status
const axios = require('axios');

async function getBuildStatus() {
  const response = await axios.get(
    'https://api.stash.gg/api/v1/launcher/status',
    {
      headers: {
        'X-Stash-Api-Key': process.env.STASH_API_KEY
      }
    }
  );
  return response.data;
}

Create Build and Generate Upload URL

Node.js - Create Build
async function createBuildAndGetUploadUrl(buildName, fileInfo) {
  const response = await axios.post(
    'https://api.stash.gg/api/v1/launcher/builds/create',
    {
      name: buildName,
      file: {
        name: fileInfo.name,
        crc32c: fileInfo.crc32c
      }
    },
    {
      headers: {
        'X-Stash-Api-Key': process.env.STASH_API_KEY,
        'Content-Type': 'application/json'
      }
    }
  );
  
  return {
    buildId: response.data.id,
    uploadUrl: response.data.signed_resumable_post_url,
    headers: response.data.headers
  };
}

Get Build Artifact

Node.js - Get Build Artifact
async function getBuildArtifact(buildId) {
  const response = await axios.get(
    `https://api.stash.gg/api/v1/launcher/builds/${buildId}`,
    {
      headers: {
        'X-Stash-Api-Key': process.env.STASH_API_KEY
      }
    }
  );
  return response.data;
}

Update Build Artifact

Node.js - Update Build Artifact
async function updateBuildArtifact(buildId, executable) {
  const response = await axios.patch(
    `https://api.stash.gg/api/v1/launcher/builds/${buildId}`,
    {
      update: {
        executable: {
          win_exe: executable.windows, // or mac_exe for Mac
        }
      }
    },
    {
      headers: {
        'X-Stash-Api-Key': process.env.STASH_API_KEY,
        'Content-Type': 'application/json'
      }
    }
  );
  return response.data;
}

Complete Build Upload Flow

Node.js - Complete Build Upload Example
const axios = require('axios');
const fs = require('fs');
const FormData = require('form-data');

async function uploadBuild(buildName, buildFilePath) {
  const apiKey = process.env.STASH_API_KEY;
  
  // Step 1: Create build and get upload URL
  const fileStats = fs.statSync(buildFilePath);
  const fileBuffer = fs.readFileSync(buildFilePath);
  const crc32c = calculateCRC32C(fileBuffer); // You'll need a CRC32C library
  
  const createResponse = await axios.post(
    'https://api.stash.gg/api/v1/launcher/builds/create',
    {
      name: buildName,
      file: {
        name: path.basename(buildFilePath),
        crc32c: crc32c
      }
    },
    {
      headers: {
        'X-Stash-Api-Key': apiKey,
        'Content-Type': 'application/json'
      }
    }
  );
  
  const { id: buildId, signed_resumable_post_url: uploadUrl, headers: uploadHeaders } = createResponse.data;
  
  // Step 2: Upload file to signed URL
  const formData = new FormData();
  formData.append('file', fs.createReadStream(buildFilePath));
  
  await axios.post(uploadUrl, formData, {
    headers: {
      ...uploadHeaders,
      ...formData.getHeaders()
    }
  });
  
  // Step 3: Wait for processing (poll GetBuildArtifact)
  let build = null;
  let attempts = 0;
  while (attempts < 30) { // Poll for up to 5 minutes
    await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10 seconds
    
    const getResponse = await axios.get(
      `https://api.stash.gg/api/v1/launcher/builds/${buildId}`,
      {
        headers: {
          'X-Stash-Api-Key': apiKey
        }
      }
    );
    
    build = getResponse.data;
    if (build.post_process_metadata) {
      break; // Processing complete
    }
    attempts++;
  }
  
  if (!build.post_process_metadata) {
    throw new Error('Build processing timed out');
  }
  
  // Step 4: Update build with executable paths
  const executables = extractExecutables(build.post_process_metadata);
  
  await axios.patch(
    `https://api.stash.gg/api/v1/launcher/builds/${buildId}`,
    {
      update: {
        executable: {
          win_exe: executables.windows,
          mac_exe: executables.mac
        }
      }
    },
    {
      headers: {
        'X-Stash-Api-Key': apiKey,
        'Content-Type': 'application/json'
      }
    }
  );
  
  return buildId;
}

Channel Management

Set Build Artifact Channel

Node.js - Set Build Channel
async function setBuildArtifactChannel(partnerId, shopId, channelId, platform, artifactId) {
  const response = await axios.post(
    'https://api.stash.gg/api/v1/launcher/channels/set',
    {
      partner_id: partnerId,
      shop_id: shopId,
      channel_id: channelId,
      platform: platform, // 'WIN_PLATFORM' or 'MAC_PLATFORM'
      artifact_id: artifactId
    },
    {
      headers: {
        'X-Stash-Api-Key': process.env.STASH_API_KEY,
        'Content-Type': 'application/json'
      }
    }
  );
  return response.data;
}

Overview

The SDK Token Service allows your game backend to temporarily save authentication tokens that can be retrieved via launcher deeplinks. This enables seamless authentication when users click launcher links.

Flow

  1. User authenticates in game → Game backend has access/refresh tokens
  2. Game backend saves tokens → Calls SaveTokens with API key
  3. Stash returns code_challenge → Short code that can be passed via deeplink
  4. Game opens launcher with deeplink → Includes code_challenge
  5. Launcher retrieves tokens → Calls GetTokens with code_challenge
  6. Launcher uses tokens → Authenticates user automatically

Save Tokens Example

Node.js - Save Tokens for Launcher
// Called by your game backend after user authenticates
async function saveTokensForLauncher(accessToken, refreshToken) {
  const response = await axios.post(
    'https://api.stash.gg/api/v1/sdk/launcher/tokens/save',
    {
      access_token: accessToken,
      refresh_token: refreshToken
    },
    {
      headers: {
        'X-Stash-Api-Key': process.env.STASH_API_KEY,
        'Content-Type': 'application/json'
      }
    }
  );
  
  // Returns code_challenge to include in launcher deeplink
  // e.g., "stash-launcher://auth?code=abc123"
  return response.data.code_challenge;
}

The code_challenge is scoped to your shop and expires after 30 minutes. It can only be used once.

Common Pitfalls

Build Processing Timeouts

Common Mistake: Not waiting long enough for build processing to complete

Solution: Build processing can take several minutes. Poll the build status every 10-15 seconds and allow up to 5 minutes for processing. Check post_process_metadata to confirm completion.

Common Mistake: Updating build with executable paths before processing completes

Solution: Wait for post_process_metadata to be available before updating the build with executable paths.

Channel Assignment Mistakes

Common Mistake: Assigning a build to a channel before it's fully processed

Solution: Only assign builds to channels after processing is complete and executable paths are set.

Common Mistake: Using incorrect platform identifier

Solution: Use exact platform identifiers: WIN_PLATFORM or MAC_PLATFORM (case-sensitive).

Common Mistake: Using expired or already-used code_challenge

Solution: Code challenges expire after 30 minutes and can only be used once. Generate a new code_challenge for each authentication flow.

Common Mistake: Not including code_challenge in launcher deeplink

Solution: Ensure the deeplink includes the code_challenge returned from SaveTokens (e.g., stash-launcher://auth?code=abc123).

Build Upload Failures

Common Mistake: Uploading file before creating the build record

Solution: Always create the build first to get the signed upload URL, then upload the file to that URL.

Common Mistake: Not including required headers in file upload

Solution: Include all headers returned from the build creation response when uploading the file.

Common Mistake: Incorrect CRC32C checksum

Solution: Calculate and include the correct CRC32C checksum when creating the build. Mismatched checksums will cause upload failures.

Error Handling

Common 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 in your request.

403 Permission Denied

Error: invalid key or PermissionDenied

Causes:

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

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

Build-Specific Errors

Build Not Found

Error: NotFound when querying a build

Causes:

  • Build ID doesn't exist
  • Build belongs to a different shop

Solution: Verify the build ID and ensure it belongs to your shop.

Processing Not Complete

When polling for build processing, if post_process_metadata is not available:

  • Wait longer (processing can take several minutes)
  • Check build status in Studio
  • Verify the uploaded file was valid

Security Best Practices

1. Store API Keys Securely

DO:

  • Store API keys in environment variables
  • Use secrets management services (AWS Secrets Manager, Azure Key Vault, etc.)
  • Never commit API keys to version control

DON'T:

  • Hardcode API keys in source code
  • Share API keys via email or chat
  • Expose API keys in client-side code

2. Use Different Keys for Different Environments

  • Development/Testing: Test environment keys
  • Staging: Staging environment keys
  • Production: Production environment keys

This allows you to:

  • Revoke test keys without affecting production
  • Monitor usage per environment
  • Limit blast radius if a key is compromised

3. Rotate Keys Regularly

  • Rotate keys every 90 days
  • Or immediately if compromised
  • Create new key before deleting old one

4. Monitor Usage

  • Log all API key usage
  • Set up alerts for unusual activity
  • Track which services use which keys

5. Least Privilege

  • Create separate keys for different services if needed
  • Delete unused keys
  • Use descriptive names to identify key purpose

Complete Integration Example

Node.js - Complete Launcher Service Example
class LauncherService {
  constructor(apiKey, partnerId, shopId) {
    this.apiKey = apiKey;
    this.partnerId = partnerId;
    this.shopId = shopId;
    this.baseUrl = 'https://api.stash.gg/api/v1/launcher';
  }
  
  async getStatus() {
    const response = await axios.get(`${this.baseUrl}/status`, {
      headers: { 'X-Stash-Api-Key': this.apiKey }
    });
    return response.data;
  }
  
  async createBuild(name, filePath) {
    // Create build
    const createResponse = await axios.post(
      `${this.baseUrl}/builds/create`,
      { 
        name, 
        file: { 
          name: path.basename(filePath), 
          crc32c: this.calculateCRC32C(filePath) 
        } 
      },
      { 
        headers: { 
          'X-Stash-Api-Key': this.apiKey, 
          'Content-Type': 'application/json' 
        } 
      }
    );
    
    const { id, signed_resumable_post_url, headers } = createResponse.data;
    
    // Upload file
    await this.uploadFile(signed_resumable_post_url, headers, filePath);
    
    // Wait for processing and update
    return await this.waitForProcessingAndUpdate(id);
  }
  
  async setChannel(channelId, platform, artifactId) {
    const response = await axios.post(
      `${this.baseUrl}/channels/set`,
      {
        partner_id: this.partnerId,
        shop_id: this.shopId,
        channel_id: channelId,
        platform,
        artifact_id: artifactId
      },
      {
        headers: {
          'X-Stash-Api-Key': this.apiKey,
          'Content-Type': 'application/json'
        }
      }
    );
    return response.data;
  }
  
  async saveTokens(accessToken, refreshToken) {
    const response = await axios.post(
      'https://api.stash.gg/api/v1/sdk/launcher/tokens/save',
      {
        access_token: accessToken,
        refresh_token: refreshToken
      },
      {
        headers: {
          'X-Stash-Api-Key': this.apiKey,
          'Content-Type': 'application/json'
        }
      }
    );
    return response.data.code_challenge;
  }
}

// Usage
const launcher = new LauncherService(
  process.env.STASH_API_KEY,
  'your-partner-id',
  'your-shop-id'
);

// Get available channels
const status = await launcher.getStatus();
console.log('Available channels:', status.channels);

// Create and upload a new build
const buildId = await launcher.createBuild('v1.2.3', './build.zip');

// Set build to a channel
await launcher.setChannel('stable', 'WIN_PLATFORM', buildId);

How is this guide?