TicketWave Logo
Webhooks

उदाहरण सर्वर

TicketWave वेबहुक्स को प्राप्त और सत्यापित करने वाला एक पूरा, चलने योग्य Express सर्वर।

उदाहरण वेबहुक सर्वर

Express में एक न्यूनतम लेकिन production-shaped रिसीवर: यह signatures सत्यापित करता है, replays से बचाता है, retries को deduplicate करता है और कोई काम करने से पहले जवाब देता है।

इसे कॉपी करें, किसी endpoint को इसकी ओर point करें, और काम पूरा।

सेटअप

प्रोजेक्ट बनाएं

mkdir ticketwave-webhooks
cd ticketwave-webhooks
npm init -y
npm install express dotenv

नीचे Express 5 का उपयोग किया गया है, लेकिन यह code बिना बदलाव के Express 4 पर भी काम करता है।

सर्वर जोड़ें

अगले section से file को server.js के रूप में save करें।

अपने environment variables सेट करें

अपने project के root में एक .env file बनाएं और निम्न environment variables जोड़ें:

# .env
TICKETWAVE_WEBHOOK_SECRET="whsec_your_secret_here"
PORT=3000
  1. Dashboard → Webhooks → Endpoints से secret copy करें और उसे TICKETWAVE_WEBHOOK_SECRET में pass करें।
  2. अपनी machine पर एक open port चुनें और उसे PORT variable में set करें।

Secret को कभी भी server.js में hardcode न करें और न ही उसे commit करें। जिसके पास भी यह होगा, वह ऐसे requests forge कर सकता है जो आपकी signature check पास कर लें।

इसे चलाएं

node server.js

फिर dashboard में अपने endpoint पर Send test event दबाएं और console देखें।

सर्वर

server.js
const express = require('express');
const crypto = require('node:crypto');
const dotenv = require('dotenv');
dotenv.config();

const app = express();

const PORT = process.env.PORT || 3000;
const WEBHOOK_SECRET = process.env.TICKETWAVE_WEBHOOK_SECRET;

if (!WEBHOOK_SECRET) {
    console.error('Missing TICKETWAVE_WEBHOOK_SECRET');
    process.exit(1);
}

// Signature RAW body पर बनती है, इसलिए untouched bytes को वैसे ही रखें।
app.use('/webhooks/ticketwave', express.raw({ type: 'application/json' }));

// इससे पुरानी किसी भी request को reject करें, ताकि captured request बाद में replay न की जा सके।
const MAX_TIMESTAMP_AGE = 5 * 60; // 5 minutes

function verifySignature(req) {
    const Signature = req.get('X-TicketWave-Signature');
    const Timestamp = req.get('X-TicketWave-Timestamp');

    if (!Signature || !Timestamp) return false;

    // 1. Timestamp हाल का होना चाहिए
    const Age = Math.abs(Math.floor(Date.now() / 1000) - Number(Timestamp));
    if (!Number.isFinite(Age) || Age > MAX_TIMESTAMP_AGE) return false;

    // 2. `${timestamp}.${rawBody}` पर HMAC दोबारा compute करें
    const Expected = crypto
        .createHmac('sha256', WEBHOOK_SECRET)
        .update(`${Timestamp}.${req.body}`)
        .digest('hex');

    // 3. Constant time में compare करें
    const Received = Signature.replace('sha256=', '');
    const ExpectedBuffer = Buffer.from(Expected, 'hex');
    const ReceivedBuffer = Buffer.from(Received, 'hex');

    if (ExpectedBuffer.length !== ReceivedBuffer.length) return false;
    return crypto.timingSafeEqual(ExpectedBuffer, ReceivedBuffer);
}

// संभाली गई delivery ids याद रखें, क्योंकि retry वही event फिर से भेजता है।
const HandledDeliveries = new Set();

app.post('/webhooks/ticketwave', (req, res) => {
    if (!verifySignature(req)) {
        console.warn('Rejected a request with an invalid signature');
        return res.status(401).json({ error: 'invalid signature' });
    }

    const DeliveryId = req.get('X-TicketWave-Delivery');
    const Payload = JSON.parse(req.body);

    // तुरंत जवाब दें - आपके पास 10 seconds हैं, और धीमे replies retry हो जाते हैं।
    res.status(200).json({ received: true });

    // उस delivery को ignore करें जिसे हम पहले ही संभाल चुके हैं
    if (HandledDeliveries.has(DeliveryId)) return;
    HandledDeliveries.add(DeliveryId);

    handleEvent(Payload).catch((err) => {
        console.error(`Failed to handle ${Payload.event}:`, err);
    });
});

async function handleEvent(payload) {
    const { event, guild_id: guildId, data } = payload;

    // Dashboard का test button यही भेजता है
    if (data.test) {
        console.log(`Test event (${event}) received from guild ${guildId}`);
        return;
    }

    switch (event) {
        case 'ticket.created':
            console.log(`[${guildId}] ${data.ticket_id} opened by ${data.user?.username}`);
            break;

        case 'ticket.closed':
            console.log(`[${guildId}] ${data.ticket_id} closed by ${data.closed_by?.username} (${data.reason ?? 'no reason'})`);
            break;

        case 'ticket.updated':
            console.log(`[${guildId}] ${data.ticket_id} updated: ${data.action}`, data.changes);
            break;

        case 'message.sent':
            console.log(`[${guildId}] ${data.ticket_id} ${data.is_staff ? 'staff' : 'member'} ${data.author?.username}: ${data.content}`);
            break;

        case 'blacklist.added':
            console.log(`[${guildId}] ${data.user?.username} blacklisted (${data.reason ?? 'no reason'})`);
            break;

        case 'blacklist.removed':
            console.log(`[${guildId}] ${data.user?.username} removed from the blacklist`);
            break;

        default:
            // नए event types समय के साथ जोड़े जाते हैं - जो आप नहीं जानते, उस पर कभी throw न करें।
            console.log(`[${guildId}] Unhandled event ${event}`);
    }
}

app.listen(PORT, () => {
    console.log(`Listening for TicketWave webhooks on port ${PORT}`);
});

कोड ऐसा क्यों दिखता है

चार बातें गलत करना आसान है, और चारों ही silent failures हैं।

express.json की जगह express.raw

Signature TicketWave द्वारा भेजे गए exact bytes को cover करती है। express.json() body को object में parse करता है; उसे फिर से serialise करने से key order या whitespace बदल सकता है और digest अब match नहीं करेगा।

अगर आपका app globally express.json() इस्तेमाल करता है, तो उसे webhook route के बाद mount करें, या raw parser को ठीक उसी webhook path तक सीमित करें जैसा ऊपर दिखाया गया है। वरना JSON parser जीत जाएगा और req.body एक Buffer नहीं, बल्कि object होगा।

=== की जगह timingSafeEqual

Strings को === से compare करने पर जैसे ही दो bytes अलग होते हैं, comparison रुक जाता है। इसमें लगने वाला समय यह leak करता है कि signature का कितना हिस्सा सही था, और यह एक-एक byte brute-force करने के लिए काफी है। crypto.timingSafeEqual हमेशा समान समय लेता है।

यह तब भी throw करता है जब दोनों buffers की length अलग हो, इसलिए पहले length check किया जाता है।

Timestamp check

इसके बिना, कोई व्यक्ति जिसने valid request capture कर ली हो, उसे हमेशा के लिए replay कर सकता है। क्योंकि timestamp signed payload का हिस्सा है, signature तोड़े बिना उसे किसी fresh timestamp से बदला नहीं जा सकता।

काम करने से पहले जवाब देना

आपके पास 10 seconds हैं। इससे धीमा कुछ भी failure माना जाता है और retry होता है, इसलिए धीमी database write एक event को तीन बना सकती है। पहले 200 जवाब दें, फिर काम करें।

सही तरीके से deduplicate करना

ऊपर वाला Set demo के लिए ठीक है, लेकिन यह हमेशा बढ़ता रहता है और restart के बाद फिर से खाली हो जाता है। Production में, delivery id को ऐसी जगह store करें जहाँ वह बना रहे:

// किसी भी SQL database के साथ example
async function alreadyHandled(deliveryId) {
    const [rows] = await db.query(
        'SELECT 1 FROM webhook_deliveries WHERE delivery_id = ?',
        [deliveryId]
    );
    if (rows.length > 0) return true;

    await db.query(
        'INSERT INTO webhook_deliveries (delivery_id) VALUES (?)',
        [deliveryId]
    );
    return false;
}

delivery_id पर unique index होने से यह तब भी safe रहता है जब दो retries एक ही समय पर आ जाएं।

लोकल रूप से development

TicketWave private या loopback addresses पर endpoints को refuse करता है, इसलिए http://localhost:3000 सीधे इस्तेमाल नहीं किया जा सकता। अपने local server के सामने एक tunnel लगाएं और उससे मिलने वाला public HTTPS URL register करें:

# ngrok
ngrok http 3000

# or Cloudflare Tunnel
cloudflared tunnel --url http://localhost:3000

दिखाया गया https://….ngrok-free.app/webhooks/ticketwave URL अपने endpoint के रूप में register करें, और असली ticket को छूने से पहले wiring जांचने के लिए Send test event का उपयोग करें।

Free tunnel URLs हर restart पर बदल जाते हैं। ऐसा होने पर dashboard में endpoint URL अपडेट करें, वरना deliveries fail होने लगेंगी।

आगे क्या करें

Want to…Do this
High message volume handle करनाRoute में payload को queue पर push करें और उसे कहीं और process करें
कई endpoints चलानाहर एक का अपना secret होता है — route के हिसाब से सही वाला चुनें
किसी failing delivery को debug करनाDashboard → Webhooks में delivery खोलें और exact request तथा आपका response देखें
किसी event को फिर से भेजनाdelivery detail page पर Retry Webhook का उपयोग करें

अगले कदम

How is this guide?