Webhooks

Set up and handle webhook events from Discord, Telegram, and custom integrations. Includes signature verification and payload examples.

Webhooks

> Spray and Play receives webhook events from external services. Set up secure webhook

> endpoints to handle Discord bot interactions, Telegram messages, and custom integrations.

Overview

Our platform uses webhooks to receive events from:

ServiceEventsSecurity
DiscordBot commands, interactionsEd25519 signature
TelegramMessages, callbacksSecret token

These are incoming webhooks (we receive them), not outgoing webhooks.

---

Discord Webhooks

Endpoint

POST /api/webhooks/discord

Receives events from the Discord bot.

Events Received

EventTypeDescription
interaction_create2Slash command used
message_create1Direct message received
guild_member_add-New member joined server

Payload Example

{
  "type": 2,
  "id": "1234567890123456789",
  "token": "interaction_token_xyz",
  "application_id": "987654321098765432",
  "guild_id": "111111111111111111",
  "channel_id": "222222222222222222",
  "member": {
    "user": {
      "id": "555555555555555555",
      "username": "sprayer",
      "discriminator": "0001",
      "avatar": "abc123"
    },
    "roles": [],
    "joined_at": "2026-01-01T00:00:00.000000+00:00"
  },
  "data": {
    "id": "333333333333333333",
    "name": "balance",
    "type": 1,
    "options": []
  },
  "timestamp": "2026-02-16T15:30:00.000000+00:00"
}

Signature Verification

Discord webhooks include signature headers for verification:

import crypto from 'crypto';

const DISCORD_PUBLIC_KEY = process.env.DISCORD_PUBLIC_KEY!;

function verifyDiscordWebhook(
  payload: string,
  signature: string | null,
  timestamp: string | null
): boolean {
  if (!signature || !timestamp) {
    return false;
  }

  const message = timestamp + payload;
  const signatureBuffer = Buffer.from(signature, 'hex');
  const publicKeyBuffer = Buffer.from(DISCORD_PUBLIC_KEY, 'hex');

  try {
    return crypto.verify(
      'ed25519',
      Buffer.from(message),
      publicKeyBuffer,
      signatureBuffer
    );
  } catch {
    return false;
  }
}

// Next.js API Route Example
export async function POST(request: Request) {
  const signature = request.headers.get('x-signature-ed25519');
  const timestamp = request.headers.get('x-signature-timestamp');
  const payload = await request.text();

  // Verify signature
  if (!verifyDiscordWebhook(payload, signature, timestamp)) {
    return new Response('Invalid signature', { status: 401 });
  }

  const event = JSON.parse(payload);

  // Handle interaction
  switch (event.type) {
    case 1: // Ping
      return new Response(JSON.stringify({ type: 1 }), {
        headers: { 'Content-Type': 'application/json' }
      });
    
    case 2: // Application Command
      return handleSlashCommand(event);
    
    default:
      return new Response('Unknown event type', { status: 400 });
  }
}

async function handleSlashCommand(event: any) {
  const { name, options } = event.data;

  switch (name) {
    case 'balance':
      return new Response(JSON.stringify({
        type: 4, // Channel message with source
        data: {
          content: 'Your balance: $1,000.50'
        }
      }), {
        headers: { 'Content-Type': 'application/json' }
      });
    
    default:
      return new Response('Unknown command', { status: 400 });
  }
}

Discord Bot Setup

1. Create a Discord application at discord.com/developers

2. Enable "Message Content Intent" in the Bot section

3. Set Interactions Endpoint URL to:

```

https://app.playtrenches.xyz/api/webhooks/discord

```

4. Copy credentials to environment variables:

```bash

DISCORD_BOT_TOKEN="your-bot-token"

DISCORD_PUBLIC_KEY="your-public-key"

DISCORD_APPLICATION_ID="your-app-id"

```

---

Telegram Webhooks

Endpoint

POST /api/webhooks/telegram

Receives updates from the Telegram bot.

Events Received

EventDescription
messageText message received
callback_queryInline button clicked
chat_memberMember status changed
edited_messageMessage edited

Payload Example

{
  "update_id": 123456789,
  "message": {
    "message_id": 1,
    "from": {
      "id": 123456789,
      "is_bot": false,
      "first_name": "John",
      "username": "sprayer",
      "language_code": "en"
    },
    "chat": {
      "id": 123456789,
      "first_name": "John",
      "username": "sprayer",
      "type": "private"
    },
    "date": 1708102200,
    "text": "/balance"
  }
}

Secret Token Verification

Telegram supports secret tokens in webhook URLs:

https://app.playtrenches.xyz/api/webhooks/telegram?secret=YOUR_SECRET_TOKEN
// Verify secret token
export async function POST(request: Request) {
  const url = new URL(request.url);
  const secret = url.searchParams.get('secret');
  
  if (secret !== process.env.TELEGRAM_WEBHOOK_SECRET) {
    return new Response('Unauthorized', { status: 401 });
  }

  const update = await request.json();

  // Handle message
  if (update.message) {
    await handleMessage(update.message);
  }

  return new Response('OK', { status: 200 });
}

async function handleMessage(message: any) {
  const { text, chat, from } = message;

  switch (text) {
    case '/start':
      await sendMessage(chat.id, 'Welcome to Spray and Play Bot! 🎮');
      break;
    
    case '/balance':
      const balance = await getUserBalance(from.id);
      await sendMessage(chat.id, `Your balance: $${balance}`);
      break;
    
    default:
      await sendMessage(chat.id, 'Unknown command. Try /help');
  }
}

async function sendMessage(chatId: number, text: string) {
  await fetch(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      chat_id: chatId,
      text,
      parse_mode: 'HTML'
    })
  });
}

Telegram Bot Setup

1. Create a bot via @BotFather

2. Get your bot token

3. Set webhook:

```bash

curl -X POST "https://api.telegram.org/bot/setWebhook" \

-H "Content-Type: application/json" \

-d '{

"url": "https://app.playtrenches.xyz/api/webhooks/telegram",

"secret_token": "YOUR_SECRET_TOKEN"

}'

```

4. Configure environment:

```bash

TELEGRAM_BOT_TOKEN="your-bot-token"

TELEGRAM_WEBHOOK_SECRET="your-secret-token"

```

---

Custom Webhooks

Creating Custom Webhook Endpoints

You can create custom webhook endpoints for third-party integrations:

// /api/webhooks/custom/route.ts
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  // Verify custom auth
  const apiKey = request.headers.get('x-api-key');
  if (!verifyApiKey(apiKey)) {
    return NextResponse.json(
      { error: 'Invalid API key' },
      { status: 401 }
    );
  }

  const payload = await request.json();

  // Process webhook event
  await processWebhookEvent(payload);

  return NextResponse.json({ received: true });
}

async function processWebhookEvent(payload: any) {
  // Handle the event
  switch (payload.event) {
    case 'payment.received':
      await handlePaymentReceived(payload);
      break;
    case 'user.registered':
      await handleUserRegistered(payload);
      break;
    default:
      console.warn('Unknown event:', payload.event);
  }
}

Webhook Event Schema

interface WebhookEvent {
  id: string;
  event: string;
  timestamp: string;
  data: Record<string, unknown>;
}

interface PaymentReceivedEvent extends WebhookEvent {
  event: 'payment.received';
  data: {
    userId: string;
    amount: number;
    currency: string;
    txHash: string;
    status: 'confirmed' | 'pending';
  };
}

---

Testing Webhooks

Local Development with ngrok

Use ngrok to expose your local server:

# Start ngrok
ngrok http 3000

# Update webhook URL with ngrok URL
# Discord: Update in Developer Portal → Interactions URL
# Telegram: POST to setWebhook with ngrok URL

Testing Tools

PlatformToolURL
DiscordDeveloper Portaldiscord.com/developers
TelegramBot APIhttps://api.telegram.org/bot<TOKEN>/getUpdates
GeneralWebhook.sitewebhook.site

Discord Test Command

# Test Discord interaction
curl -X POST https://app.playtrenches.xyz/api/webhooks/discord \
  -H "Content-Type: application/json" \
  -d '{
    "type": 1,
    "id": "test_id",
    "token": "test_token"
  }'

Telegram Test Command

# Test Telegram webhook
curl -X POST "https://app.playtrenches.xyz/api/webhooks/telegram?secret=YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "update_id": 123,
    "message": {
      "message_id": 1,
      "from": {"id": 123, "username": "test"},
      "chat": {"id": 123, "type": "private"},
      "date": 1708102200,
      "text": "/start"
    }
  }'

---

Real-Time User Updates (SSE)

For real-time updates to clients, we use Server-Sent Events (SSE), not webhooks:

GET /api/sse?userId=USER_ID

See API Endpoints for details.

// Client-side SSE connection
const eventSource = new EventSource('/api/sse?userId=123');

eventSource.addEventListener('PAYOUT_COMPLETED', (event) => {
  const data = JSON.parse(event.data);
  console.log('Payout received:', data);
});

---

Configuration

Required Environment Variables

# Discord
DISCORD_BOT_TOKEN="your-bot-token"
DISCORD_PUBLIC_KEY="your-public-key"
DISCORD_APPLICATION_ID="your-app-id"

# Telegram
TELEGRAM_BOT_TOKEN="your-bot-token"
TELEGRAM_WEBHOOK_SECRET="optional-secret"

# Custom Webhooks (if using)
WEBHOOK_SECRET="your-webhook-secret"

Webhook Endpoint Configuration

PlatformEndpointMethodSecurity
Discord/api/webhooks/discordPOSTEd25519 signature
Telegram/api/webhooks/telegramPOSTSecret token
Custom/api/webhooks/customPOSTAPI key

---

Error Handling

Discord

ResponseMeaningAction
200SuccessEvent processed
401Invalid signatureCheck public key
500Server errorDiscord will retry

Discord automatically retries failed webhooks with exponential backoff.

Telegram

ResponseMeaningAction
200 OKSuccessUpdate processed
401Invalid secretCheck secret token
Any otherErrorTelegram retries for 24h

Retry Logic

// Implement idempotency for webhook handlers
const processedEvents = new Set<string>();

export async function POST(request: Request) {
  const event = await request.json();
  
  // Check for duplicate
  if (processedEvents.has(event.id)) {
    return new Response('Already processed', { status: 200 });
  }

  try {
    await processEvent(event);
    processedEvents.add(event.id);
    return new Response('OK', { status: 200 });
  } catch (error) {
    console.error('Webhook processing failed:', error);
    return new Response('Processing failed', { status: 500 });
  }
}

---

Security Best Practices

> ### 🔐 Security Checklist

>

> - ✅ Always verify webhook signatures

> - ✅ Use HTTPS in production

> - ✅ Store secrets in environment variables

> - ✅ Implement idempotency (prevent duplicate processing)

> - ✅ Return 200 quickly, process async

> - ✅ Log all webhook events for debugging

> - ✅ Rate limit webhook endpoints

>

> ### ⚠️ Common Pitfalls

>

> - ❌ Don't trust webhook payloads without verification

> - ❌ Don't block the response while processing

> - ❌ Don't expose sensitive data in error messages

> - ❌ Don't forget to handle retries

---

Complete Implementation Example

// /app/api/webhooks/discord/route.ts
import { NextResponse } from 'next/server';
import crypto from 'crypto';

const PUBLIC_KEY = process.env.DISCORD_PUBLIC_KEY!;

function verifySignature(
  payload: string,
  signature: string,
  timestamp: string
): boolean {
  const message = timestamp + payload;
  const sig = Buffer.from(signature, 'hex');
  const key = Buffer.from(PUBLIC_KEY, 'hex');
  
  try {
    return crypto.verify('ed25519', Buffer.from(message), key, sig);
  } catch {
    return false;
  }
}

export async function POST(request: Request) {
  const signature = request.headers.get('x-signature-ed25519');
  const timestamp = request.headers.get('x-signature-timestamp');
  const body = await request.text();

  // Security: Verify signature
  if (!signature || !timestamp || 
      !verifySignature(body, signature, timestamp)) {
    return NextResponse.json(
      { error: 'Invalid signature' },
      { status: 401 }
    );
  }

  const interaction = JSON.parse(body);

  // Handle ping
  if (interaction.type === 1) {
    return NextResponse.json({ type: 1 });
  }

  // Process command asynchronously
  // Return immediate acknowledgment
  processCommand(interaction).catch(console.error);

  return NextResponse.json({
    type: 5, // Deferred response
    data: { flags: 64 } // Ephemeral
  });
}

async function processCommand(interaction: any) {
  const { name, options } = interaction.data;
  
  // Update with actual response
  await fetch(
    `https://discord.com/api/v10/webhooks/${interaction.application_id}/${interaction.token}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        content: `Command "${name}" processed!`
      })
    }
  );
}

---

Related Documentation