> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ticketcord.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Integrate TicketCord with external services via webhooks

Webhooks allow you to receive real-time notifications when events occur in your ticket system. Use them to integrate with services like Zapier, Make.com, Slack, Jira, or your own custom applications.

<Info>
  Webhooks are an **Enterprise** feature. [Upgrade your plan](https://ticketcord.net/pricing) to access webhooks.
</Info>

## Quick Start

For most integrations (Zapier, Make.com, n8n, etc.), simply:

1. Go to **Webhooks** in your dashboard
2. Click **Create Webhook**
3. Enter the URL from your integration service
4. Select the events you want to receive
5. Save - you're done!

No additional configuration needed for standard integrations.

## Event Types

TicketCord supports 23 webhook events:

### Ticket Events

| Event              | Description                   |
| ------------------ | ----------------------------- |
| `ticket.created`   | A new ticket was created      |
| `ticket.claimed`   | Staff member claimed a ticket |
| `ticket.unclaimed` | Ticket was unclaimed          |
| `ticket.closed`    | Ticket was closed             |
| `ticket.reopened`  | Closed ticket was reopened    |
| `ticket.deleted`   | Ticket was deleted            |
| `ticket.renamed`   | Ticket subject was changed    |

### Message Events

| Event             | Description             |
| ----------------- | ----------------------- |
| `message.created` | New message in a ticket |
| `message.edited`  | Message was edited      |
| `message.deleted` | Message was deleted     |

### Priority & SLA Events

| Event                | Description                           |
| -------------------- | ------------------------------------- |
| `priority.changed`   | Ticket priority was updated           |
| `sla.breach`         | SLA time limit was exceeded           |
| `sla.warning`        | SLA is close to breaching             |
| `sentiment.negative` | Negative sentiment detected in ticket |

### Approval Events

| Event                | Description            |
| -------------------- | ---------------------- |
| `approval.requested` | Approval was requested |
| `approval.approved`  | Approval was granted   |
| `approval.denied`    | Approval was denied    |

### Ticket Notes

| Event                 | Description               |
| --------------------- | ------------------------- |
| `ticket.note.added`   | Internal note was added   |
| `ticket.note.removed` | Internal note was removed |

### Access Control Events

| Event                 | Description                  |
| --------------------- | ---------------------------- |
| `ticket.user.added`   | User was added to ticket     |
| `ticket.user.removed` | User was removed from ticket |
| `ticket.role.added`   | Role was added to ticket     |
| `ticket.role.removed` | Role was removed from ticket |

## Payload Structure

All webhooks are delivered as `POST` requests with JSON payloads.

### Base Structure

Every webhook follows this structure:

```json theme={null}
{
  "event": "ticket.created",
  "timestamp": 1702483200,
  "data": { ... },
  "attemptNumber": 1
}
```

| Field           | Type   | Description                                    |
| --------------- | ------ | ---------------------------------------------- |
| `event`         | string | Event type (e.g., `ticket.created`)            |
| `timestamp`     | number | Unix timestamp when event occurred             |
| `data`          | object | Event-specific payload (see examples below)    |
| `attemptNumber` | number | Delivery attempt (1 = first try, 2+ = retries) |

### Payload Examples

<AccordionGroup>
  <Accordion title="ticket.created">
    ```json theme={null}
    {
      "event": "ticket.created",
      "timestamp": 1702483200,
      "data": {
        "ticketId": "507f1f77bcf86cd799439011",
        "ticketNumber": 42,
        "channelId": "111222333444555666",
        "creatorId": "123456789012345678",
        "creatorName": "John Doe",
        "category": "Billing",
        "priority": "medium",
        "subject": "Need help with billing",
        "createdAt": "2024-12-13T10:00:00.000Z"
      },
      "attemptNumber": 1
    }
    ```
  </Accordion>

  <Accordion title="ticket.closed">
    ```json theme={null}
    {
      "event": "ticket.closed",
      "timestamp": 1702486800,
      "data": {
        "ticketId": "507f1f77bcf86cd799439011",
        "ticketNumber": 42,
        "channelId": "111222333444555666",
        "creatorId": "123456789012345678",
        "creatorName": "John Doe",
        "closedAt": "2024-12-13T11:00:00.000Z",
        "closeReason": "Issue resolved",
        "staffMemberId": "999888777666555444",
        "staffName": "Support Agent"
      },
      "attemptNumber": 1
    }
    ```
  </Accordion>

  <Accordion title="ticket.claimed">
    ```json theme={null}
    {
      "event": "ticket.claimed",
      "timestamp": 1702484000,
      "data": {
        "ticketId": "507f1f77bcf86cd799439011",
        "ticketNumber": 42,
        "channelId": "111222333444555666",
        "creatorId": "123456789012345678",
        "claimerId": "999888777666555444",
        "claimerName": "Support Agent"
      },
      "attemptNumber": 1
    }
    ```
  </Accordion>

  <Accordion title="message.created">
    ```json theme={null}
    {
      "event": "message.created",
      "timestamp": 1702484500,
      "data": {
        "messageId": "222333444555666777",
        "ticketId": "507f1f77bcf86cd799439011",
        "channelId": "111222333444555666",
        "authorId": "123456789012345678",
        "authorName": "John Doe",
        "content": "Hello, I need help with my subscription.",
        "embeds": 0,
        "attachments": 1,
        "isStaff": false,
        "isBot": false,
        "createdAt": "2024-12-13T10:15:00.000Z"
      },
      "attemptNumber": 1
    }
    ```
  </Accordion>

  <Accordion title="priority.changed">
    ```json theme={null}
    {
      "event": "priority.changed",
      "timestamp": 1702485000,
      "data": {
        "ticketId": "507f1f77bcf86cd799439011",
        "channelId": "111222333444555666",
        "oldPriority": "medium",
        "newPriority": "high",
        "changedBy": "999888777666555444",
        "changedByName": "Support Agent",
        "reason": "Urgent billing issue",
        "timestamp": "2024-12-13T10:30:00.000Z"
      },
      "attemptNumber": 1
    }
    ```
  </Accordion>
</AccordionGroup>

### Headers

Every webhook request includes these headers:

| Header                   | Description                             |
| ------------------------ | --------------------------------------- |
| `Content-Type`           | Always `application/json`               |
| `User-Agent`             | `TicketCord-Webhooks/1.0`               |
| `X-TicketCord-Event`     | The event type (e.g., `ticket.created`) |
| `X-TicketCord-Timestamp` | Unix timestamp when request was created |
| `X-TicketCord-Signature` | HMAC signature for verification         |

## Retry Policy

Failed webhook deliveries are automatically retried:

* **Maximum retries**: 3 attempts
* **Backoff**: Exponential (1s, 2s, 4s)
* **Success codes**: Any 2xx response is considered successful
* **Timeout**: 30 seconds per request

***

## Building a Custom Webhook Receiver

<Info>
  This section is for developers building custom integrations. Most users can skip this and use services like Zapier or Make.com.
</Info>

### Basic Receiver

A minimal webhook receiver that accepts events and responds quickly:

<CodeGroup dropdown>
  ```typescript server.ts theme={null}
  import express, { Request, Response } from 'express';

  const app = express();
  app.use(express.json());

  interface WebhookPayload {
    event: string;
    timestamp: number;
    data: Record<string, unknown>;
    attemptNumber: number;
  }

  app.post('/webhooks/ticketcord', (req: Request, res: Response) => {
    const { event, data, attemptNumber } = req.body as WebhookPayload;

    console.log(`Received: ${event} (attempt ${attemptNumber})`);

    // TODO: Process event (send to queue, database, etc.)

    // Always respond quickly with 200 to acknowledge receipt
    res.status(200).json({ received: true });
  });

  app.listen(3000);
  ```

  ```javascript server.js theme={null}
  const express = require('express');
  const app = express();

  app.use(express.json());

  app.post('/webhooks/ticketcord', (req, res) => {
    const { event, data, attemptNumber } = req.body;

    console.log(`Received: ${event} (attempt ${attemptNumber})`);

    // TODO: Process event (send to queue, database, etc.)

    // Always respond quickly with 200 to acknowledge receipt
    res.status(200).json({ received: true });
  });

  app.listen(3000);
  ```

  ```python server.py theme={null}
  from flask import Flask, request, jsonify

  app = Flask(__name__)

  @app.route('/webhooks/ticketcord', methods=['POST'])
  def handle_webhook():
      payload = request.json
      event = payload['event']
      data = payload['data']

      print(f"Received: {event}")

      # TODO: Process event (send to queue, database, etc.)

      # Always respond quickly with 200 to acknowledge receipt
      return jsonify({'received': True}), 200

  if __name__ == '__main__':
      app.run(port=3000)
  ```

  ```go server.go theme={null}
  package main

  import (
  	"encoding/json"
  	"io"
  	"log"
  	"net/http"
  )

  type WebhookPayload struct {
  	Event         string                 `json:"event"`
  	Timestamp     int64                  `json:"timestamp"`
  	Data          map[string]interface{} `json:"data"`
  	AttemptNumber int                    `json:"attemptNumber"`
  }

  func main() {
  	http.HandleFunc("/webhooks/ticketcord", func(w http.ResponseWriter, r *http.Request) {
  		body, _ := io.ReadAll(r.Body)
  		defer r.Body.Close()

  		var payload WebhookPayload
  		json.Unmarshal(body, &payload)

  		log.Printf("Received: %s (attempt %d)", payload.Event, payload.AttemptNumber)

  		// TODO: Process event (send to queue, database, etc.)

  		// Always respond quickly with 200 to acknowledge receipt
  		w.Header().Set("Content-Type", "application/json")
  		json.NewEncoder(w).Encode(map[string]bool{"received": true})
  	})

  	log.Fatal(http.ListenAndServe(":3000", nil))
  }
  ```

  ```php server.php theme={null}
  <?php
  header('Content-Type: application/json');

  $payload = json_decode(file_get_contents('php://input'), true);
  $event = $payload['event'];
  $data = $payload['data'];

  error_log("Received: $event");

  // TODO: Process event (send to queue, database, etc.)

  // Always respond quickly with 200 to acknowledge receipt
  http_response_code(200);
  echo json_encode(['received' => true]);
  ```
</CodeGroup>

<Tip>
  **Best Practice**: Respond with `200` immediately, then process events asynchronously (via queue, background job, etc.) to avoid timeouts.
</Tip>

### Signature Verification

For production systems, **always verify** that webhook requests actually come from TicketCord. This prevents attackers from sending fake events to your endpoint.

#### How Signatures Work

Each webhook request is signed using your webhook's secret key:

1. TicketCord creates a signature string: `v0:{timestamp}:{payload}`
2. Generates HMAC-SHA256 hash using your secret
3. Sends the hash in the `X-TicketCord-Signature` header

#### Verification Steps

1. Extract the timestamp from `X-TicketCord-Timestamp` header
2. Get the raw JSON payload body (before parsing)
3. Compute: `HMAC-SHA256("v0:{timestamp}:{payload}", your_secret)`
4. Compare your computed signature with `X-TicketCord-Signature`
5. Verify timestamp is within 5 minutes (replay attack protection)

<Warning>
  **Why verify timestamps?** Without timestamp validation, an attacker could intercept a valid webhook and replay it later. By checking the timestamp is recent, you ensure the request is fresh.
</Warning>

#### Complete Examples with Signature Verification

<CodeGroup dropdown>
  ```typescript server.ts theme={null}
  import crypto from 'crypto';
  import express, { Request, Response } from 'express';

  // Extend Express Request to include rawBody
  declare global {
    namespace Express {
      interface Request {
        rawBody?: string;
      }
    }
  }

  const app = express();

  // ============================================================
  // IMPORTANT: Capture raw body BEFORE JSON parsing
  // The signature is computed on the raw string, not parsed JSON
  // ============================================================
  app.use(express.json({
    verify: (req: Request, _res, buf) => {
      req.rawBody = buf.toString();
    }
  }));

  // Load your webhook secret from environment variables
  const WEBHOOK_SECRET = process.env.TICKETCORD_WEBHOOK_SECRET;

  if (!WEBHOOK_SECRET) {
    console.error('ERROR: TICKETCORD_WEBHOOK_SECRET environment variable not set');
    process.exit(1);
  }

  /**
   * Verify that a webhook request is authentic
   */
  function verifySignature(payload: string, timestamp: number, signature: string): boolean {
    // STEP 1: Check timestamp freshness (prevent replay attacks)
    const currentTime = Math.floor(Date.now() / 1000);
    const age = Math.abs(currentTime - timestamp);

    if (age > 300) {
      console.warn(`[Security] Request too old: ${age} seconds`);
      return false;
    }

    // STEP 2: Compute expected signature
    const signatureBase = `v0:${timestamp}:${payload}`;
    const expectedSignature = crypto
      .createHmac('sha256', WEBHOOK_SECRET)
      .update(signatureBase)
      .digest('hex');

    // STEP 3: Compare signatures using constant-time comparison
    try {
      return crypto.timingSafeEqual(
        Buffer.from(signature, 'hex'),
        Buffer.from(expectedSignature, 'hex')
      );
    } catch {
      return false;
    }
  }

  app.post('/webhooks/ticketcord', (req: Request, res: Response) => {
    const signature = req.headers['x-ticketcord-signature'] as string;
    const timestampHeader = req.headers['x-ticketcord-timestamp'] as string;
    const eventType = req.headers['x-ticketcord-event'] as string;

    if (!signature || !timestampHeader) {
      return res.status(401).json({ error: 'Missing signature headers' });
    }

    const timestamp = parseInt(timestampHeader, 10);
    if (isNaN(timestamp)) {
      return res.status(401).json({ error: 'Invalid timestamp' });
    }

    if (!verifySignature(req.rawBody!, timestamp, signature)) {
      console.warn(`[Security] Invalid signature for ${eventType} event`);
      return res.status(401).json({ error: 'Invalid signature' });
    }

    console.log(`[Webhook] Verified ${eventType} event`);

    // Process the event...

    res.status(200).json({ received: true });
  });

  const PORT = process.env.PORT || 3000;
  app.listen(PORT, () => {
    console.log(`Secure webhook server running on port ${PORT}`);
  });
  ```

  ```javascript server.js theme={null}
  const crypto = require('crypto');
  const express = require('express');
  const app = express();

  // ============================================================
  // IMPORTANT: Capture raw body BEFORE JSON parsing
  // The signature is computed on the raw string, not parsed JSON
  // ============================================================
  app.use(express.json({
    verify: (req, res, buf) => {
      req.rawBody = buf.toString();
    }
  }));

  // Load your webhook secret from environment variables
  const WEBHOOK_SECRET = process.env.TICKETCORD_WEBHOOK_SECRET;

  if (!WEBHOOK_SECRET) {
    console.error('ERROR: TICKETCORD_WEBHOOK_SECRET environment variable not set');
    process.exit(1);
  }

  /**
   * Verify that a webhook request is authentic
   */
  function verifySignature(payload, timestamp, signature) {
    // STEP 1: Check timestamp freshness (prevent replay attacks)
    const currentTime = Math.floor(Date.now() / 1000);
    const age = Math.abs(currentTime - timestamp);

    if (age > 300) {
      console.warn(`[Security] Request too old: ${age} seconds`);
      return false;
    }

    // STEP 2: Compute expected signature
    const signatureBase = `v0:${timestamp}:${payload}`;
    const expectedSignature = crypto
      .createHmac('sha256', WEBHOOK_SECRET)
      .update(signatureBase)
      .digest('hex');

    // STEP 3: Compare signatures using constant-time comparison
    try {
      return crypto.timingSafeEqual(
        Buffer.from(signature, 'hex'),
        Buffer.from(expectedSignature, 'hex')
      );
    } catch (error) {
      return false;
    }
  }

  app.post('/webhooks/ticketcord', (req, res) => {
    const signature = req.headers['x-ticketcord-signature'];
    const timestampHeader = req.headers['x-ticketcord-timestamp'];
    const eventType = req.headers['x-ticketcord-event'];

    if (!signature || !timestampHeader) {
      return res.status(401).json({ error: 'Missing signature headers' });
    }

    const timestamp = parseInt(timestampHeader, 10);
    if (isNaN(timestamp)) {
      return res.status(401).json({ error: 'Invalid timestamp' });
    }

    if (!verifySignature(req.rawBody, timestamp, signature)) {
      console.warn(`[Security] Invalid signature for ${eventType} event`);
      return res.status(401).json({ error: 'Invalid signature' });
    }

    console.log(`[Webhook] Verified ${eventType} event`);

    // Process the event...

    res.status(200).json({ received: true });
  });

  const PORT = process.env.PORT || 3000;
  app.listen(PORT, () => {
    console.log(`Secure webhook server running on port ${PORT}`);
  });
  ```

  ```python server.py theme={null}
  import os
  import hmac
  import hashlib
  import time
  from flask import Flask, request, jsonify, abort

  app = Flask(__name__)

  # Load your webhook secret from environment variables
  WEBHOOK_SECRET = os.environ.get('TICKETCORD_WEBHOOK_SECRET')

  if not WEBHOOK_SECRET:
      raise RuntimeError('TICKETCORD_WEBHOOK_SECRET environment variable not set')


  def verify_signature(payload: bytes, timestamp: int, signature: str) -> bool:
      """Verify that a webhook request is authentic."""
      # STEP 1: Check timestamp freshness (prevent replay attacks)
      current_time = int(time.time())
      age = abs(current_time - timestamp)

      if age > 300:
          print(f"[Security] Request too old: {age} seconds")
          return False

      # STEP 2: Compute expected signature
      signature_base = f"v0:{timestamp}:{payload.decode('utf-8')}"
      expected_signature = hmac.new(
          WEBHOOK_SECRET.encode('utf-8'),
          signature_base.encode('utf-8'),
          hashlib.sha256
      ).hexdigest()

      # STEP 3: Compare signatures using constant-time comparison
      return hmac.compare_digest(signature, expected_signature)


  @app.route('/webhooks/ticketcord', methods=['POST'])
  def handle_webhook():
      signature = request.headers.get('X-TicketCord-Signature', '')
      timestamp_header = request.headers.get('X-TicketCord-Timestamp', '')
      event_type = request.headers.get('X-TicketCord-Event', '')

      if not signature or not timestamp_header:
          abort(401, description='Missing signature headers')

      try:
          timestamp = int(timestamp_header)
      except ValueError:
          abort(401, description='Invalid timestamp')

      # request.data gives us the raw bytes before JSON parsing
      if not verify_signature(request.data, timestamp, signature):
          print(f'[Security] Invalid signature for {event_type} event')
          abort(401, description='Invalid signature')

      print(f'[Webhook] Verified {event_type} event')

      # Process the event...

      return jsonify({'received': True}), 200


  if __name__ == '__main__':
      port = int(os.environ.get('PORT', 3000))
      app.run(host='0.0.0.0', port=port)
  ```

  ```go server.go theme={null}
  package main

  import (
  	"crypto/hmac"
  	"crypto/sha256"
  	"encoding/hex"
  	"encoding/json"
  	"fmt"
  	"io"
  	"log"
  	"math"
  	"net/http"
  	"os"
  	"strconv"
  	"time"
  )

  var webhookSecret = os.Getenv("TICKETCORD_WEBHOOK_SECRET")

  func init() {
  	if webhookSecret == "" {
  		log.Fatal("TICKETCORD_WEBHOOK_SECRET environment variable not set")
  	}
  }

  // verifySignature checks that a webhook request is authentic
  func verifySignature(payload []byte, timestamp int64, signature string) bool {
  	// STEP 1: Check timestamp freshness (prevent replay attacks)
  	currentTime := time.Now().Unix()
  	age := math.Abs(float64(currentTime - timestamp))

  	if age > 300 {
  		log.Printf("[Security] Request too old: %.0f seconds", age)
  		return false
  	}

  	// STEP 2: Compute expected signature
  	signatureBase := fmt.Sprintf("v0:%d:%s", timestamp, string(payload))
  	mac := hmac.New(sha256.New, []byte(webhookSecret))
  	mac.Write([]byte(signatureBase))
  	expectedSignature := hex.EncodeToString(mac.Sum(nil))

  	// STEP 3: Compare signatures using constant-time comparison
  	return hmac.Equal([]byte(signature), []byte(expectedSignature))
  }

  func handleWebhook(w http.ResponseWriter, r *http.Request) {
  	if r.Method != http.MethodPost {
  		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
  		return
  	}

  	signature := r.Header.Get("X-TicketCord-Signature")
  	timestampHeader := r.Header.Get("X-TicketCord-Timestamp")
  	eventType := r.Header.Get("X-TicketCord-Event")

  	if signature == "" || timestampHeader == "" {
  		http.Error(w, "Missing signature headers", http.StatusUnauthorized)
  		return
  	}

  	timestamp, err := strconv.ParseInt(timestampHeader, 10, 64)
  	if err != nil {
  		http.Error(w, "Invalid timestamp", http.StatusUnauthorized)
  		return
  	}

  	body, err := io.ReadAll(r.Body)
  	if err != nil {
  		http.Error(w, "Failed to read body", http.StatusBadRequest)
  		return
  	}
  	defer r.Body.Close()

  	if !verifySignature(body, timestamp, signature) {
  		log.Printf("[Security] Invalid signature for %s event", eventType)
  		http.Error(w, "Invalid signature", http.StatusUnauthorized)
  		return
  	}

  	log.Printf("[Webhook] Verified %s event", eventType)

  	// Process the event...

  	w.Header().Set("Content-Type", "application/json")
  	json.NewEncoder(w).Encode(map[string]bool{"received": true})
  }

  func main() {
  	http.HandleFunc("/webhooks/ticketcord", handleWebhook)

  	port := os.Getenv("PORT")
  	if port == "" {
  		port = "3000"
  	}

  	log.Printf("Secure webhook server running on port %s", port)
  	log.Fatal(http.ListenAndServe(":"+port, nil))
  }
  ```

  ```php server.php theme={null}
  <?php
  /**
   * TicketCord Secure Webhook Receiver
   */

  $webhookSecret = getenv('TICKETCORD_WEBHOOK_SECRET');

  if (!$webhookSecret) {
      http_response_code(500);
      exit('TICKETCORD_WEBHOOK_SECRET not set');
  }

  /**
   * Verify that a webhook request is authentic
   */
  function verifySignature(string $payload, int $timestamp, string $signature): bool {
      global $webhookSecret;

      // STEP 1: Check timestamp freshness (prevent replay attacks)
      $age = abs(time() - $timestamp);
      if ($age > 300) {
          error_log("[Security] Request too old: $age seconds");
          return false;
      }

      // STEP 2: Compute expected signature
      $signatureBase = "v0:{$timestamp}:{$payload}";
      $expectedSignature = hash_hmac('sha256', $signatureBase, $webhookSecret);

      // STEP 3: Compare signatures using constant-time comparison
      return hash_equals($expectedSignature, $signature);
  }

  header('Content-Type: application/json');

  $signature = $_SERVER['HTTP_X_TICKETCORD_SIGNATURE'] ?? '';
  $timestampHeader = $_SERVER['HTTP_X_TICKETCORD_TIMESTAMP'] ?? '';
  $eventType = $_SERVER['HTTP_X_TICKETCORD_EVENT'] ?? '';

  if (empty($signature) || empty($timestampHeader)) {
      http_response_code(401);
      echo json_encode(['error' => 'Missing signature headers']);
      exit;
  }

  $timestamp = (int)$timestampHeader;
  if ($timestamp === 0) {
      http_response_code(401);
      echo json_encode(['error' => 'Invalid timestamp']);
      exit;
  }

  $rawPayload = file_get_contents('php://input');

  if (!verifySignature($rawPayload, $timestamp, $signature)) {
      error_log("[Security] Invalid signature for $eventType event");
      http_response_code(401);
      echo json_encode(['error' => 'Invalid signature']);
      exit;
  }

  error_log("[Webhook] Verified $eventType event");

  // Process the event...

  http_response_code(200);
  echo json_encode(['received' => true]);
  ```
</CodeGroup>

### Security Best Practices

<Warning>
  Always verify signatures in production to prevent unauthorized requests.
</Warning>

1. **Always verify signatures** - Don't trust requests without valid signatures
2. **Check timestamps** - Reject requests older than 5 minutes to prevent replay attacks
3. **Use HTTPS** - Your webhook endpoint should only accept HTTPS connections
4. **Use constant-time comparison** - Prevents timing attacks when comparing signatures
5. **Keep secrets secure** - Store your webhook secret in environment variables, never in code
6. **Handle retries gracefully** - Your endpoint may receive the same event multiple times (use idempotency keys)
7. **Respond quickly** - Return a 2xx response within 30 seconds to avoid timeouts

### Testing Your Receiver

Use the **Test** button in the TicketCord dashboard to send a test webhook to your endpoint. This sends a `test.ping` event:

```json theme={null}
{
  "event": "test.ping",
  "timestamp": 1702483200,
  "data": {
    "message": "This is a test webhook from TicketCord",
    "testMode": true
  },
  "attemptNumber": 1
}
```

### Debugging

Check the **View Logs** option in the dashboard to see:

* Request/response details for each delivery
* HTTP status codes returned by your endpoint
* Error messages for failed deliveries
* Retry attempts and timing

<Card title="Need Help?" icon="headset" href="https://ticketcord.net/support">
  Get instant support via private DM
</Card>
