Skip to content

Node.js Examples

Ready-to-use Node.js code for common Pixlpay API operations.

Setup

Using fetch (Node 18+)

javascript
class PixlpayClient {
    constructor(subdomain, apiToken) {
        this.baseUrl = `https://${subdomain}.pixlpay.net/api/external/v1`;
        this.apiToken = apiToken;
    }

    async request(method, endpoint, data = null) {
        const options = {
            method,
            headers: {
                'Authorization': `Bearer ${this.apiToken}`,
                'Accept': 'application/json',
                'Content-Type': 'application/json',
            },
        };

        if (data) {
            options.body = JSON.stringify(data);
        }

        const response = await fetch(`${this.baseUrl}${endpoint}`, options);
        const json = await response.json();

        return {
            status: response.status,
            data: json,
        };
    }

    get(endpoint) {
        return this.request('GET', endpoint);
    }

    post(endpoint, data) {
        return this.request('POST', endpoint, data);
    }
}

module.exports = PixlpayClient;

Fetching Products

javascript
const PixlpayClient = require('./pixlpay-client');

const client = new PixlpayClient('yourstore', process.env.PIXLPAY_TOKEN);

async function listProducts() {
    const response = await client.get('/products');

    if (response.status === 200) {
        response.data.data.forEach(product => {
            console.log(`${product.name} - $${product.price}`);
        });
    }
}

// Get single product
async function getProduct(id) {
    const response = await client.get(`/products/${id}`);
    return response.data.data;
}

Fetching Orders

javascript
const client = new PixlpayClient('yourstore', process.env.PIXLPAY_TOKEN);

// Get pending orders
async function getPendingOrders() {
    const response = await client.get('/orders?status=pending');
    return response.data.data;
}

// Get orders by date range
async function getOrdersByDate(from, to) {
    const response = await client.get(`/orders?from=${from}&to=${to}`);
    return response.data.data;
}

Fulfilling Orders

javascript
const client = new PixlpayClient('yourstore', process.env.PIXLPAY_TOKEN);

async function fulfillOrder(orderId) {
    const response = await client.post(`/orders/${orderId}/fulfill`, {
        note: 'Delivered via API'
    });

    if (response.status === 200) {
        console.log('Order fulfilled successfully!');
        return true;
    }
    return false;
}

Webhook Signature Verification

Pixlpay signs all webhooks with HMAC-SHA256. Always verify signatures before processing webhook payloads.

Signature Verification Function

javascript
const crypto = require('crypto');

/**
 * Verify webhook signature using HMAC-SHA256.
 *
 * @param {Buffer|string} payload - Raw request body
 * @param {string} signature - Value from X-Webhook-Signature header
 * @param {string} secret - Your webhook secret
 * @returns {boolean} True if signature is valid
 */
function verifyWebhookSignature(payload, signature, secret) {
    // Compute HMAC-SHA256 signature
    const computed = crypto
        .createHmac('sha256', secret)
        .update(payload)
        .digest('hex');

    // Use timing-safe comparison to prevent timing attacks
    try {
        return crypto.timingSafeEqual(
            Buffer.from(computed, 'utf8'),
            Buffer.from(signature, 'utf8')
        );
    } catch (e) {
        // Lengths don't match - signatures are definitely different
        return false;
    }
}

module.exports = { verifyWebhookSignature };

Express.js Webhook Handler

javascript
const express = require('express');
const crypto = require('crypto');

const app = express();

/**
 * Verify webhook signature using HMAC-SHA256.
 */
function verifyWebhookSignature(payload, signature, secret) {
    const computed = crypto
        .createHmac('sha256', secret)
        .update(payload)
        .digest('hex');

    try {
        return crypto.timingSafeEqual(
            Buffer.from(computed, 'utf8'),
            Buffer.from(signature, 'utf8')
        );
    } catch (e) {
        return false;
    }
}

// IMPORTANT: Use express.raw() to get the raw body before JSON parsing
// This must come BEFORE any express.json() middleware for this route
app.post('/webhooks/pixlpay',
    express.raw({ type: 'application/json' }),
    (req, res) => {
        // Get headers
        const signature = req.headers['x-webhook-signature'] || '';
        const eventType = req.headers['x-webhook-event'] || '';
        const deliveryId = req.headers['x-webhook-id'] || '';

        // Get secret from environment
        const secret = process.env.PIXLPAY_WEBHOOK_SECRET;

        // Verify signature FIRST
        if (!verifyWebhookSignature(req.body, signature, secret)) {
            console.error(`Invalid webhook signature for delivery: ${deliveryId}`);
            return res.status(401).send('Invalid signature');
        }

        // Now safe to parse the payload
        const event = JSON.parse(req.body.toString());
        const data = event.data;

        // Handle different events
        switch (eventType) {
            case 'order.received':
                handleOrderReceived(data);
                break;
            case 'order.refunded':
                handleOrderRefunded(data);
                break;
            case 'subscription.created':
                handleSubscriptionCreated(data);
                break;
            case 'subscription.cancelled':
                handleSubscriptionCancelled(data);
                break;
            case 'delivery.completed':
                handleDeliveryCompleted(data);
                break;
            case 'delivery.failed':
                handleDeliveryFailed(data);
                break;
            default:
                console.log(`Unhandled event type: ${eventType}`);
        }

        // Respond quickly with 200
        res.sendStatus(200);
    }
);

// Event handlers
function handleOrderReceived(order) {
    console.log(`Order received: ${order.order_number}`);
    // Your delivery logic here
}

function handleOrderRefunded(order) {
    console.log(`Order refunded: ${order.order_number}`);
    // Revoke access, remove roles, etc.
}

function handleSubscriptionCreated(subscription) {
    console.log(`Subscription created: ${subscription.id}`);
    // Grant recurring access
}

function handleSubscriptionCancelled(subscription) {
    console.log(`Subscription cancelled: ${subscription.id}`);
    // Revoke recurring access
}

function handleDeliveryCompleted(delivery) {
    console.log(`Delivery completed: ${delivery.id}`);
}

function handleDeliveryFailed(delivery) {
    console.log(`Delivery failed: ${delivery.id}`);
    // Alert admin, retry logic, etc.
}

app.listen(3000, () => {
    console.log('Webhook server listening on port 3000');
});

Webhook Verification with Idempotency

Prevent replay attacks by tracking processed webhooks:

javascript
const express = require('express');
const crypto = require('crypto');

const app = express();

// Simple in-memory store for demo - use Redis in production
const processedWebhooks = new Map();

// Clean up old entries every hour
setInterval(() => {
    const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000;
    for (const [key, timestamp] of processedWebhooks) {
        if (timestamp < oneDayAgo) {
            processedWebhooks.delete(key);
        }
    }
}, 60 * 60 * 1000);

function verifyWebhookSignature(payload, signature, secret) {
    const computed = crypto
        .createHmac('sha256', secret)
        .update(payload)
        .digest('hex');

    try {
        return crypto.timingSafeEqual(
            Buffer.from(computed, 'utf8'),
            Buffer.from(signature, 'utf8')
        );
    } catch (e) {
        return false;
    }
}

app.post('/webhooks/pixlpay',
    express.raw({ type: 'application/json' }),
    (req, res) => {
        const signature = req.headers['x-webhook-signature'] || '';
        const deliveryId = req.headers['x-webhook-id'] || '';
        const secret = process.env.PIXLPAY_WEBHOOK_SECRET;

        // Verify signature
        if (!verifyWebhookSignature(req.body, signature, secret)) {
            return res.status(401).send('Invalid signature');
        }

        // Check for replay attack (idempotency)
        if (processedWebhooks.has(deliveryId)) {
            // Already processed - return success but don't reprocess
            return res.status(200).send('Already processed');
        }

        // Process the webhook...
        const event = JSON.parse(req.body.toString());
        processEvent(event);

        // Mark as processed
        processedWebhooks.set(deliveryId, Date.now());

        res.sendStatus(200);
    }
);

function processEvent(event) {
    console.log(`Processing event: ${event.event_type}`);
    // Your processing logic here
}

app.listen(3000);

Using with Redis for Production Idempotency

javascript
const express = require('express');
const crypto = require('crypto');
const Redis = require('ioredis');

const app = express();
const redis = new Redis(process.env.REDIS_URL);

function verifyWebhookSignature(payload, signature, secret) {
    const computed = crypto
        .createHmac('sha256', secret)
        .update(payload)
        .digest('hex');

    try {
        return crypto.timingSafeEqual(
            Buffer.from(computed, 'utf8'),
            Buffer.from(signature, 'utf8')
        );
    } catch (e) {
        return false;
    }
}

app.post('/webhooks/pixlpay',
    express.raw({ type: 'application/json' }),
    async (req, res) => {
        const signature = req.headers['x-webhook-signature'] || '';
        const deliveryId = req.headers['x-webhook-id'] || '';
        const secret = process.env.PIXLPAY_WEBHOOK_SECRET;

        // Verify signature
        if (!verifyWebhookSignature(req.body, signature, secret)) {
            return res.status(401).send('Invalid signature');
        }

        // Check for replay attack using Redis
        const cacheKey = `pixlpay_webhook:${deliveryId}`;
        const exists = await redis.exists(cacheKey);

        if (exists) {
            return res.status(200).send('Already processed');
        }

        // Process the webhook
        const event = JSON.parse(req.body.toString());
        await processEvent(event);

        // Mark as processed (expire after 24 hours)
        await redis.setex(cacheKey, 86400, '1');

        res.sendStatus(200);
    }
);

async function processEvent(event) {
    console.log(`Processing: ${event.event_type}`);
    // Your async processing logic here
}

app.listen(3000);

Creating Webhooks

javascript
const client = new PixlpayClient('yourstore', process.env.PIXLPAY_TOKEN);

async function createWebhook() {
    const response = await client.post('/webhooks', {
        url: 'https://yoursite.com/webhooks/pixlpay',
        events: ['order.received', 'order.refunded', 'subscription.cancelled']
    });

    if (response.status === 201) {
        const webhook = response.data.data;
        console.log(`Webhook created! Secret: ${webhook.secret}`);

        // IMPORTANT: Store this secret securely - you'll need it for verification
        // Save to .env or secure config
        return webhook;
    }
}

Analytics

javascript
const client = new PixlpayClient('yourstore', process.env.PIXLPAY_TOKEN);

async function getRevenueStats() {
    const response = await client.get('/analytics/revenue?from=2025-01-01&to=2025-01-31');

    if (response.status === 200) {
        const { total_revenue, total_orders, average_order_value } = response.data.data;
        console.log(`Revenue: $${total_revenue}`);
        console.log(`Orders: ${total_orders}`);
        console.log(`AOV: $${average_order_value}`);
    }
}

Error Handling

javascript
const client = new PixlpayClient('yourstore', process.env.PIXLPAY_TOKEN);

async function safeRequest() {
    try {
        const response = await client.get('/products/99999');

        if (response.status !== 200) {
            console.error(`Error: ${response.data.message}`);

            switch (response.status) {
                case 401:
                    console.error('Check your API token');
                    break;
                case 404:
                    console.error('Resource not found');
                    break;
                case 429:
                    console.error('Rate limited - slow down!');
                    break;
            }
        }
    } catch (error) {
        console.error('Network error:', error.message);
    }
}

Built for game developers, by game developers.