Appcharge provides real-time, structured webhook events that report on key player activities such as logins, web store interactions, and orders. These events allow you to monitor behavior and trigger backend logic, such as initiating a workflow when an order is completed.

Webhook setup

To begin receiving events, you must:
  1. Create a webhook endpoint and a secret key for the signature, following the format described below.
  2. Register the events webhook and secret key with Appcharge via the Publisher Dashboard.
Once configured, Appcharge will send HTTPS POST requests to your endpoint containing JSON payloads that describe each event.

Event payload structure

Webhook events are sent as structured JSON objects, and the fields included can vary depending on the event type. Some fields may be optional, so your integration should account for missing fields gracefully to maintain reliability. This approach ensures compatibility as the event schema evolves and new fields are introduced.

Webhook signature verification

To ensure the authenticity and integrity of webhook events, Appcharge signs each request using a cryptographic signature. Only requests with valid signatures should be accepted. Signature validation protects you from:
  • Forged or malicious requests.
  • Payload tampering.
  • Replay attacks with outdated data.
  • Unauthorized access to internal systems.

Signature generation

We use HMAC-SHA256 with the following components:
  • Timestamp: Unix timestamp in milliseconds.
  • Payload: The full JSON body of the webhook.
  • Secret: Your private signing key.

Signature format

The signature is included in the signature header using this format:
signature=t={timestamp},v1={signature}

Headers included in the request

  • Content-Type: application/json
  • signature: t={timestamp},v1={signature}
  • x-project-id: 1234: Project ID. Located in the Publisher Dashboard under Settings > Company > Project ID.

Signature algorithm details

  • Algorithm: HMAC-SHA256
  • Input Format: {timestamp}.{json_payload}
  • Output: Hex-encoded string
  • Timestamp: Unix timestamp in milliseconds

Signature validation process

These steps demonstrate how you can validate the webhook before processing:

Step 1 | Extract signature components

const signatureHeader = req.headers['signature'];
const [timestamp, signature] = signatureHeader.split(',');
const timestampValue = timestamp.split('=')[1];
const signatureValue = signature.split('=')[1];

Step 2 | Validate timestamp (replay protection)

We recommend using a 5-minute tolerance window. Adjust this window according to your network conditions and security requirements.
const currentTime = Date.now();
const requestTime = parseInt(timestampValue);
const timeDiff = Math.abs(currentTime - requestTime);
const maxAge = 5 * 60 * 1000; // 5 minutes in milliseconds

if (timeDiff > maxAge) {
    throw new Error('The request is expired - possible replay attack.');
}

Step 3 | Reconstruct expected signature

const payloadToSign = `${timestampValue}.${JSON.stringify(req.body)}`;
const expectedSignature = crypto
    .createHmac('sha256', yourSecretKey)
    .update(payloadToSign)
    .digest('hex');

Step 4 | Compare signatures

if (signatureValue !== expectedSignature) {
    throw new Error('Invalid signature');
}

Complete code example

const crypto = require('crypto');

function validateWebhookSignature(req, secretKey) {
    try {
        // Get signature header
        const signatureHeader = req.headers['signature'];
        if (!signatureHeader) {
            throw new Error('Missing signature header');
        }

        // Parse signature components
        const parts = signatureHeader.split(',');
        const timestampPart = parts.find(part => part.startsWith('t='));
        const signaturePart = parts.find(part => part.startsWith('v1='));
        
        if (!timestampPart || !signaturePart) {
            throw new Error('Invalid signature format');
        }

        const timestamp = timestampPart.split('=')[1];
        const signature = signaturePart.split('=')[1];

        // Validate timestamp (prevent replay attacks)
        // PUBLISHER RESPONSIBILITY: Adjust tolerance based on your needs
        const currentTime = Date.now();
        const requestTime = parseInt(timestamp);
        const timeDiff = Math.abs(currentTime - requestTime);
        const maxAge = 5 * 60 * 1000; // 5 minutes - ADJUST AS NEEDED

        if (timeDiff > maxAge) {
            throw new Error('The request is expired - possible replay attack.');
        }

        // Reconstruct expected signature
        const payloadToSign = `${timestamp}.${JSON.stringify(req.body)}`;
        const expectedSignature = crypto
            .createHmac('sha256', secretKey)
            .update(payloadToSign)
            .digest('hex');

        // Compare signatures
        if (signature !== expectedSignature) {
            throw new Error('Invalid signature');
        }

        // Get project ID
        const projectId = req.headers['x-project-id'];
        console.log(`Valid webhook from project: ${projectId}`);

        return true;
    } catch (error) {
        console.error('Webhook validation failed:', error.message);
        return false;
    }
}

// Usage example
app.post('/webhook', (req, res) => {
    // PUBLISHER RESPONSIBILITY: Always validate before processing
    const isValid = validateWebhookSignature(req, 'your-secret-key');
    
    if (!isValid) {
        return res.status(401).json({ error: 'Invalid signature' });
    }
    
    // Only process webhook after successful validation
    console.log('Processing webhook:', req.body);
    res.status(200).json({ status: 'ok' });
});

Security best practices

  • Always validate webhook signatures before processing.
  • Keep your secret key private, and never expose it in frontend or logs.
  • Use HTTPS for all webhook communication.
  • Set an appropriate timestamp tolerance. Start with 5 minutes and adjust as needed.
  • Use realistic validation during development to prevent unsafe patterns.

Session ID

Some webhook events include a sessionId property that identifies the player’s current session. This value helps track player behavior across interactions such as logins, purchases, or visits to the web store. A session begins when a player logs in or returns to the web store, and it ends either after 30 minutes of inactivity or when the player logs out. If the player opens the store in multiple tabs within the same browser, those tabs will share the same sessionId. However, accessing the store in a different browser or on another device will generate a new session ID. Use the sessionId to group related events and gain insight into player activity within a single session.