npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dispatchtickets/sdk

v0.8.3

Published

Official TypeScript SDK for Dispatch Tickets API

Readme

@dispatchtickets/sdk

npm version License: MIT

Official TypeScript SDK for the Dispatch Tickets API.

Installation

npm install @dispatchtickets/sdk

Requirements

  • Node.js 18 or later

Quick Start

import { DispatchTickets } from '@dispatchtickets/sdk';

const client = new DispatchTickets({
  apiKey: 'sk_live_...',
});

// List brands
const brands = await client.brands.list();

// Create a ticket
const ticket = await client.tickets.create('ws_abc123', {
  title: 'Help with login',
  body: 'I cannot log in to my account',
});

// Add a comment
await client.comments.create('ws_abc123', ticket.id, {
  body: 'Thanks for reaching out! Let me help you with that.',
  authorType: 'AGENT',
});

Portal API (End-User Access)

For customer-facing integrations, use DispatchPortal to let end-users view and manage their own tickets:

import { DispatchTickets, DispatchPortal } from '@dispatchtickets/sdk';

// 1. Backend: Generate a portal token for your user
const admin = new DispatchTickets({ apiKey: 'sk_live_xxx' });
const { token } = await admin.brands.generatePortalToken('br_xxx', {
  email: user.email,
  name: user.name,
});

// 2. Frontend: Use the token to access tickets
const portal = new DispatchPortal({ token });

// List user's tickets
const { data: tickets } = await portal.tickets.list();

// Create a new ticket
const ticket = await portal.tickets.create({
  title: 'Help with my order',
  body: 'Order #12345 has not arrived...',
});

// Add a comment
await portal.tickets.addComment(ticket.id, 'Here is more information...');

Portal tokens are scoped to a single customer and expire after 1 hour. See the Integration Guide for complete examples.

Configuration

const client = new DispatchTickets({
  apiKey: 'sk_live_...',        // Required
  baseUrl: 'https://...',        // Optional, default: production API
  timeout: 30000,                // Optional, request timeout in ms
  debug: false,                  // Optional, enable debug logging
  fetch: customFetch,            // Optional, custom fetch for testing
  retry: {                       // Optional, fine-grained retry config
    maxRetries: 3,
    retryableStatuses: [429, 500, 502, 503, 504],
    initialDelayMs: 1000,
    maxDelayMs: 30000,
  },
  hooks: {                       // Optional, observability hooks
    onRequest: (ctx) => console.log(`${ctx.method} ${ctx.url}`),
    onResponse: (ctx) => console.log(`${ctx.status} in ${ctx.durationMs}ms`),
    onError: (error) => Sentry.captureException(error),
    onRetry: (ctx, error, delay) => console.log(`Retrying in ${delay}ms`),
  },
});

Request Cancellation

Cancel long-running requests with an AbortController:

const controller = new AbortController();

// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);

try {
  const page = await client.tickets.listPage('ws_abc', {}, {
    signal: controller.signal,
  });
} catch (error) {
  if (error.message.includes('aborted')) {
    console.log('Request was cancelled');
  }
}

Resources

Accounts

// Get current account
const account = await client.accounts.me();

// Get usage statistics
const usage = await client.accounts.getUsage();
console.log(`${usage.ticketsThisMonth}/${usage.plan?.ticketLimit} tickets used`);

// List API keys
const apiKeys = await client.accounts.listApiKeys();

// Create a new API key
const newKey = await client.accounts.createApiKey({
  name: 'Production',
  allBrands: true,  // or brandIds: ['br_123']
});
console.log('Save this key:', newKey.key); // Only shown once!

// Update API key scope
await client.accounts.updateApiKeyScope('key_abc', {
  allBrands: false,
  brandIds: ['br_123', 'br_456'],
});

// Revoke an API key
await client.accounts.revokeApiKey('key_abc');

Brands

// Get inbound email address for a brand
const inboundEmail = client.brands.getInboundEmail('br_abc123');
// Returns: [email protected]

// Create a brand
const brand = await client.brands.create({
  name: 'Acme Support',
  slug: 'acme',
});

// List all brands
const brands = await client.brands.list();

// Get a brand
const brand = await client.brands.get('ws_abc123');

// Update a brand
await client.brands.update('ws_abc123', { name: 'New Name' });

// Delete a brand
await client.brands.delete('ws_abc123');

Tickets

// Create a ticket
const ticket = await client.tickets.create('ws_abc123', {
  title: 'Issue with billing',
  body: 'I was charged twice...',
  priority: 'high',
});

// Create with idempotency key (prevents duplicates)
const ticket = await client.tickets.create(
  'ws_abc123',
  { title: 'Issue' },
  { idempotencyKey: 'unique-request-id' }
);

// List tickets (paginated)
for await (const ticket of client.tickets.list('ws_abc123', { status: 'open' })) {
  console.log(ticket.title);
}

// Get a single page
const page = await client.tickets.listPage('ws_abc123', { limit: 50 });

// Get a ticket
const ticket = await client.tickets.get('ws_abc123', 'tkt_xyz');

// Update a ticket
await client.tickets.update('ws_abc123', 'tkt_xyz', {
  status: 'resolved',
});

// Delete a ticket
await client.tickets.delete('ws_abc123', 'tkt_xyz');

// Mark as spam
await client.tickets.markAsSpam('ws_abc123', 'tkt_xyz', true);

// Merge tickets
await client.tickets.merge('ws_abc123', 'tkt_target', ['tkt_source1', 'tkt_source2']);

// Bulk actions
await client.tickets.bulk('ws_abc123', 'close', ['tkt_1', 'tkt_2']);
await client.tickets.bulk('ws_abc123', 'assign', ['tkt_1'], { assigneeId: 'user_123' });

Comments

// Add a comment
const comment = await client.comments.create('ws_abc123', 'tkt_xyz', {
  body: 'Thanks for your patience!',
  authorType: 'AGENT',
});

// List comments
const comments = await client.comments.list('ws_abc123', 'tkt_xyz');

// Update a comment
await client.comments.update('ws_abc123', 'tkt_xyz', 'cmt_abc', {
  body: 'Updated message',
});

// Delete a comment
await client.comments.delete('ws_abc123', 'tkt_xyz', 'cmt_abc');

Attachments

// Simple upload (handles the full flow)
const attachment = await client.attachments.upload(
  'ws_abc123',
  'tkt_xyz',
  fileBuffer,
  'document.pdf',
  'application/pdf'
);

// Manual upload flow
const { uploadUrl, attachmentId } = await client.attachments.initiateUpload(
  'ws_abc123',
  'tkt_xyz',
  { filename: 'doc.pdf', contentType: 'application/pdf', size: 1024 }
);
// Upload to presigned URL...
await client.attachments.confirmUpload('ws_abc123', 'tkt_xyz', attachmentId);

// List attachments
const attachments = await client.attachments.list('ws_abc123', 'tkt_xyz');

// Get download URL
const { downloadUrl } = await client.attachments.get('ws_abc123', 'tkt_xyz', 'att_abc');

Webhooks

// Create a webhook
const webhook = await client.webhooks.create('ws_abc123', {
  url: 'https://example.com/webhook',
  secret: 'your-secret',
  events: ['ticket.created', 'ticket.updated'],
});

// List webhooks
const webhooks = await client.webhooks.list('ws_abc123');

// Get delivery history
const deliveries = await client.webhooks.getDeliveries('ws_abc123', 'whk_abc');

// Verify webhook signature
const isValid = DispatchTickets.webhooks.verifySignature(
  rawBody,
  req.headers['x-dispatch-signature'],
  'your-secret'
);

Categories

// Create a category
await client.categories.create('ws_abc123', { name: 'Billing', color: '#ef4444' });

// List categories
const categories = await client.categories.list('ws_abc123');

// Get stats
const stats = await client.categories.getStats('ws_abc123');

// Reorder
await client.categories.reorder('ws_abc123', ['cat_1', 'cat_2', 'cat_3']);

Tags

// Create a tag
await client.tags.create('ws_abc123', { name: 'urgent', color: '#f59e0b' });

// List tags
const tags = await client.tags.list('ws_abc123');

// Merge tags
await client.tags.merge('ws_abc123', 'tag_target', ['tag_source1', 'tag_source2']);

Customers

// Create a customer
const customer = await client.customers.create('ws_abc123', {
  email: '[email protected]',
  name: 'Jane Doe',
});

// List customers (paginated)
for await (const customer of client.customers.list('ws_abc123')) {
  console.log(customer.email);
}

// Search customers
const results = await client.customers.search('ws_abc123', 'jane');

Custom Fields

// Get all field definitions
const fields = await client.fields.getAll('ws_abc123');

// Get ticket fields
const ticketFields = await client.fields.list('ws_abc123', 'ticket');

// Create a field
await client.fields.create('ws_abc123', 'ticket', {
  key: 'order_id',
  label: 'Order ID',
  type: 'text',
  required: true,
});

// Update a field
await client.fields.update('ws_abc123', 'ticket', 'order_id', {
  required: false,
});

// Delete a field
await client.fields.delete('ws_abc123', 'ticket', 'order_id');

Error Handling

Use type guards for clean error handling:

import {
  DispatchTickets,
  isNotFoundError,
  isAuthenticationError,
  isRateLimitError,
  isValidationError,
} from '@dispatchtickets/sdk';

try {
  await client.tickets.get('ws_abc123', 'tkt_invalid');
} catch (error) {
  if (isNotFoundError(error)) {
    console.log('Ticket not found');
    console.log('Request ID:', error.requestId); // For debugging with support
  } else if (isAuthenticationError(error)) {
    console.log('Invalid API key');
  } else if (isRateLimitError(error)) {
    console.log(`Rate limited. Retry after ${error.retryAfter} seconds`);
    console.log(`Limit: ${error.limit}, Remaining: ${error.remaining}`);
  } else if (isValidationError(error)) {
    console.log('Validation errors:', error.errors);
  }
}

All errors include a requestId for debugging with support.

Webhook Events

Handle webhook events with full type safety:

import {
  DispatchTickets,
  parseWebhookEvent,
  isTicketCreatedEvent,
  isTicketUpdatedEvent,
  isCommentCreatedEvent,
} from '@dispatchtickets/sdk';

// Parse and validate webhook payload
const event = parseWebhookEvent(req.body);

// Use type guards for type-safe event handling
if (isTicketCreatedEvent(event)) {
  // event.data is typed as TicketCreatedData
  console.log('New ticket:', event.data.title);
  console.log('Priority:', event.data.priority);
  console.log('Customer:', event.data.customerEmail);
}

if (isTicketUpdatedEvent(event)) {
  // event.data is typed as TicketUpdatedData
  console.log('Ticket updated:', event.data.id);
  console.log('Changed fields:', event.data.changes);
}

if (isCommentCreatedEvent(event)) {
  // event.data is typed as CommentCreatedData
  console.log('New comment on', event.data.ticketNumber);
  console.log('Author:', event.data.comment.authorType);
}

Webhook Verification

import express from 'express';
import { DispatchTickets } from '@dispatchtickets/sdk';

const app = express();

// Use raw body for signature verification
app.use('/webhooks', express.raw({ type: 'application/json' }));

app.post('/webhooks', (req, res) => {
  const signature = req.headers['x-dispatch-signature'] as string;
  const isValid = DispatchTickets.webhooks.verifySignature(
    req.body.toString(),
    signature,
    process.env.WEBHOOK_SECRET!
  );

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body.toString());
  console.log('Received event:', event.type);

  res.status(200).send('OK');
});

Testing

Use the custom fetch option to mock API responses in tests:

import { DispatchTickets } from '@dispatchtickets/sdk';
import { vi } from 'vitest';

const mockFetch = vi.fn().mockResolvedValue({
  ok: true,
  status: 200,
  headers: { get: () => 'application/json' },
  json: () => Promise.resolve([{ id: 'br_123', name: 'Test' }]),
});

const client = new DispatchTickets({
  apiKey: 'sk_test_123',
  fetch: mockFetch,
});

const brands = await client.brands.list();
expect(brands).toHaveLength(1);
expect(mockFetch).toHaveBeenCalled();

Examples

See the /examples directory for complete working examples:

API Documentation

Generate TypeDoc API documentation locally:

npm run docs

This creates a docs/ folder with HTML documentation for all exported types and methods.

Links

License

MIT