LogoNightPixel

Dynamic Purchasing API

Backend developer guide for implementing dynamic item purchasing with webhook handlers

Dynamic Purchasing API

The NightPixel Dynamic Purchasing API allows players to purchase virtual items without requiring pre-defined item catalogs. Your backend server dynamically defines items through webhook callbacks, enabling flexible in-game economies and real-time item generation.

Overview

The Dynamic Purchasing system works differently from traditional fixed-catalog purchasing. Instead of pre-defining items in the NightPixel platform, your game passes an order_info string to initiate purchases, and your backend webhook defines the items dynamically.

🚧 Important

Games using dynamic purchasing must have a webhook URL configured and require approval before going live. You can test the API during development, but production deployment requires QA approval.

Webhook Implementation

Your backend must handle two webhook events: item_order_request and item_order_placed. Configure your webhook URL in the NightPixel developer dashboard.

Webhook Security

NightPixel can optionally sign webhook requests with a secret key to ensure they originate from our servers and haven't been tampered with in transit. This uses the same security approach as GitHub webhooks.

How Webhook Signing Works

When webhook signing is enabled, NightPixel will:

  1. Generate a signature using HMAC-SHA256 algorithm
  2. Use your secret key as the HMAC key
  3. Hash the entire request body (raw JSON string)
  4. Add the signature to the X-NP-Signature header

Header Details

Header Name: X-NP-Signature
Algorithm: HMAC-SHA256
Format: sha256=<hex_signature>

X-NP-Signature: sha256=a1b2c3d4e5f6789...

The signature is the hexadecimal representation of the HMAC-SHA256 hash, prefixed with sha256= to indicate the algorithm used.

Configuration

  • Enable Signing: Provide your secret key when configuring your webhook URL in the developer dashboard
  • Disable Signing: Leave the secret field empty - no X-NP-Signature header will be sent
  • Secret Requirements: Use a strong, randomly generated secret (minimum 32 characters recommended)

Webhook Event: item_order_request

Called when an order needs item definitions. Your webhook must return the items to display to the user.

Webhook Payload

{
  "event": "item_order_request",
  "game_id": 10000,
  "buyer_id": "user-123",
  "recipient_id": "user-123",
  "order_id": 12345,
  "order_info": "sword"
}

Required Response

Your webhook must return a JSON object with an items array:

{
  "items": [
    {
      "name": "Awesome Sword",
      "description": "A really neat sword!",
      "price": 10,
      "image_url": "https://media.giphy.com/media/3o85xFGXZQIC29z74Q/giphy.gif"
    }
  ]
}

Item Definition Fields

  • name (required): Display name of the item
  • description (required): Item description (255 character max)
  • price (required): Price in tokens (integer)
  • image_url (required): URL to item icon (displayed at 40x40 pixels)

Webhook Event: item_order_placed

Called when the user confirms the purchase. Your webhook should award the items and return completion status.

Webhook Payload

{
  "event": "item_order_placed",
  "game_id": 10000,
  "buyer_id": "user-123",
  "recipient_id": "user-123",
  "order_id": 12345,
  "order_info": "sword"
}

Required Response

Return completion status:

{
  "state": "completed"
}

States:

  • completed: Award items and complete the purchase
  • canceled: Cancel the order and refund tokens to the user

Complete Implementation Example

Here's a production-ready Cloudflare Workers implementation:

/**
 * NightPixel Dynamic Purchasing Webhook Handler
 * Cloudflare Workers implementation
 */

// Item catalog - in production, this would be in a database
const items = {
  sword: {
    name: "Awesome Sword",
    description: "A really neat sword with +10 attack power!",
    price: 10,
    image_url: "https://cdn.nightpixel.com/items/sword.png",
  },
  "monthly-card": {
    name: "Monthly Premium Card",
    description: "Unlock exclusive benefits and premium features for 30 days",
    price: 960,
    image_url: "https://cdn.nightpixel.com/items/premium-card.png",
  },
  "health-potion": {
    name: "Health Potion",
    description: "Restores 100 HP instantly",
    price: 5,
    image_url: "https://cdn.nightpixel.com/items/health-potion.png",
  },
};

export default {
  async fetch(request, env, ctx) {
    // Verify this is a POST request
    if (request.method !== "POST") {
      return new Response("Method not allowed", { status: 405 });
    }

    // Get the raw body for signature verification
    const body = await request.text();

    // Verify webhook signature if secret is configured
    if (env.WEBHOOK_SECRET) {
      const signature = request.headers.get("X-NP-Signature");
      if (!signature) {
        return new Response("Missing signature", { status: 401 });
      }

      const isValid = await verifyWebhookSignature(
        body,
        signature,
        env.WEBHOOK_SECRET
      );
      if (!isValid) {
        return new Response("Invalid signature", { status: 401 });
      }
    }

    let webhook;
    try {
      webhook = JSON.parse(body);
    } catch (error) {
      return new Response("Invalid JSON", { status: 400 });
    }

    // Validate required webhook fields
    if (!webhook.event || !webhook.order_id || !webhook.order_info) {
      return new Response("Missing required fields", { status: 400 });
    }

    let result = { success: true };

    try {
      switch (webhook.event) {
        case "item_order_request":
          result = await handleItemOrderRequest(webhook, env);
          break;

        case "item_order_placed":
          result = await handleItemOrderPlaced(webhook, env);
          break;

        default:
          return new Response("Unknown event type", { status: 400 });
      }
    } catch (error) {
      console.error("Webhook processing error:", error);
      return new Response("Internal server error", { status: 500 });
    }

    const response = new Response(JSON.stringify(result));
    response.headers.set("Content-Type", "application/json");
    return response;
  },
};

/**
 * Verify webhook signature using HMAC-SHA256
 */
async function verifyWebhookSignature(body, signature, secret) {
  try {
    // Remove 'sha256=' prefix from signature
    const expectedSignature = signature.replace("sha256=", "");

    // Generate HMAC-SHA256 hash of the body
    const encoder = new TextEncoder();
    const key = encoder.encode(secret);
    const data = encoder.encode(body);

    const cryptoKey = await crypto.subtle.importKey(
      "raw",
      key,
      { name: "HMAC", hash: "SHA-256" },
      false,
      ["sign"]
    );

    const signatureBuffer = await crypto.subtle.sign("HMAC", cryptoKey, data);
    const generatedSignature = Array.from(new Uint8Array(signatureBuffer))
      .map((b) => b.toString(16).padStart(2, "0"))
      .join("");

    return generatedSignature === expectedSignature;
  } catch (error) {
    console.error("Signature verification failed:", error);
    return false;
  }
}

/**
 * Handle item definition request
 */
async function handleItemOrderRequest(webhook, env) {
  const { order_info, buyer_id, order_id } = webhook;

  // Log the request for debugging
  console.log(
    `Item order request: ${order_id} for ${order_info} by ${buyer_id}`
  );

  const itemId = order_info;

  // Look up item definition
  const item = items[itemId];
  if (!item) {
    throw new Error(`Item not found: ${itemId}`);
  }

  // Return item definition
  return {
    items: [
      {
        name: item.name,
        description: item.description,
        price: item.price,
        image_url: item.image_url,
      },
    ],
  };
}

/**
 * Handle purchase completion
 */
async function handleItemOrderPlaced(webhook, env) {
  const { order_info, buyer_id, recipient_id, order_id } = webhook;

  console.log(
    `Processing purchase: ${order_id} for ${order_info} by ${buyer_id}`
  );

  const itemId = order_info;

  // Validate the item exists
  const item = items[itemId];
  if (!item) {
    console.error(`Item not found during purchase: ${itemId}`);
    return { state: "canceled" };
  }

  try {
    // Award items to the player
    await awardItemsToPlayer(recipient_id, itemId, env);

    // Log successful purchase
    await logPurchase(
      order_id,
      buyer_id,
      recipient_id,
      itemId,
      item.price,
      env
    );

    return { state: "completed" };
  } catch (error) {
    console.error(`Failed to award items: ${error.message}`);
    return { state: "canceled" };
  }
}

/**
 * Award items to player's inventory
 */
async function awardItemsToPlayer(playerId, itemId, env) {
  // In production, this would update your game database
  // Example with Cloudflare D1 or external database:

  /*
  const stmt = env.DB.prepare(`
    INSERT INTO player_inventory (player_id, item_id, awarded_at)
    VALUES (?, ?, ?)
    ON CONFLICT(player_id, item_id)
    DO UPDATE SET quantity = quantity + 1
  `);

  await stmt.bind(playerId, itemId, new Date().toISOString()).run();
  */

  console.log(`Awarded ${itemId} to player ${playerId}`);
}

/**
 * Log purchase for analytics and support
 */
async function logPurchase(
  orderId,
  buyerId,
  recipientId,
  itemId,
  totalPrice,
  env
) {
  // In production, log to your analytics system
  /*
  const stmt = env.DB.prepare(`
    INSERT INTO purchase_log (order_id, buyer_id, recipient_id, item_id, total_price, created_at)
    VALUES (?, ?, ?, ?, ?, ?)
  `);

  await stmt.bind(orderId, buyerId, recipientId, itemId, totalPrice, new Date().toISOString()).run();
  */

  console.log(
    `Purchase logged: Order ${orderId} ${itemId} for ${totalPrice} tokens`
  );
}

Error Handling

Webhook Error Responses

Your webhook should handle errors gracefully:

// Handle invalid items
if (!item) {
  return new Response(JSON.stringify({
    error: "Item not found",
    item_id: itemId
  }), {
    status: 404,
    headers: { "Content-Type": "application/json" }
  });
}

// Handle server errors
catch (error) {
  console.error('Database error:', error);
  return new Response(JSON.stringify({
    error: "Internal server error"
  }), {
    status: 500,
    headers: { "Content-Type": "application/json" }
  });
}

Common Error Scenarios

  1. Item Not Found: Return 404 with error details
  2. Database Errors: Return 500 and cancel the order
  3. Validation Failures: Return 400 with validation messages
  4. Inventory Full: Cancel order with appropriate message
  5. Player Restrictions: Check if player can receive items

Security Considerations

Webhook Signature Verification

Highly Recommended: Always verify webhook signatures in production to ensure requests originate from NightPixel servers.

Verification Process

To verify a webhook signature, your server must:

  1. Extract the signature from the X-NP-Signature header
  2. Remove the sha256= prefix to get the hex signature
  3. Generate your own HMAC-SHA256 hash using:
    • Your webhook secret as the key
    • The raw request body as the data
  4. Compare signatures using a timing-safe comparison function
  5. Reject requests with missing or invalid signatures

Important Security Notes

  • Use the raw request body: Hash the exact bytes received, not parsed JSON
  • Timing-safe comparison: Use dedicated functions like crypto.timingSafeEqual() to prevent timing attacks
  • Header case: The header name X-NP-Signature is case-insensitive in HTTP
  • UTF-8 encoding: Ensure consistent encoding when generating your hash
// Node.js/Express example
const crypto = require("crypto");

function verifySignature(body, signature, secret) {
  // Step 1: Remove the 'sha256=' prefix from the header value
  const expectedSignature = signature.replace("sha256=", "");

  // Step 2: Generate HMAC-SHA256 hash using your secret and the raw body
  const computedSignature = crypto
    .createHmac("sha256", secret) // Create HMAC with SHA256 algorithm
    .update(body, "utf8") // Hash the request body
    .digest("hex"); // Get hex representation

  // Step 3: Compare signatures
  return expectedSignature === computedSignature;
}

// Middleware to verify all webhook requests
app.use("/webhook", (req, res, next) => {
  // Extract the signature from the X-NP-Signature header
  const signature = req.headers["x-np-signature"];

  // Reject requests without signatures
  if (!signature) {
    return res.status(401).json({ error: "Missing X-NP-Signature header" });
  }

  // Verify the signature against your webhook secret
  if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  // Signature is valid, proceed to handle the webhook
  next();
});

Other Language Examples:

# Python/Flask example
import hmac
import hashlib

def verify_signature(body, signature, secret):
    # Remove 'sha256=' prefix from the signature header
    expected_signature = signature.replace('sha256=', '')

    # Generate HMAC-SHA256 hash using the webhook secret
    computed_signature = hmac.new(
        secret.encode('utf-8'),      # Convert secret to bytes
        body.encode('utf-8'),        # Convert body to bytes
        hashlib.sha256               # Use SHA256 algorithm
    ).hexdigest()                    # Get hex string

    # Use timing-safe comparison to prevent timing attacks
    return hmac.compare_digest(expected_signature, computed_signature)

# Flask route example
@app.route('/webhook', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-NP-Signature')

    if not signature:
        return {'error': 'Missing X-NP-Signature header'}, 401

    if not verify_signature(request.data.decode(), signature, os.environ['WEBHOOK_SECRET']):
        return {'error': 'Invalid signature'}, 401

    # Process webhook...
# PHP example
function verifySignature($body, $signature, $secret) {
    // Remove 'sha256=' prefix from the signature header
    $expectedSignature = str_replace('sha256=', '', $signature);

    // Generate HMAC-SHA256 hash using the webhook secret
    $computedSignature = hash_hmac('sha256', $body, $secret);

    // Use timing-safe comparison to prevent timing attacks
    return hash_equals($expectedSignature, $computedSignature);
}

// Example usage in webhook endpoint
$signature = $_SERVER['HTTP_X_NP_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');

if (empty($signature)) {
    http_response_code(401);
    echo json_encode(['error' => 'Missing X-NP-Signature header']);
    exit;
}

if (!verifySignature($body, $signature, $_ENV['WEBHOOK_SECRET'])) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Process webhook...

Input Validation

Always validate webhook payloads:

function validateWebhook(webhook) {
  const required = ["event", "game_id", "buyer_id", "order_id", "order_info"];

  for (const field of required) {
    if (!webhook[field]) {
      throw new Error(`Missing required field: ${field}`);
    }
  }

  // Validate game_id matches your game
  if (webhook.game_id !== YOUR_GAME_ID) {
    throw new Error("Invalid game_id");
  }

  // Validate event types
  const validEvents = ["item_order_request", "item_order_placed"];
  if (!validEvents.includes(webhook.event)) {
    throw new Error("Invalid event type");
  }
}

Test Cases

  1. Valid Purchase Flow

    • Test item_order_request with valid item
    • Test item_order_placed completion
  2. Error Scenarios

    • Unknown item IDs
    • Malformed order_info
    • Server errors during item award
  3. Edge Cases

    • Very long order_info strings
    • Special characters in item data
    • Network timeouts

Integration Checklist

  • Webhook URL shared with NightPixel team
  • Webhook secret configured for signature verification
  • Both webhook events handled (item_order_request, item_order_placed)
  • Webhook signature verification implemented
  • Item definitions return all required fields
  • Purchase completion awards items correctly
  • Error handling for invalid items
  • Input validation and sanitization
  • Logging for debugging and analytics
  • Rate limiting implemented
  • Database transactions for consistency
  • Idempotency for duplicate webhooks

Best Practices

Performance Optimization

  1. Cache Item Definitions: Cache frequently requested items
  2. Async Operations: Use async/await for database operations
  3. Connection Pooling: Implement database connection pooling
  4. Response Time: Keep webhook responses under 5 seconds

Ready to implement dynamic purchasing? Start by setting up your webhook endpoint and testing with the provided examples. For additional support, check our Server API Reference or Integration Guide.