Skip to main content
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.
Webhooks are an Enterprise feature. Upgrade your plan to access webhooks.

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

EventDescription
ticket.createdA new ticket was created
ticket.claimedStaff member claimed a ticket
ticket.unclaimedTicket was unclaimed
ticket.closedTicket was closed
ticket.reopenedClosed ticket was reopened
ticket.deletedTicket was deleted
ticket.renamedTicket subject was changed

Message Events

EventDescription
message.createdNew message in a ticket
message.editedMessage was edited
message.deletedMessage was deleted

Priority & SLA Events

EventDescription
priority.changedTicket priority was updated
sla.breachSLA time limit was exceeded
sla.warningSLA is close to breaching
sentiment.negativeNegative sentiment detected in ticket

Approval Events

EventDescription
approval.requestedApproval was requested
approval.approvedApproval was granted
approval.deniedApproval was denied

Ticket Notes

EventDescription
ticket.note.addedInternal note was added
ticket.note.removedInternal note was removed

Access Control Events

EventDescription
ticket.user.addedUser was added to ticket
ticket.user.removedUser was removed from ticket
ticket.role.addedRole was added to ticket
ticket.role.removedRole was removed from ticket

Payload Structure

All webhooks are delivered as POST requests with JSON payloads.

Base Structure

Every webhook follows this structure:
{
  "event": "ticket.created",
  "timestamp": 1702483200,
  "data": { ... },
  "attemptNumber": 1
}
FieldTypeDescription
eventstringEvent type (e.g., ticket.created)
timestampnumberUnix timestamp when event occurred
dataobjectEvent-specific payload (see examples below)
attemptNumbernumberDelivery attempt (1 = first try, 2+ = retries)

Payload Examples

{
  "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
}
{
  "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
}
{
  "event": "ticket.claimed",
  "timestamp": 1702484000,
  "data": {
    "ticketId": "507f1f77bcf86cd799439011",
    "ticketNumber": 42,
    "channelId": "111222333444555666",
    "creatorId": "123456789012345678",
    "claimerId": "999888777666555444",
    "claimerName": "Support Agent"
  },
  "attemptNumber": 1
}
{
  "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
}
{
  "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
}

Headers

Every webhook request includes these headers:
HeaderDescription
Content-TypeAlways application/json
User-AgentTicketCord-Webhooks/1.0
X-TicketCord-EventThe event type (e.g., ticket.created)
X-TicketCord-TimestampUnix timestamp when request was created
X-TicketCord-SignatureHMAC 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

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

Basic Receiver

A minimal webhook receiver that accepts events and responds quickly:
server.ts
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);
Best Practice: Respond with 200 immediately, then process events asynchronously (via queue, background job, etc.) to avoid timeouts.

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)
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.

Complete Examples with Signature Verification

server.ts
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}`);
});

Security Best Practices

Always verify signatures in production to prevent unauthorized requests.
  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:
{
  "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

Need Help?

Get instant support via private DM