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:
- Generate a signature using HMAC-SHA256 algorithm
- Use your secret key as the HMAC key
- Hash the entire request body (raw JSON string)
- 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 itemdescription
(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 purchasecanceled
: 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
- Item Not Found: Return 404 with error details
- Database Errors: Return 500 and cancel the order
- Validation Failures: Return 400 with validation messages
- Inventory Full: Cancel order with appropriate message
- 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:
- Extract the signature from the
X-NP-Signature
header - Remove the
sha256=
prefix to get the hex signature - Generate your own HMAC-SHA256 hash using:
- Your webhook secret as the key
- The raw request body as the data
- Compare signatures using a timing-safe comparison function
- 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
-
Valid Purchase Flow
- Test item_order_request with valid item
- Test item_order_placed completion
-
Error Scenarios
- Unknown item IDs
- Malformed order_info
- Server errors during item award
-
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
- Cache Item Definitions: Cache frequently requested items
- Async Operations: Use async/await for database operations
- Connection Pooling: Implement database connection pooling
- 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.