@dispatchtickets/sdk 0.3.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -2,6 +2,105 @@
2
2
  * Custom fetch function type
3
3
  */
4
4
  type FetchFunction = typeof fetch;
5
+ /**
6
+ * Request context passed to hooks
7
+ */
8
+ interface RequestContext {
9
+ /** HTTP method */
10
+ method: string;
11
+ /** Full URL being requested */
12
+ url: string;
13
+ /** Request headers */
14
+ headers: Record<string, string>;
15
+ /** Request body (if any) */
16
+ body?: unknown;
17
+ /** Retry attempt number (0 = first attempt) */
18
+ attempt: number;
19
+ }
20
+ /**
21
+ * Response context passed to hooks
22
+ */
23
+ interface ResponseContext {
24
+ /** The request that was made */
25
+ request: RequestContext;
26
+ /** HTTP status code */
27
+ status: number;
28
+ /** Response headers */
29
+ headers: Headers;
30
+ /** Request ID from server */
31
+ requestId?: string;
32
+ /** Rate limit info from server */
33
+ rateLimit?: RateLimitInfo;
34
+ /** Duration of the request in milliseconds */
35
+ durationMs: number;
36
+ }
37
+ /**
38
+ * Retry configuration options
39
+ */
40
+ interface RetryConfig {
41
+ /**
42
+ * Maximum number of retry attempts
43
+ * @default 3
44
+ */
45
+ maxRetries?: number;
46
+ /**
47
+ * HTTP status codes that should trigger a retry
48
+ * @default [429, 500, 502, 503, 504]
49
+ */
50
+ retryableStatuses?: number[];
51
+ /**
52
+ * Whether to retry on network errors
53
+ * @default true
54
+ */
55
+ retryOnNetworkError?: boolean;
56
+ /**
57
+ * Whether to retry on timeout errors
58
+ * @default true
59
+ */
60
+ retryOnTimeout?: boolean;
61
+ /**
62
+ * Initial delay between retries in milliseconds
63
+ * @default 1000
64
+ */
65
+ initialDelayMs?: number;
66
+ /**
67
+ * Maximum delay between retries in milliseconds
68
+ * @default 30000
69
+ */
70
+ maxDelayMs?: number;
71
+ /**
72
+ * Multiplier for exponential backoff
73
+ * @default 2
74
+ */
75
+ backoffMultiplier?: number;
76
+ /**
77
+ * Jitter factor (0-1) to add randomness to delays
78
+ * @default 0.25
79
+ */
80
+ jitter?: number;
81
+ }
82
+ /**
83
+ * Hooks for observability and customization
84
+ */
85
+ interface Hooks {
86
+ /**
87
+ * Called before each request is sent
88
+ * Can modify the request or throw to abort
89
+ */
90
+ onRequest?: (context: RequestContext) => void | Promise<void>;
91
+ /**
92
+ * Called after each successful response
93
+ */
94
+ onResponse?: (context: ResponseContext) => void | Promise<void>;
95
+ /**
96
+ * Called when an error occurs (before retry)
97
+ */
98
+ onError?: (error: Error, context: RequestContext) => void | Promise<void>;
99
+ /**
100
+ * Called before each retry attempt
101
+ */
102
+ onRetry?: (context: RequestContext, error: Error, delayMs: number) => void | Promise<void>;
103
+ }
5
104
  interface HttpClientConfig {
6
105
  baseUrl: string;
7
106
  apiKey: string;
@@ -12,6 +111,14 @@ interface HttpClientConfig {
12
111
  * Custom fetch implementation for testing/mocking
13
112
  */
14
113
  fetch?: FetchFunction;
114
+ /**
115
+ * Fine-grained retry configuration
116
+ */
117
+ retry?: RetryConfig;
118
+ /**
119
+ * Hooks for observability
120
+ */
121
+ hooks?: Hooks;
15
122
  }
16
123
  interface RequestOptions {
17
124
  method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
@@ -20,40 +127,103 @@ interface RequestOptions {
20
127
  query?: Record<string, string | number | boolean | undefined>;
21
128
  headers?: Record<string, string>;
22
129
  idempotencyKey?: string;
130
+ /**
131
+ * AbortSignal to cancel the request
132
+ */
133
+ signal?: AbortSignal;
23
134
  }
24
135
  /**
25
- * HTTP client with retry logic and error handling
136
+ * Rate limit information from response headers
137
+ */
138
+ interface RateLimitInfo {
139
+ /** Maximum requests allowed in the current window */
140
+ limit: number;
141
+ /** Remaining requests in the current window */
142
+ remaining: number;
143
+ /** Unix timestamp (seconds) when the rate limit resets */
144
+ reset: number;
145
+ }
146
+ /**
147
+ * Response wrapper with rate limit info
148
+ */
149
+ interface ResponseWithRateLimit<T> {
150
+ data: T;
151
+ rateLimit?: RateLimitInfo;
152
+ requestId?: string;
153
+ }
154
+ /**
155
+ * HTTP client with retry logic, hooks, and error handling
26
156
  */
27
157
  declare class HttpClient {
28
158
  private readonly config;
29
159
  private readonly fetchFn;
160
+ private readonly retryConfig;
161
+ /** Rate limit info from the last response */
162
+ private _lastRateLimit?;
163
+ /** Request ID from the last response */
164
+ private _lastRequestId?;
30
165
  constructor(config: HttpClientConfig);
166
+ /**
167
+ * Get rate limit info from the last response
168
+ */
169
+ get lastRateLimit(): RateLimitInfo | undefined;
170
+ /**
171
+ * Get request ID from the last response
172
+ */
173
+ get lastRequestId(): string | undefined;
31
174
  /**
32
175
  * Execute an HTTP request with retry logic
33
176
  */
34
177
  request<T>(options: RequestOptions): Promise<T>;
178
+ /**
179
+ * Execute request and return response with rate limit info
180
+ */
181
+ requestWithRateLimit<T>(options: RequestOptions): Promise<ResponseWithRateLimit<T>>;
182
+ private shouldRetry;
183
+ private calculateDelay;
35
184
  private buildUrl;
36
185
  private buildHeaders;
37
186
  private executeRequest;
187
+ private extractRateLimitInfo;
38
188
  private handleResponse;
39
- private calculateBackoff;
40
189
  private sleep;
41
190
  }
42
191
 
192
+ /**
193
+ * Options for API requests
194
+ */
195
+ interface ApiRequestOptions {
196
+ /**
197
+ * AbortSignal to cancel the request
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * const controller = new AbortController();
202
+ * setTimeout(() => controller.abort(), 5000);
203
+ *
204
+ * const ticket = await client.tickets.get('ws_abc', 'tkt_xyz', {
205
+ * signal: controller.signal,
206
+ * });
207
+ * ```
208
+ */
209
+ signal?: AbortSignal;
210
+ }
43
211
  /**
44
212
  * Base class for all resource classes
213
+ * @internal
45
214
  */
46
215
  declare abstract class BaseResource {
47
216
  protected readonly http: HttpClient;
48
217
  constructor(http: HttpClient);
49
- protected _get<T>(path: string, query?: RequestOptions['query']): Promise<T>;
218
+ protected _get<T>(path: string, query?: RequestOptions['query'], options?: ApiRequestOptions): Promise<T>;
50
219
  protected _post<T>(path: string, body?: unknown, options?: {
51
220
  idempotencyKey?: string;
52
221
  query?: RequestOptions['query'];
222
+ signal?: AbortSignal;
53
223
  }): Promise<T>;
54
- protected _patch<T>(path: string, body?: unknown): Promise<T>;
55
- protected _put<T>(path: string, body?: unknown): Promise<T>;
56
- protected _delete<T>(path: string, query?: RequestOptions['query']): Promise<T>;
224
+ protected _patch<T>(path: string, body?: unknown, options?: ApiRequestOptions): Promise<T>;
225
+ protected _put<T>(path: string, body?: unknown, options?: ApiRequestOptions): Promise<T>;
226
+ protected _delete<T>(path: string, query?: RequestOptions['query'], options?: ApiRequestOptions): Promise<T>;
57
227
  }
58
228
 
59
229
  /**
@@ -176,6 +346,8 @@ interface Brand {
176
346
  autoresponseBody?: string;
177
347
  fromName?: string;
178
348
  fromEmail?: string;
349
+ /** Allowed origins for portal API CORS */
350
+ portalOrigins: string[];
179
351
  createdAt: string;
180
352
  updatedAt: string;
181
353
  }
@@ -206,6 +378,8 @@ interface UpdateBrandInput {
206
378
  autoresponseBody?: string;
207
379
  metadata?: Record<string, unknown>;
208
380
  ticketSchema?: Record<string, unknown>;
381
+ /** Allowed origins for portal API CORS. Empty array = block all cross-origin requests. */
382
+ portalOrigins?: string[];
209
383
  }
210
384
  /**
211
385
  * Brand deletion preview result
@@ -217,6 +391,134 @@ interface DeleteBrandPreview {
217
391
  tagCount: number;
218
392
  }
219
393
 
394
+ /**
395
+ * Portal API Types
396
+ *
397
+ * Types for the customer-facing portal API.
398
+ * Used with the DispatchPortal client.
399
+ */
400
+ /**
401
+ * Response from portal token generation or verification
402
+ */
403
+ interface PortalTokenResponse {
404
+ /** JWT portal token for Authorization header */
405
+ token: string;
406
+ /** ISO 8601 expiration timestamp */
407
+ expiresAt: string;
408
+ /** Customer ID */
409
+ customerId: string;
410
+ /** Customer email */
411
+ email: string;
412
+ /** Customer name (if provided) */
413
+ name?: string;
414
+ }
415
+ /**
416
+ * Input for generating a portal token
417
+ */
418
+ interface GeneratePortalTokenInput {
419
+ /** Customer email address */
420
+ email: string;
421
+ /** Customer name (optional) */
422
+ name?: string;
423
+ }
424
+ /**
425
+ * Input for sending a magic link email
426
+ */
427
+ interface SendMagicLinkInput {
428
+ /** Customer email address */
429
+ email: string;
430
+ /** URL to redirect to after verification (your portal URL) */
431
+ portalUrl: string;
432
+ }
433
+ /**
434
+ * Ticket summary (returned in list views)
435
+ */
436
+ interface PortalTicket {
437
+ /** Ticket ID */
438
+ id: string;
439
+ /** Formatted ticket number (e.g., "TKT-1001") */
440
+ ticketNumber: string;
441
+ /** Ticket title */
442
+ title: string;
443
+ /** Current status */
444
+ status: string;
445
+ /** Priority level */
446
+ priority: string;
447
+ /** Number of comments on the ticket */
448
+ commentCount: number;
449
+ /** Creation timestamp */
450
+ createdAt: string;
451
+ /** Last update timestamp */
452
+ updatedAt: string;
453
+ }
454
+ /**
455
+ * Ticket detail with body and comments
456
+ */
457
+ interface PortalTicketDetail extends PortalTicket {
458
+ /** Ticket body/description */
459
+ body: string;
460
+ /** Brand information */
461
+ brand: {
462
+ /** Brand name */
463
+ name: string;
464
+ };
465
+ /** Comments on the ticket (excludes internal comments) */
466
+ comments: PortalComment[];
467
+ }
468
+ /**
469
+ * Comment on a ticket
470
+ */
471
+ interface PortalComment {
472
+ /** Comment ID */
473
+ id: string;
474
+ /** Comment body */
475
+ body: string;
476
+ /** Author type */
477
+ authorType: 'CUSTOMER' | 'STAFF';
478
+ /** Author display name */
479
+ authorName: string;
480
+ /** Creation timestamp */
481
+ createdAt: string;
482
+ }
483
+ /**
484
+ * Input for creating a ticket via portal
485
+ */
486
+ interface PortalCreateTicketInput {
487
+ /** Ticket title */
488
+ title: string;
489
+ /** Ticket body/description (optional) */
490
+ body?: string;
491
+ }
492
+ /**
493
+ * Filters for listing tickets
494
+ */
495
+ interface PortalListTicketsFilters {
496
+ /** Filter by status */
497
+ status?: string;
498
+ /** Sort field */
499
+ sort?: 'createdAt' | 'updatedAt';
500
+ /** Sort order */
501
+ order?: 'asc' | 'desc';
502
+ /** Maximum results per page (1-50, default 20) */
503
+ limit?: number;
504
+ /** Pagination cursor */
505
+ cursor?: string;
506
+ }
507
+ /**
508
+ * Paginated list of tickets
509
+ */
510
+ interface PortalTicketListResponse {
511
+ /** Array of tickets */
512
+ data: PortalTicket[];
513
+ /** Pagination info */
514
+ pagination: {
515
+ /** Whether more results are available */
516
+ hasMore: boolean;
517
+ /** Cursor for next page (null if no more) */
518
+ nextCursor: string | null;
519
+ };
520
+ }
521
+
220
522
  /**
221
523
  * Brands resource for managing workspaces
222
524
  */
@@ -271,6 +573,61 @@ declare class BrandsResource extends BaseResource {
271
573
  * ```
272
574
  */
273
575
  getInboundEmail(brandId: string, domain?: string): string;
576
+ /**
577
+ * Generate a portal token for a customer
578
+ *
579
+ * Use this for "authenticated mode" where your backend already knows who the
580
+ * customer is (they're logged into your app). Generate a portal token and
581
+ * pass it to your frontend to initialize the DispatchPortal client.
582
+ *
583
+ * @param brandId - The brand ID
584
+ * @param data - Customer email and optional name
585
+ * @returns Portal token response with JWT token and expiry
586
+ *
587
+ * @example
588
+ * ```typescript
589
+ * // On your backend (Node.js, Next.js API route, etc.)
590
+ * const { token, expiresAt } = await client.brands.generatePortalToken(
591
+ * 'br_abc123',
592
+ * {
593
+ * email: req.user.email,
594
+ * name: req.user.name,
595
+ * }
596
+ * );
597
+ *
598
+ * // Return token to your frontend
599
+ * res.json({ portalToken: token });
600
+ * ```
601
+ */
602
+ generatePortalToken(brandId: string, data: GeneratePortalTokenInput): Promise<PortalTokenResponse>;
603
+ /**
604
+ * Send a magic link email to a customer
605
+ *
606
+ * Use this for "self-auth mode" where customers access the portal without
607
+ * being logged into your app. They receive an email with a link that
608
+ * authenticates them directly.
609
+ *
610
+ * @param brandId - The brand ID
611
+ * @param data - Customer email and portal URL to redirect to
612
+ * @returns Success status
613
+ *
614
+ * @example
615
+ * ```typescript
616
+ * // Customer requests access via your portal login form
617
+ * await client.brands.sendPortalMagicLink('br_abc123', {
618
+ * email: 'customer@example.com',
619
+ * portalUrl: 'https://yourapp.com/support/portal',
620
+ * });
621
+ *
622
+ * // Customer receives email with link like:
623
+ * // https://yourapp.com/support/portal?token=xyz...
624
+ *
625
+ * // Your portal page then calls DispatchPortal.verify(token)
626
+ * ```
627
+ */
628
+ sendPortalMagicLink(brandId: string, data: SendMagicLinkInput): Promise<{
629
+ success: boolean;
630
+ }>;
274
631
  }
275
632
 
276
633
  /**
@@ -1024,10 +1381,30 @@ declare class FieldsResource extends BaseResource {
1024
1381
 
1025
1382
  /**
1026
1383
  * Configuration options for the Dispatch Tickets client
1384
+ *
1385
+ * @example
1386
+ * ```typescript
1387
+ * const client = new DispatchTickets({
1388
+ * apiKey: 'sk_live_...',
1389
+ * timeout: 30000,
1390
+ * retry: {
1391
+ * maxRetries: 5,
1392
+ * retryableStatuses: [429, 500, 502, 503, 504],
1393
+ * },
1394
+ * hooks: {
1395
+ * onRequest: (ctx) => console.log(`${ctx.method} ${ctx.url}`),
1396
+ * onResponse: (ctx) => console.log(`${ctx.status} in ${ctx.durationMs}ms`),
1397
+ * },
1398
+ * });
1399
+ * ```
1027
1400
  */
1028
1401
  interface DispatchTicketsConfig {
1029
1402
  /**
1030
1403
  * Your API key (required)
1404
+ *
1405
+ * Get your API key from the Dispatch Tickets dashboard.
1406
+ * Keys starting with `sk_live_` are production keys.
1407
+ * Keys starting with `sk_test_` are test keys.
1031
1408
  */
1032
1409
  apiKey: string;
1033
1410
  /**
@@ -1043,22 +1420,90 @@ interface DispatchTicketsConfig {
1043
1420
  /**
1044
1421
  * Maximum number of retries for failed requests
1045
1422
  * @default 3
1423
+ * @deprecated Use `retry.maxRetries` instead for fine-grained control
1046
1424
  */
1047
1425
  maxRetries?: number;
1048
1426
  /**
1049
1427
  * Enable debug logging
1428
+ *
1429
+ * When enabled, logs all requests, responses, and request IDs to console.
1050
1430
  * @default false
1051
1431
  */
1052
1432
  debug?: boolean;
1053
1433
  /**
1054
1434
  * Custom fetch implementation for testing/mocking
1435
+ *
1436
+ * @example
1437
+ * ```typescript
1438
+ * const mockFetch = vi.fn().mockResolvedValue({
1439
+ * ok: true,
1440
+ * status: 200,
1441
+ * headers: { get: () => 'application/json' },
1442
+ * json: () => Promise.resolve({ id: 'test' }),
1443
+ * });
1444
+ *
1445
+ * const client = new DispatchTickets({
1446
+ * apiKey: 'sk_test_...',
1447
+ * fetch: mockFetch,
1448
+ * });
1449
+ * ```
1055
1450
  */
1056
1451
  fetch?: FetchFunction;
1452
+ /**
1453
+ * Fine-grained retry configuration
1454
+ *
1455
+ * @example
1456
+ * ```typescript
1457
+ * const client = new DispatchTickets({
1458
+ * apiKey: 'sk_live_...',
1459
+ * retry: {
1460
+ * maxRetries: 5,
1461
+ * retryableStatuses: [429, 500, 502, 503, 504],
1462
+ * initialDelayMs: 500,
1463
+ * maxDelayMs: 60000,
1464
+ * backoffMultiplier: 2,
1465
+ * jitter: 0.25,
1466
+ * },
1467
+ * });
1468
+ * ```
1469
+ */
1470
+ retry?: RetryConfig;
1471
+ /**
1472
+ * Hooks for observability and customization
1473
+ *
1474
+ * @example
1475
+ * ```typescript
1476
+ * const client = new DispatchTickets({
1477
+ * apiKey: 'sk_live_...',
1478
+ * hooks: {
1479
+ * onRequest: (ctx) => {
1480
+ * console.log(`[${new Date().toISOString()}] ${ctx.method} ${ctx.url}`);
1481
+ * },
1482
+ * onResponse: (ctx) => {
1483
+ * console.log(`[${ctx.requestId}] ${ctx.status} in ${ctx.durationMs}ms`);
1484
+ * },
1485
+ * onError: (error, ctx) => {
1486
+ * console.error(`Request failed: ${error.message}`);
1487
+ * // Send to error tracking service
1488
+ * Sentry.captureException(error);
1489
+ * },
1490
+ * onRetry: (ctx, error, delayMs) => {
1491
+ * console.log(`Retrying in ${delayMs}ms (attempt ${ctx.attempt + 1})`);
1492
+ * },
1493
+ * },
1494
+ * });
1495
+ * ```
1496
+ */
1497
+ hooks?: Hooks;
1057
1498
  }
1058
1499
  /**
1059
1500
  * Dispatch Tickets SDK client
1060
1501
  *
1061
- * @example
1502
+ * The main entry point for interacting with the Dispatch Tickets API.
1503
+ * Create a client instance with your API key and use the resource methods
1504
+ * to manage tickets, comments, attachments, and more.
1505
+ *
1506
+ * @example Basic usage
1062
1507
  * ```typescript
1063
1508
  * import { DispatchTickets } from '@dispatchtickets/sdk';
1064
1509
  *
@@ -1080,59 +1525,530 @@ interface DispatchTicketsConfig {
1080
1525
  * console.log(ticket.title);
1081
1526
  * }
1082
1527
  * ```
1528
+ *
1529
+ * @example With request cancellation
1530
+ * ```typescript
1531
+ * const controller = new AbortController();
1532
+ *
1533
+ * // Cancel after 5 seconds
1534
+ * setTimeout(() => controller.abort(), 5000);
1535
+ *
1536
+ * try {
1537
+ * const tickets = await client.tickets.listPage('ws_abc123', {}, {
1538
+ * signal: controller.signal,
1539
+ * });
1540
+ * } catch (error) {
1541
+ * if (error.message === 'Request aborted by user') {
1542
+ * console.log('Request was cancelled');
1543
+ * }
1544
+ * }
1545
+ * ```
1546
+ *
1547
+ * @example With hooks for logging
1548
+ * ```typescript
1549
+ * const client = new DispatchTickets({
1550
+ * apiKey: 'sk_live_...',
1551
+ * hooks: {
1552
+ * onRequest: (ctx) => console.log(`→ ${ctx.method} ${ctx.url}`),
1553
+ * onResponse: (ctx) => console.log(`← ${ctx.status} (${ctx.durationMs}ms)`),
1554
+ * },
1555
+ * });
1556
+ * ```
1083
1557
  */
1084
1558
  declare class DispatchTickets {
1085
1559
  private readonly http;
1086
1560
  /**
1087
1561
  * Accounts resource for managing the current account and API keys
1562
+ *
1563
+ * @example
1564
+ * ```typescript
1565
+ * // Get current account
1566
+ * const account = await client.accounts.me();
1567
+ *
1568
+ * // Get usage statistics
1569
+ * const usage = await client.accounts.getUsage();
1570
+ *
1571
+ * // Create a new API key
1572
+ * const newKey = await client.accounts.createApiKey({
1573
+ * name: 'Production',
1574
+ * allBrands: true,
1575
+ * });
1576
+ * ```
1088
1577
  */
1089
1578
  readonly accounts: AccountsResource;
1090
1579
  /**
1091
1580
  * Brands (workspaces) resource
1581
+ *
1582
+ * Brands are isolated containers for tickets. Each brand can have its own
1583
+ * email address, categories, tags, and settings.
1584
+ *
1585
+ * @example
1586
+ * ```typescript
1587
+ * // List all brands
1588
+ * const brands = await client.brands.list();
1589
+ *
1590
+ * // Create a new brand
1591
+ * const brand = await client.brands.create({
1592
+ * name: 'Acme Support',
1593
+ * slug: 'acme',
1594
+ * });
1595
+ *
1596
+ * // Get inbound email address
1597
+ * const email = client.brands.getInboundEmail('br_abc123');
1598
+ * // Returns: br_abc123@inbound.dispatchtickets.com
1599
+ * ```
1092
1600
  */
1093
1601
  readonly brands: BrandsResource;
1094
1602
  /**
1095
1603
  * Tickets resource
1604
+ *
1605
+ * @example
1606
+ * ```typescript
1607
+ * // Create a ticket
1608
+ * const ticket = await client.tickets.create('ws_abc123', {
1609
+ * title: 'Issue with billing',
1610
+ * body: 'I was charged twice...',
1611
+ * priority: 'high',
1612
+ * });
1613
+ *
1614
+ * // Iterate through all tickets
1615
+ * for await (const ticket of client.tickets.list('ws_abc123', { status: 'open' })) {
1616
+ * console.log(ticket.title);
1617
+ * }
1618
+ * ```
1096
1619
  */
1097
1620
  readonly tickets: TicketsResource;
1098
1621
  /**
1099
1622
  * Comments resource
1623
+ *
1624
+ * @example
1625
+ * ```typescript
1626
+ * // Add a comment
1627
+ * const comment = await client.comments.create('ws_abc123', 'tkt_xyz', {
1628
+ * body: 'Thanks for your patience!',
1629
+ * authorType: 'AGENT',
1630
+ * });
1631
+ *
1632
+ * // List comments
1633
+ * const comments = await client.comments.list('ws_abc123', 'tkt_xyz');
1634
+ * ```
1100
1635
  */
1101
1636
  readonly comments: CommentsResource;
1102
1637
  /**
1103
1638
  * Attachments resource
1639
+ *
1640
+ * @example
1641
+ * ```typescript
1642
+ * // Simple upload
1643
+ * const attachment = await client.attachments.upload(
1644
+ * 'ws_abc123',
1645
+ * 'tkt_xyz',
1646
+ * fileBuffer,
1647
+ * 'document.pdf',
1648
+ * 'application/pdf'
1649
+ * );
1650
+ *
1651
+ * // Get download URL
1652
+ * const { downloadUrl } = await client.attachments.get('ws_abc123', 'tkt_xyz', 'att_abc');
1653
+ * ```
1104
1654
  */
1105
1655
  readonly attachments: AttachmentsResource;
1106
1656
  /**
1107
1657
  * Webhooks resource
1658
+ *
1659
+ * @example
1660
+ * ```typescript
1661
+ * // Create a webhook
1662
+ * const webhook = await client.webhooks.create('ws_abc123', {
1663
+ * url: 'https://example.com/webhook',
1664
+ * secret: 'your-secret',
1665
+ * events: ['ticket.created', 'ticket.updated'],
1666
+ * });
1667
+ * ```
1108
1668
  */
1109
1669
  readonly webhooks: WebhooksResource;
1110
1670
  /**
1111
1671
  * Categories resource
1672
+ *
1673
+ * @example
1674
+ * ```typescript
1675
+ * // Create a category
1676
+ * await client.categories.create('ws_abc123', { name: 'Billing', color: '#ef4444' });
1677
+ *
1678
+ * // Get category stats
1679
+ * const stats = await client.categories.getStats('ws_abc123');
1680
+ * ```
1112
1681
  */
1113
1682
  readonly categories: CategoriesResource;
1114
1683
  /**
1115
1684
  * Tags resource
1685
+ *
1686
+ * @example
1687
+ * ```typescript
1688
+ * // Create a tag
1689
+ * await client.tags.create('ws_abc123', { name: 'urgent', color: '#f59e0b' });
1690
+ *
1691
+ * // Merge tags
1692
+ * await client.tags.merge('ws_abc123', 'tag_target', ['tag_source1', 'tag_source2']);
1693
+ * ```
1116
1694
  */
1117
1695
  readonly tags: TagsResource;
1118
1696
  /**
1119
1697
  * Customers resource
1698
+ *
1699
+ * @example
1700
+ * ```typescript
1701
+ * // Create a customer
1702
+ * const customer = await client.customers.create('ws_abc123', {
1703
+ * email: 'user@example.com',
1704
+ * name: 'Jane Doe',
1705
+ * });
1706
+ *
1707
+ * // Search customers
1708
+ * const results = await client.customers.search('ws_abc123', 'jane');
1709
+ * ```
1120
1710
  */
1121
1711
  readonly customers: CustomersResource;
1122
1712
  /**
1123
1713
  * Custom fields resource
1714
+ *
1715
+ * @example
1716
+ * ```typescript
1717
+ * // Get all field definitions
1718
+ * const fields = await client.fields.getAll('ws_abc123');
1719
+ *
1720
+ * // Create a field
1721
+ * await client.fields.create('ws_abc123', 'ticket', {
1722
+ * key: 'order_id',
1723
+ * label: 'Order ID',
1724
+ * type: 'text',
1725
+ * required: true,
1726
+ * });
1727
+ * ```
1124
1728
  */
1125
1729
  readonly fields: FieldsResource;
1126
1730
  /**
1127
1731
  * Static webhook utilities
1732
+ *
1733
+ * @example
1734
+ * ```typescript
1735
+ * // Verify webhook signature
1736
+ * const isValid = DispatchTickets.webhooks.verifySignature(
1737
+ * rawBody,
1738
+ * req.headers['x-dispatch-signature'],
1739
+ * 'your-secret'
1740
+ * );
1741
+ *
1742
+ * // Generate signature for testing
1743
+ * const signature = DispatchTickets.webhooks.generateSignature(
1744
+ * JSON.stringify(payload),
1745
+ * 'your-secret'
1746
+ * );
1747
+ * ```
1128
1748
  */
1129
1749
  static readonly webhooks: {
1130
1750
  verifySignature(payload: string, signature: string, secret: string): boolean;
1131
1751
  generateSignature(payload: string, secret: string): string;
1132
1752
  };
1753
+ /**
1754
+ * Create a new Dispatch Tickets client
1755
+ *
1756
+ * @param config - Client configuration options
1757
+ * @throws Error if API key is not provided
1758
+ *
1759
+ * @example
1760
+ * ```typescript
1761
+ * const client = new DispatchTickets({
1762
+ * apiKey: 'sk_live_...',
1763
+ * });
1764
+ * ```
1765
+ */
1133
1766
  constructor(config: DispatchTicketsConfig);
1134
1767
  }
1135
1768
 
1769
+ /**
1770
+ * Options for creating a ticket via portal
1771
+ */
1772
+ interface PortalCreateTicketOptions extends ApiRequestOptions {
1773
+ /** Idempotency key to prevent duplicate creation */
1774
+ idempotencyKey?: string;
1775
+ }
1776
+ /**
1777
+ * Options for adding a comment via portal
1778
+ */
1779
+ interface PortalAddCommentOptions extends ApiRequestOptions {
1780
+ /** Idempotency key to prevent duplicate creation */
1781
+ idempotencyKey?: string;
1782
+ }
1783
+ /**
1784
+ * Portal tickets resource
1785
+ *
1786
+ * Provides methods for customers to manage their tickets via the portal API.
1787
+ *
1788
+ * @example
1789
+ * ```typescript
1790
+ * const portal = new DispatchPortal({ token: 'portal_token...' });
1791
+ *
1792
+ * // List tickets
1793
+ * const { data: tickets } = await portal.tickets.list();
1794
+ *
1795
+ * // Create a ticket
1796
+ * const ticket = await portal.tickets.create({
1797
+ * title: 'Need help with my order',
1798
+ * body: 'Order #12345 has not arrived...',
1799
+ * });
1800
+ *
1801
+ * // Add a comment
1802
+ * await portal.tickets.addComment(ticket.id, 'Any update on this?');
1803
+ * ```
1804
+ */
1805
+ declare class PortalTicketsResource extends BaseResource {
1806
+ /**
1807
+ * List the customer's tickets
1808
+ *
1809
+ * Returns a paginated list of tickets belonging to the authenticated customer.
1810
+ *
1811
+ * @param filters - Optional filters and pagination
1812
+ * @param options - Request options
1813
+ * @returns Paginated ticket list
1814
+ *
1815
+ * @example
1816
+ * ```typescript
1817
+ * // List all tickets
1818
+ * const { data: tickets, pagination } = await portal.tickets.list();
1819
+ *
1820
+ * // Filter by status
1821
+ * const openTickets = await portal.tickets.list({ status: 'open' });
1822
+ *
1823
+ * // Paginate
1824
+ * const page2 = await portal.tickets.list({ cursor: pagination.nextCursor });
1825
+ * ```
1826
+ */
1827
+ list(filters?: PortalListTicketsFilters, options?: ApiRequestOptions): Promise<PortalTicketListResponse>;
1828
+ /**
1829
+ * Iterate through all tickets with automatic pagination
1830
+ *
1831
+ * @param filters - Optional filters (cursor is managed automatically)
1832
+ * @returns Async iterator of tickets
1833
+ *
1834
+ * @example
1835
+ * ```typescript
1836
+ * for await (const ticket of portal.tickets.listAll({ status: 'open' })) {
1837
+ * console.log(ticket.title);
1838
+ * }
1839
+ * ```
1840
+ */
1841
+ listAll(filters?: Omit<PortalListTicketsFilters, 'cursor'>): AsyncIterable<PortalTicket>;
1842
+ /**
1843
+ * Get a ticket by ID
1844
+ *
1845
+ * Returns the ticket with its comments (excludes internal comments).
1846
+ *
1847
+ * @param ticketId - Ticket ID
1848
+ * @param options - Request options
1849
+ * @returns Ticket detail with comments
1850
+ *
1851
+ * @example
1852
+ * ```typescript
1853
+ * const ticket = await portal.tickets.get('tkt_abc123');
1854
+ * console.log(ticket.title);
1855
+ * console.log(`${ticket.comments.length} comments`);
1856
+ * ```
1857
+ */
1858
+ get(ticketId: string, options?: ApiRequestOptions): Promise<PortalTicketDetail>;
1859
+ /**
1860
+ * Create a new ticket
1861
+ *
1862
+ * @param data - Ticket data
1863
+ * @param options - Request options including idempotency key
1864
+ * @returns Created ticket
1865
+ *
1866
+ * @example
1867
+ * ```typescript
1868
+ * const ticket = await portal.tickets.create({
1869
+ * title: 'Need help with billing',
1870
+ * body: 'I was charged twice for my subscription...',
1871
+ * });
1872
+ * ```
1873
+ */
1874
+ create(data: PortalCreateTicketInput, options?: PortalCreateTicketOptions): Promise<PortalTicket>;
1875
+ /**
1876
+ * Add a comment to a ticket
1877
+ *
1878
+ * @param ticketId - Ticket ID
1879
+ * @param body - Comment text
1880
+ * @param options - Request options including idempotency key
1881
+ * @returns Created comment
1882
+ *
1883
+ * @example
1884
+ * ```typescript
1885
+ * const comment = await portal.tickets.addComment(
1886
+ * 'tkt_abc123',
1887
+ * 'Here is the additional information you requested...'
1888
+ * );
1889
+ * ```
1890
+ */
1891
+ addComment(ticketId: string, body: string, options?: PortalAddCommentOptions): Promise<PortalComment>;
1892
+ /**
1893
+ * Build query parameters from filters
1894
+ */
1895
+ private buildListQuery;
1896
+ }
1897
+
1898
+ /**
1899
+ * Configuration options for the Dispatch Portal client
1900
+ *
1901
+ * @example
1902
+ * ```typescript
1903
+ * const portal = new DispatchPortal({
1904
+ * token: 'portal_token_from_backend...',
1905
+ * });
1906
+ * ```
1907
+ */
1908
+ interface DispatchPortalConfig {
1909
+ /**
1910
+ * Portal token (required)
1911
+ *
1912
+ * Obtain this token by:
1913
+ * 1. Your backend calls `client.brands.generatePortalToken()` with the customer's email
1914
+ * 2. Pass the token to your frontend
1915
+ * 3. Frontend creates DispatchPortal with this token
1916
+ *
1917
+ * Or for self-auth mode:
1918
+ * 1. Customer clicks magic link email
1919
+ * 2. Your portal page calls `DispatchPortal.verify()` with the URL token
1920
+ * 3. Use the returned token to create DispatchPortal
1921
+ */
1922
+ token: string;
1923
+ /**
1924
+ * Base URL for the API
1925
+ * @default 'https://dispatch-tickets-api.onrender.com/v1'
1926
+ */
1927
+ baseUrl?: string;
1928
+ /**
1929
+ * Request timeout in milliseconds
1930
+ * @default 30000
1931
+ */
1932
+ timeout?: number;
1933
+ /**
1934
+ * Custom fetch implementation (for testing)
1935
+ */
1936
+ fetch?: FetchFunction;
1937
+ }
1938
+ /**
1939
+ * Dispatch Portal SDK client
1940
+ *
1941
+ * Customer-facing client for interacting with the portal API.
1942
+ * Use this to let customers view and manage their own tickets.
1943
+ *
1944
+ * @example Authenticated mode (backend generates token)
1945
+ * ```typescript
1946
+ * // On your backend:
1947
+ * const client = new DispatchTickets({ apiKey: 'sk_live_...' });
1948
+ * const { token } = await client.brands.generatePortalToken('br_abc123', {
1949
+ * email: 'customer@example.com',
1950
+ * name: 'Jane Doe',
1951
+ * });
1952
+ * // Pass token to frontend...
1953
+ *
1954
+ * // On your frontend:
1955
+ * const portal = new DispatchPortal({ token });
1956
+ * const { data: tickets } = await portal.tickets.list();
1957
+ * ```
1958
+ *
1959
+ * @example Self-auth mode (magic link)
1960
+ * ```typescript
1961
+ * // Customer clicks magic link, lands on your portal with ?token=xyz
1962
+ * const urlToken = new URLSearchParams(window.location.search).get('token');
1963
+ *
1964
+ * // Verify the magic link token
1965
+ * const { token } = await DispatchPortal.verify(urlToken);
1966
+ *
1967
+ * // Now use the portal token
1968
+ * const portal = new DispatchPortal({ token });
1969
+ * const { data: tickets } = await portal.tickets.list();
1970
+ * ```
1971
+ */
1972
+ declare class DispatchPortal {
1973
+ private readonly http;
1974
+ private readonly config;
1975
+ /**
1976
+ * Tickets resource for viewing and creating tickets
1977
+ *
1978
+ * @example
1979
+ * ```typescript
1980
+ * // List tickets
1981
+ * const { data: tickets } = await portal.tickets.list();
1982
+ *
1983
+ * // Create a ticket
1984
+ * const ticket = await portal.tickets.create({
1985
+ * title: 'Need help',
1986
+ * body: 'Something is broken...',
1987
+ * });
1988
+ *
1989
+ * // Add a comment
1990
+ * await portal.tickets.addComment(ticket.id, 'Here is more info...');
1991
+ * ```
1992
+ */
1993
+ readonly tickets: PortalTicketsResource;
1994
+ /**
1995
+ * Create a new Dispatch Portal client
1996
+ *
1997
+ * @param config - Portal client configuration
1998
+ * @throws Error if token is not provided
1999
+ */
2000
+ constructor(config: DispatchPortalConfig);
2001
+ /**
2002
+ * Verify a magic link token and get a portal token
2003
+ *
2004
+ * Call this when the customer clicks the magic link and lands on your portal.
2005
+ * The magic link contains a short-lived token that can be exchanged for a
2006
+ * longer-lived portal token.
2007
+ *
2008
+ * @param magicLinkToken - The token from the magic link URL
2009
+ * @param baseUrl - Optional API base URL
2010
+ * @param fetchFn - Optional custom fetch function (for testing)
2011
+ * @returns Portal token response
2012
+ *
2013
+ * @example
2014
+ * ```typescript
2015
+ * // Customer lands on: https://yourapp.com/portal?token=xyz
2016
+ * const urlToken = new URLSearchParams(window.location.search).get('token');
2017
+ *
2018
+ * const { token, email, name } = await DispatchPortal.verify(urlToken);
2019
+ *
2020
+ * // Store token and create client
2021
+ * localStorage.setItem('portalToken', token);
2022
+ * const portal = new DispatchPortal({ token });
2023
+ * ```
2024
+ */
2025
+ static verify(magicLinkToken: string, baseUrl?: string, fetchFn?: FetchFunction): Promise<PortalTokenResponse>;
2026
+ /**
2027
+ * Refresh the current portal token
2028
+ *
2029
+ * Call this before the token expires to get a new token with extended expiry.
2030
+ * The new token will have the same customer context.
2031
+ *
2032
+ * @returns New portal token response
2033
+ *
2034
+ * @example
2035
+ * ```typescript
2036
+ * // Check if token is close to expiry and refresh
2037
+ * const { token: newToken, expiresAt } = await portal.refresh();
2038
+ *
2039
+ * // Create new client with refreshed token
2040
+ * const newPortal = new DispatchPortal({ token: newToken });
2041
+ * ```
2042
+ */
2043
+ refresh(): Promise<PortalTokenResponse>;
2044
+ /**
2045
+ * Get the current token
2046
+ *
2047
+ * Useful for storing/passing the token.
2048
+ */
2049
+ get token(): string;
2050
+ }
2051
+
1136
2052
  /**
1137
2053
  * Base error class for all Dispatch Tickets SDK errors
1138
2054
  */
@@ -1140,20 +2056,32 @@ declare class DispatchTicketsError extends Error {
1140
2056
  readonly code: string;
1141
2057
  readonly statusCode?: number;
1142
2058
  readonly details?: Record<string, unknown>;
1143
- constructor(message: string, code: string, statusCode?: number, details?: Record<string, unknown>);
2059
+ /** Request ID for debugging with support */
2060
+ readonly requestId?: string;
2061
+ constructor(message: string, code: string, statusCode?: number, details?: Record<string, unknown>, requestId?: string);
1144
2062
  }
1145
2063
  /**
1146
2064
  * Thrown when API key is missing or invalid
1147
2065
  */
1148
2066
  declare class AuthenticationError extends DispatchTicketsError {
1149
- constructor(message?: string);
2067
+ constructor(message?: string, requestId?: string);
1150
2068
  }
1151
2069
  /**
1152
2070
  * Thrown when rate limit is exceeded
1153
2071
  */
1154
2072
  declare class RateLimitError extends DispatchTicketsError {
1155
2073
  readonly retryAfter?: number;
1156
- constructor(message?: string, retryAfter?: number);
2074
+ /** Rate limit ceiling */
2075
+ readonly limit?: number;
2076
+ /** Remaining requests in current window */
2077
+ readonly remaining?: number;
2078
+ /** Unix timestamp when rate limit resets */
2079
+ readonly reset?: number;
2080
+ constructor(message?: string, retryAfter?: number, requestId?: string, rateLimitInfo?: {
2081
+ limit?: number;
2082
+ remaining?: number;
2083
+ reset?: number;
2084
+ });
1157
2085
  }
1158
2086
  /**
1159
2087
  * Thrown when request validation fails
@@ -1166,7 +2094,7 @@ declare class ValidationError extends DispatchTicketsError {
1166
2094
  constructor(message?: string, errors?: Array<{
1167
2095
  field: string;
1168
2096
  message: string;
1169
- }>);
2097
+ }>, requestId?: string);
1170
2098
  }
1171
2099
  /**
1172
2100
  * Thrown when a resource is not found
@@ -1174,19 +2102,19 @@ declare class ValidationError extends DispatchTicketsError {
1174
2102
  declare class NotFoundError extends DispatchTicketsError {
1175
2103
  readonly resourceType?: string;
1176
2104
  readonly resourceId?: string;
1177
- constructor(message?: string, resourceType?: string, resourceId?: string);
2105
+ constructor(message?: string, resourceType?: string, resourceId?: string, requestId?: string);
1178
2106
  }
1179
2107
  /**
1180
2108
  * Thrown when there's a conflict (e.g., duplicate resource)
1181
2109
  */
1182
2110
  declare class ConflictError extends DispatchTicketsError {
1183
- constructor(message?: string);
2111
+ constructor(message?: string, requestId?: string);
1184
2112
  }
1185
2113
  /**
1186
2114
  * Thrown when the server returns an unexpected error
1187
2115
  */
1188
2116
  declare class ServerError extends DispatchTicketsError {
1189
- constructor(message?: string, statusCode?: number);
2117
+ constructor(message?: string, statusCode?: number, requestId?: string);
1190
2118
  }
1191
2119
  /**
1192
2120
  * Thrown when request times out
@@ -1200,6 +2128,42 @@ declare class TimeoutError extends DispatchTicketsError {
1200
2128
  declare class NetworkError extends DispatchTicketsError {
1201
2129
  constructor(message?: string);
1202
2130
  }
2131
+ /**
2132
+ * Check if an error is a DispatchTicketsError
2133
+ */
2134
+ declare function isDispatchTicketsError(error: unknown): error is DispatchTicketsError;
2135
+ /**
2136
+ * Check if an error is an AuthenticationError
2137
+ */
2138
+ declare function isAuthenticationError(error: unknown): error is AuthenticationError;
2139
+ /**
2140
+ * Check if an error is a RateLimitError
2141
+ */
2142
+ declare function isRateLimitError(error: unknown): error is RateLimitError;
2143
+ /**
2144
+ * Check if an error is a ValidationError
2145
+ */
2146
+ declare function isValidationError(error: unknown): error is ValidationError;
2147
+ /**
2148
+ * Check if an error is a NotFoundError
2149
+ */
2150
+ declare function isNotFoundError(error: unknown): error is NotFoundError;
2151
+ /**
2152
+ * Check if an error is a ConflictError
2153
+ */
2154
+ declare function isConflictError(error: unknown): error is ConflictError;
2155
+ /**
2156
+ * Check if an error is a ServerError
2157
+ */
2158
+ declare function isServerError(error: unknown): error is ServerError;
2159
+ /**
2160
+ * Check if an error is a TimeoutError
2161
+ */
2162
+ declare function isTimeoutError(error: unknown): error is TimeoutError;
2163
+ /**
2164
+ * Check if an error is a NetworkError
2165
+ */
2166
+ declare function isNetworkError(error: unknown): error is NetworkError;
1203
2167
 
1204
2168
  /**
1205
2169
  * All supported webhook event types
@@ -1379,4 +2343,4 @@ declare const webhookUtils: {
1379
2343
  */
1380
2344
  declare function collectAll<T>(iterable: AsyncIterable<T>): Promise<T[]>;
1381
2345
 
1382
- export { type Account, type AccountUsage, type ApiKey, type ApiKeyWithSecret, type Attachment, type AttachmentStatus, type AttachmentWithUrl, AuthenticationError, type AuthorType, type Brand, type BulkAction, type BulkActionResult, type Category, type CategoryStats, type Comment, type CommentCreatedData, type CommentCreatedEvent, type Company, ConflictError, type CreateApiKeyInput, type CreateBrandInput, type CreateCategoryInput, type CreateCommentInput, type CreateCommentOptions, type CreateCustomerInput, type CreateFieldInput, type CreateTagInput, type CreateTicketInput, type CreateTicketOptions, type CreateWebhookInput, type Customer, type DeleteBrandPreview, DispatchTickets, type DispatchTicketsConfig, DispatchTicketsError, type EntityType, type EventCommentData, type EventCustomerInfo, type FieldDefinition, type FieldDefinitions, type FieldType, type InitiateUploadInput, type InitiateUploadResponse, type Link, type ListCustomersFilters, type ListTicketsFilters, type MergeTagsInput, type MergeTicketsInput, NetworkError, NotFoundError, type PaginatedResponse, RateLimitError, ServerError, type SortOrder, type Tag, type Ticket, type TicketCreatedData, type TicketCreatedEvent, type TicketPriority, type TicketSource, type TicketStatus, type TicketUpdatedData, type TicketUpdatedEvent, TimeoutError, type UpdateApiKeyScopeInput, type UpdateBrandInput, type UpdateCategoryInput, type UpdateCommentInput, type UpdateCustomerInput, type UpdateFieldInput, type UpdateTagInput, type UpdateTicketInput, ValidationError, type Webhook, type WebhookDelivery, type WebhookEvent, type WebhookEventEnvelope, type WebhookEventMap, type WebhookEventName, type WebhookEventType, collectAll, isCommentCreatedEvent, isTicketCreatedEvent, isTicketUpdatedEvent, parseWebhookEvent, webhookUtils };
2346
+ export { type Account, type AccountUsage, type ApiKey, type ApiKeyWithSecret, type ApiRequestOptions, type Attachment, type AttachmentStatus, type AttachmentWithUrl, AuthenticationError, type AuthorType, type Brand, type BulkAction, type BulkActionResult, type Category, type CategoryStats, type Comment, type CommentCreatedData, type CommentCreatedEvent, type Company, ConflictError, type CreateApiKeyInput, type CreateBrandInput, type CreateCategoryInput, type CreateCommentInput, type CreateCommentOptions, type CreateCustomerInput, type CreateFieldInput, type CreateTagInput, type CreateTicketInput, type CreateTicketOptions, type CreateWebhookInput, type Customer, type DeleteBrandPreview, DispatchPortal, type DispatchPortalConfig, DispatchTickets, type DispatchTicketsConfig, DispatchTicketsError, type EntityType, type EventCommentData, type EventCustomerInfo, type FieldDefinition, type FieldDefinitions, type FieldType, type GeneratePortalTokenInput, type Hooks, type InitiateUploadInput, type InitiateUploadResponse, type Link, type ListCustomersFilters, type ListTicketsFilters, type MergeTagsInput, type MergeTicketsInput, NetworkError, NotFoundError, type PaginatedResponse, type PortalComment, type PortalCreateTicketInput, type PortalListTicketsFilters, type PortalTicket, type PortalTicketDetail, type PortalTicketListResponse, type PortalTokenResponse, RateLimitError, type RateLimitInfo, type RequestContext, type ResponseContext, type RetryConfig, type SendMagicLinkInput, ServerError, type SortOrder, type Tag, type Ticket, type TicketCreatedData, type TicketCreatedEvent, type TicketPriority, type TicketSource, type TicketStatus, type TicketUpdatedData, type TicketUpdatedEvent, TimeoutError, type UpdateApiKeyScopeInput, type UpdateBrandInput, type UpdateCategoryInput, type UpdateCommentInput, type UpdateCustomerInput, type UpdateFieldInput, type UpdateTagInput, type UpdateTicketInput, ValidationError, type Webhook, type WebhookDelivery, type WebhookEvent, type WebhookEventEnvelope, type WebhookEventMap, type WebhookEventName, type WebhookEventType, collectAll, isAuthenticationError, isCommentCreatedEvent, isConflictError, isDispatchTicketsError, isNetworkError, isNotFoundError, isRateLimitError, isServerError, isTicketCreatedEvent, isTicketUpdatedEvent, isTimeoutError, isValidationError, parseWebhookEvent, webhookUtils };