@masters-union/outbound-sdk 0.4.5 → 0.4.6

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
@@ -47,7 +47,35 @@ interface SendEmailResponse {
47
47
  interface BulkEmailParams {
48
48
  fromEmail: string;
49
49
  emailSubject: string;
50
- emails: BulkEmailRecipient[];
50
+ /**
51
+ * Inline recipients, up to 1000 per request. Mutually exclusive with `listId`.
52
+ * Required unless `listId` is given.
53
+ */
54
+ emails?: BulkEmailRecipient[];
55
+ /**
56
+ * Send to a saved contact list instead. NO RECIPIENT CAP: the server expands the
57
+ * list in the background and returns one job id to track. `htmlBody` is required
58
+ * in this mode, and `mapping` says which contact column feeds each `{{variable}}`.
59
+ */
60
+ listId?: string;
61
+ /**
62
+ * OPTIONAL shared HTML body (mail merge). Supply it once and give each recipient
63
+ * a small `variables` object instead of its own full body: the template then
64
+ * crosses the wire once rather than once per recipient.
65
+ *
66
+ * BACKWARD COMPATIBLE: rendering happens if and only if a recipient carries a
67
+ * `variables` object. A recipient without one is sent verbatim, even if its body
68
+ * contains `{{...}}`.
69
+ */
70
+ htmlBody?: string;
71
+ /** OPTIONAL shared plain-text body (mail merge). */
72
+ textBody?: string;
73
+ /** `{ variableName: columnName }` for a list send. */
74
+ mapping?: VariableMapping;
75
+ /** Values for variables no column supplies, applied to every recipient. */
76
+ defaults?: Record<string, string>;
77
+ /** Whether a contact missing a variable is skipped (default) or sent anyway. */
78
+ onMissingVariable?: 'skip' | 'send';
51
79
  senderName?: string;
52
80
  replyTo?: string;
53
81
  /**
@@ -63,7 +91,13 @@ interface BulkEmailParams {
63
91
  }
64
92
  interface BulkEmailRecipient {
65
93
  toEmail: string;
66
- htmlBody: string;
94
+ /** Required unless a shared top-level `htmlBody` is supplied. */
95
+ htmlBody?: string;
96
+ /**
97
+ * Mail-merge values for this recipient. Its PRESENCE is what opts the entry into
98
+ * server-side rendering, so omit it entirely for a body you want sent verbatim.
99
+ */
100
+ variables?: Record<string, string>;
67
101
  subject?: string;
68
102
  textBody?: string;
69
103
  ccEmail?: string;
@@ -268,6 +302,16 @@ interface TemplateSendParams {
268
302
  interface TemplateBulkSendParams {
269
303
  templateId: string;
270
304
  fromEmail: string;
305
+ /**
306
+ * Send to a saved contact list instead of `recipients`. No recipient cap — the
307
+ * server expands the list in the background.
308
+ */
309
+ listId?: string;
310
+ /** `{ variableName: columnName }` for a list send. */
311
+ mapping?: VariableMapping;
312
+ /** Values for variables no column supplies. */
313
+ defaults?: Record<string, string>;
314
+ onMissingVariable?: 'skip' | 'send';
271
315
  senderName?: string;
272
316
  replyTo?: string;
273
317
  /**
@@ -280,7 +324,8 @@ interface TemplateBulkSendParams {
280
324
  priority?: 'critical' | 'urgent' | 'high' | 'normal' | 'low' | 'deferred' | 'backlog';
281
325
  idempotencyKey?: string;
282
326
  campaignId?: string;
283
- recipients: TemplateBulkRecipient[];
327
+ /** Inline recipients, up to 1000. Mutually exclusive with `listId`. */
328
+ recipients?: TemplateBulkRecipient[];
284
329
  }
285
330
  interface TemplateBulkRecipient {
286
331
  toEmail: string;
@@ -782,6 +827,213 @@ interface CampaignAnalytics {
782
827
  bounceReasons: CampaignBounceReason[];
783
828
  timeseries: CampaignTimeseriesPoint[];
784
829
  }
830
+ /** Only `subscribed` is ever sent to. */
831
+ type ContactStatus = 'subscribed' | 'invalid' | 'bounced' | 'complained' | 'unsubscribed' | 'suppressed';
832
+ /** The precise cause behind a non-subscribed status. */
833
+ type ContactStatusReason = 'invalid_format' | 'too_long' | 'bounce' | 'complaint' | 'unsubscribe' | 'manual' | 'ses_send_reject';
834
+ interface ContactList {
835
+ id: string;
836
+ name: string;
837
+ description: string | null;
838
+ status: 'active' | 'deleting';
839
+ contactCount: number;
840
+ /** Every attribute key seen on import — the valid targets for a send `mapping`. */
841
+ fields: string[];
842
+ metadata: Record<string, unknown> | null;
843
+ lastImportAt: string | null;
844
+ lastSentAt: string | null;
845
+ lastValidatedAt: string | null;
846
+ createdAt: string;
847
+ updatedAt: string;
848
+ }
849
+ interface ContactListDetail {
850
+ list: ContactList;
851
+ /** How many contacts sit in each status. */
852
+ statusBreakdown: Record<ContactStatus, number>;
853
+ /** Shorthand for `statusBreakdown.subscribed` — how many this list can reach. */
854
+ sendable: number;
855
+ sendablePercent: number;
856
+ }
857
+ interface Contact {
858
+ id: string;
859
+ email: string;
860
+ /** The original CSV value, present only when sanitization changed it. */
861
+ emailRaw: string | null;
862
+ name: string | null;
863
+ attributes: Record<string, string>;
864
+ status: ContactStatus;
865
+ statusReason: ContactStatusReason | null;
866
+ statusChangedAt: string | null;
867
+ source: 'import' | 'api' | 'manual';
868
+ createdAt: string;
869
+ updatedAt: string;
870
+ }
871
+ interface CreateContactListParams {
872
+ name: string;
873
+ description?: string;
874
+ metadata?: Record<string, unknown>;
875
+ }
876
+ interface UpdateContactListParams {
877
+ name?: string;
878
+ description?: string | null;
879
+ metadata?: Record<string, unknown> | null;
880
+ }
881
+ interface ListContactListsParams {
882
+ page?: number;
883
+ limit?: number;
884
+ search?: string;
885
+ sort?: 'recent' | 'created' | 'name' | 'size';
886
+ }
887
+ interface ListContactListsResponse {
888
+ lists: ContactList[];
889
+ pagination: {
890
+ page: number;
891
+ limit: number;
892
+ total: number;
893
+ totalPages: number;
894
+ };
895
+ quota: {
896
+ used: number;
897
+ allocated: number;
898
+ remaining: number;
899
+ };
900
+ }
901
+ interface ImportContactRow {
902
+ email: string;
903
+ name?: string;
904
+ /** Anything else the CSV carried. These keys become the list's `fields`. */
905
+ attributes?: Record<string, string>;
906
+ }
907
+ interface ImportContactsParams {
908
+ rows: ImportContactRow[];
909
+ /**
910
+ * One id per FILE. Together with `chunkIndex` it makes a retried chunk a no-op
911
+ * instead of a double-insert, so a failed upload is safe to retry blindly.
912
+ */
913
+ importId?: string;
914
+ chunkIndex?: number;
915
+ /** Which row wins when the same address appears twice in one chunk. */
916
+ duplicatesInFile?: 'first' | 'last';
917
+ /**
918
+ * What to do with an address already in the list. `update` merges attributes and
919
+ * refreshes the name; it NEVER re-subscribes a bounced or opted-out contact.
920
+ */
921
+ onExisting?: 'skip' | 'update';
922
+ }
923
+ interface ImportContactsResponse {
924
+ importId: string | null;
925
+ chunkIndex: number;
926
+ /** Rows with no usable address at all — the only rows that are not stored. */
927
+ rejected: Array<{
928
+ row: number;
929
+ value: string;
930
+ reason: string;
931
+ }>;
932
+ /** A sample of the addresses stored with `status: 'invalid'`. */
933
+ invalidSamples: Array<{
934
+ email: string;
935
+ emailRaw: string | null;
936
+ reason: ContactStatusReason;
937
+ }>;
938
+ summary: {
939
+ requested: number;
940
+ added: number;
941
+ updated: number;
942
+ skipped: number;
943
+ /** How many addresses sanitization actually changed. */
944
+ sanitized: number;
945
+ /** Stored, but marked invalid — NOT discarded. */
946
+ invalid: number;
947
+ /** Stored, but already on your suppression list. */
948
+ suppressed: number;
949
+ duplicatesInFile: number;
950
+ rejected: number;
951
+ };
952
+ list: {
953
+ id: string;
954
+ contactCount: number;
955
+ fields: string[];
956
+ };
957
+ }
958
+ interface ListContactsParams {
959
+ limit?: number;
960
+ /** From the previous response's `pagination.nextCursor`. Keyset, not offset. */
961
+ cursor?: string;
962
+ sort?: 'created' | 'email';
963
+ status?: ContactStatus;
964
+ search?: string;
965
+ }
966
+ interface ListContactsResponse {
967
+ contacts: Contact[];
968
+ pagination: {
969
+ limit: number;
970
+ hasMore: boolean;
971
+ nextCursor: string | null;
972
+ /** A FLOOR when `totalCapped` is true — an exact filtered count is not run per page. */
973
+ total: number;
974
+ totalCapped: boolean;
975
+ };
976
+ }
977
+ interface AddContactParams {
978
+ email: string;
979
+ name?: string;
980
+ attributes?: Record<string, string>;
981
+ }
982
+ interface UpdateContactParams {
983
+ name?: string;
984
+ /** REPLACES the stored object wholesale. Import merges instead. */
985
+ attributes?: Record<string, string>;
986
+ status?: ContactStatus;
987
+ }
988
+ interface BulkDeleteContactsParams {
989
+ contactIds?: string[];
990
+ emails?: string[];
991
+ /** A filter rather than ids — how you clear every invalid address in one call. */
992
+ status?: ContactStatus;
993
+ all?: boolean;
994
+ }
995
+ interface BulkDeleteContactsResponse {
996
+ message: string;
997
+ deleted?: number;
998
+ listId?: string;
999
+ }
1000
+ interface ContactListFieldsResponse {
1001
+ fields: Array<{
1002
+ name: string;
1003
+ sample: string | null;
1004
+ }>;
1005
+ /** Mappable alongside `fields`, but not attributes. */
1006
+ reservedFields: string[];
1007
+ }
1008
+ interface RecountContactListResponse {
1009
+ contactCount: number;
1010
+ previous: number;
1011
+ drift: number | null;
1012
+ timedOut?: boolean;
1013
+ message?: string;
1014
+ }
1015
+ interface AcceptedResponse {
1016
+ message: string;
1017
+ listId?: string;
1018
+ }
1019
+ /**
1020
+ * Which contact column feeds each `{{variable}}`, as `{ variableName: columnName }`.
1021
+ * Omit an entry when the column is already named the same as the variable.
1022
+ * `email` and `name` are always mappable in addition to the list's own fields.
1023
+ */
1024
+ type VariableMapping = Record<string, string>;
1025
+ interface ListSendResponse {
1026
+ message: string;
1027
+ mode: 'list';
1028
+ jobId: string;
1029
+ listId: string;
1030
+ listName: string;
1031
+ /** How many contacts were sendable when the send was accepted. */
1032
+ estimatedRecipientCount: number;
1033
+ status: 'expanding';
1034
+ statusUrl: string;
1035
+ usingSesTemplate?: boolean;
1036
+ }
785
1037
 
786
1038
  declare class HttpClient {
787
1039
  private config;
@@ -801,7 +1053,18 @@ declare class EmailResource {
801
1053
  private http;
802
1054
  constructor(http: HttpClient);
803
1055
  send(params: SendEmailParams, overrides?: RequestOverrides): Promise<SendEmailResponse>;
804
- bulk(params: BulkEmailParams, overrides?: RequestOverrides): Promise<BulkEmailResponse>;
1056
+ /**
1057
+ * Send to many recipients at once.
1058
+ *
1059
+ * Three shapes, discriminated by what you pass:
1060
+ * - `emails[]` with a full `htmlBody` each — the original behaviour, unchanged.
1061
+ * - `emails[]` + a shared `htmlBody` + per-recipient `variables` — MAIL MERGE.
1062
+ * The template crosses the wire once instead of once per recipient.
1063
+ * - `listId` (+ `mapping`) — a saved contact list, with NO recipient cap. Returns
1064
+ * a `ListSendResponse` with `mode: 'list'` and a job id you can poll; the
1065
+ * server materializes the recipients in the background.
1066
+ */
1067
+ bulk(params: BulkEmailParams, overrides?: RequestOverrides): Promise<BulkEmailResponse | ListSendResponse>;
805
1068
  status(jobId: string, overrides?: RequestOverrides): Promise<JobStatusResponse>;
806
1069
  /**
807
1070
  * Look up the current status of a single message by its `messageId` — the ID
@@ -850,7 +1113,14 @@ declare class TemplatesResource {
850
1113
  }, overrides?: RequestOverrides): Promise<TemplateResponse>;
851
1114
  preview(id: string, params?: TemplatePreviewParams, overrides?: RequestOverrides): Promise<TemplatePreviewResponse>;
852
1115
  send(params: TemplateSendParams, overrides?: RequestOverrides): Promise<SendEmailResponse>;
853
- bulkSend(params: TemplateBulkSendParams, overrides?: RequestOverrides): Promise<TemplateBulkSendResponse>;
1116
+ /**
1117
+ * Send a template to many recipients.
1118
+ *
1119
+ * Pass `recipients[]` (up to 1000) for an inline send, or `listId` (+ `mapping`)
1120
+ * to send to a whole saved contact list with no recipient cap — the server
1121
+ * expands the list in the background and returns `mode: 'list'` with a job id.
1122
+ */
1123
+ bulkSend(params: TemplateBulkSendParams, overrides?: RequestOverrides): Promise<TemplateBulkSendResponse | ListSendResponse>;
854
1124
  stats(overrides?: RequestOverrides): Promise<TemplateStatsResponse>;
855
1125
  }
856
1126
 
@@ -942,6 +1212,97 @@ declare class CampaignsResource {
942
1212
  analytics(campaignId: string, params?: CampaignAnalyticsParams, overrides?: RequestOverrides): Promise<CampaignAnalytics>;
943
1213
  }
944
1214
 
1215
+ /**
1216
+ * Contact lists: reusable audiences you can send to without a recipient limit.
1217
+ *
1218
+ * The per-request cap on `email.bulk` / `templates.bulkSend` exists because those
1219
+ * carry their recipients in the request body. A list send carries only a `listId`,
1220
+ * and the server expands the list in the background — so there is no cap, and the
1221
+ * caller gets one job id to track however large the audience is.
1222
+ *
1223
+ * ── Address hygiene ──
1224
+ * Every imported address is sanitized (display-name unwrapping, `mailto:`,
1225
+ * zero-width characters, quotes, case) and then validated with the SAME rules the
1226
+ * send path applies. Addresses that fail are STORED with `status: 'invalid'` rather
1227
+ * than discarded, so you can list them, export them and fix them. Addresses on your
1228
+ * suppression list are stored with the matching status. Only `subscribed` contacts
1229
+ * are ever sent to, and bounces, complaints and unsubscribes flow back onto the
1230
+ * contact automatically.
1231
+ */
1232
+ declare class ContactListsResource {
1233
+ private http;
1234
+ constructor(http: HttpClient);
1235
+ create(params: CreateContactListParams, overrides?: RequestOverrides): Promise<{
1236
+ message: string;
1237
+ list: ContactList;
1238
+ }>;
1239
+ list(params?: ListContactListsParams, overrides?: RequestOverrides): Promise<ListContactListsResponse>;
1240
+ /** Auto-paginating iterator over every contact list, mirroring templates.listAll(). */
1241
+ listAll(params?: Omit<ListContactListsParams, 'page'>, overrides?: RequestOverrides): AsyncGenerator<ContactList>;
1242
+ /** A list plus its per-status breakdown (how many are sendable, invalid, bounced...). */
1243
+ get(listId: string, overrides?: RequestOverrides): Promise<ContactListDetail>;
1244
+ update(listId: string, params: UpdateContactListParams, overrides?: RequestOverrides): Promise<{
1245
+ message: string;
1246
+ list: ContactList;
1247
+ }>;
1248
+ /**
1249
+ * Delete a list. Returns 202: the contacts are removed by a background job in
1250
+ * chunks, so a multi-million-row list does not block on one enormous transaction.
1251
+ * Fails with a conflict while a send is still expanding this list.
1252
+ */
1253
+ delete(listId: string, overrides?: RequestOverrides): Promise<AcceptedResponse>;
1254
+ /**
1255
+ * Import a chunk of contacts (max 1000 rows per call).
1256
+ *
1257
+ * Pass the same `importId` with an incrementing `chunkIndex` for every chunk of
1258
+ * one file: that pair makes a retried chunk a no-op instead of a double-insert,
1259
+ * so a network failure mid-upload is safe to retry blindly.
1260
+ *
1261
+ * `onExisting: 'update'` merges attributes into contacts already in the list. It
1262
+ * never re-subscribes someone who bounced, complained or opted out.
1263
+ */
1264
+ importContacts(listId: string, params: ImportContactsParams, overrides?: RequestOverrides): Promise<ImportContactsResponse>;
1265
+ /**
1266
+ * List contacts. KEYSET paginated: pass the previous response's
1267
+ * `pagination.nextCursor` as `cursor` to advance. There is no page number,
1268
+ * because offset paging degrades badly past a few hundred pages.
1269
+ */
1270
+ contacts(listId: string, params?: ListContactsParams, overrides?: RequestOverrides): Promise<ListContactsResponse>;
1271
+ /** Auto-paginating iterator over every contact in a list. */
1272
+ contactsAll(listId: string, params?: Omit<ListContactsParams, 'cursor'>, overrides?: RequestOverrides): AsyncGenerator<Contact>;
1273
+ addContact(listId: string, params: AddContactParams, overrides?: RequestOverrides): Promise<{
1274
+ message: string;
1275
+ contact: Contact;
1276
+ }>;
1277
+ /** `attributes` REPLACES the stored object wholesale here; import merges instead. */
1278
+ updateContact(listId: string, contactId: string, params: UpdateContactParams, overrides?: RequestOverrides): Promise<{
1279
+ message: string;
1280
+ contact: Contact;
1281
+ }>;
1282
+ deleteContact(listId: string, contactId: string, overrides?: RequestOverrides): Promise<void>;
1283
+ /**
1284
+ * Delete many contacts. Pass `contactIds` or `emails` (up to 1000), or a FILTER
1285
+ * — `{ status: 'invalid' }` or `{ all: true }` — which is how you clear tens of
1286
+ * thousands of dead addresses in one small request. Filtered deletes return 202
1287
+ * and run in the background.
1288
+ */
1289
+ bulkDeleteContacts(listId: string, params: BulkDeleteContactsParams, overrides?: RequestOverrides): Promise<BulkDeleteContactsResponse>;
1290
+ /**
1291
+ * The column names available for send-time variable mapping, with a sample value
1292
+ * for each. This is what tells you which `mapping` keys a list will accept.
1293
+ */
1294
+ fields(listId: string, options?: {
1295
+ refresh?: boolean;
1296
+ }, overrides?: RequestOverrides): Promise<ContactListFieldsResponse>;
1297
+ /**
1298
+ * Re-run validation and re-sync suppression across the whole list. Useful after a
1299
+ * bulk suppression import, or to refresh a list that has been sitting. Returns 202.
1300
+ */
1301
+ revalidate(listId: string, overrides?: RequestOverrides): Promise<AcceptedResponse>;
1302
+ /** Reconcile the denormalized contact count with an exact COUNT. */
1303
+ recount(listId: string, overrides?: RequestOverrides): Promise<RecountContactListResponse>;
1304
+ }
1305
+
945
1306
  declare class Outbound {
946
1307
  readonly email: EmailResource;
947
1308
  readonly templates: TemplatesResource;
@@ -950,6 +1311,7 @@ declare class Outbound {
950
1311
  readonly dashboard: DashboardResource;
951
1312
  readonly quota: QuotaResource;
952
1313
  readonly campaigns: CampaignsResource;
1314
+ readonly contactLists: ContactListsResource;
953
1315
  constructor(config?: OutboundConfig);
954
1316
  /**
955
1317
  * Verify a webhook signature using HMAC-SHA256.
@@ -993,4 +1355,4 @@ declare class NetworkError extends OutboundError {
993
1355
  constructor(message?: string);
994
1356
  }
995
1357
 
996
- export { type AddSuppressionParams, type AnyIncomingWebhookPayload, type Attachment, AuthenticationError, BadRequestError, type BulkAddSuppressionsParams, type BulkAddSuppressionsResponse, type BulkEmailParams, type BulkEmailRecipient, type BulkEmailResponse, type BulkRemoveSuppressionsParams, type BulkRemoveSuppressionsResponse, type BulkResponseRecipient, type CampaignAnalytics, type CampaignAnalyticsParams, type CampaignBounceReason, type CampaignDomainStat, type CampaignRates, type CampaignStatus, type CampaignSummary, type CampaignTimeseriesPoint, type CampaignTotals, type CancelEmailParams, type CancelEmailResponse, ConflictError, type CreateTemplateParams, type CreateWebhookParams, type CreateWebhookResponse, type DashboardResponse, type DedicatedIpWarmup, type EmailJob, type EmailList, type EmailRecipientStatus, type EmailStatus, ForbiddenError, type GlobalQuotaResponse, type IncomingWebhookEvent, type IncomingWebhookEventV2, type IncomingWebhookPayload, type IncomingWebhookPayloadV2, type IpPoolStatus, type JobStatusResponse, type ListCampaignsParams, type ListCampaignsResponse, type ListSuppressionsParams, type ListSuppressionsResponse, type ListTemplatesParams, type ListTemplatesResponse, type ListWebhooksResponse, type MessageContentResponse, type MessageStatusRecipient, type MessageStatusResponse, NetworkError, NotFoundError, Outbound, type OutboundConfig, OutboundError, type QuotaResponse, RateLimitError, type RejectedEmail, type RequestOverrides, type ResolvedConfig, type SearchSuppressionsParams, type SearchSuppressionsResponse, type SendEmailParams, type SendEmailResponse, ServerError, type Suppression, type SuppressionReason, type SuppressionResponse, type Template, type TemplateBulkRecipient, type TemplateBulkSendParams, type TemplateBulkSendResponse, type TemplatePreviewParams, type TemplatePreviewResponse, type TemplateResponse, type TemplateSendParams, type TemplateStatsResponse, TimeoutError, type UpdateTemplateParams, type UpdateWebhookParams, type UpdateWebhookResponse, type Webhook, type WebhookEvent, type WebhookEventDetails, type WebhookEventStatus, type WebhookRetryStrategy };
1358
+ export { type AcceptedResponse, type AddContactParams, type AddSuppressionParams, type AnyIncomingWebhookPayload, type Attachment, AuthenticationError, BadRequestError, type BulkAddSuppressionsParams, type BulkAddSuppressionsResponse, type BulkDeleteContactsParams, type BulkDeleteContactsResponse, type BulkEmailParams, type BulkEmailRecipient, type BulkEmailResponse, type BulkRemoveSuppressionsParams, type BulkRemoveSuppressionsResponse, type BulkResponseRecipient, type CampaignAnalytics, type CampaignAnalyticsParams, type CampaignBounceReason, type CampaignDomainStat, type CampaignRates, type CampaignStatus, type CampaignSummary, type CampaignTimeseriesPoint, type CampaignTotals, type CancelEmailParams, type CancelEmailResponse, type CheckQuotaResponse, ConflictError, type Contact, type ContactList, type ContactListDetail, type ContactListFieldsResponse, type ContactStatus, type ContactStatusReason, type CreateContactListParams, type CreateTemplateParams, type CreateWebhookParams, type CreateWebhookResponse, type DashboardResponse, type DedicatedIpWarmup, type EmailJob, type EmailList, type EmailRecipientStatus, type EmailStatus, ForbiddenError, type GlobalQuotaResponse, type ImportContactRow, type ImportContactsParams, type ImportContactsResponse, type IncomingWebhookEvent, type IncomingWebhookEventV2, type IncomingWebhookPayload, type IncomingWebhookPayloadV2, type IpPoolStatus, type JobStatusResponse, type ListCampaignsParams, type ListCampaignsResponse, type ListContactListsParams, type ListContactListsResponse, type ListContactsParams, type ListContactsResponse, type ListSendResponse, type ListSuppressionsParams, type ListSuppressionsResponse, type ListTemplatesParams, type ListTemplatesResponse, type ListWebhooksResponse, type MessageContentResponse, type MessageStatusRecipient, type MessageStatusResponse, NetworkError, NotFoundError, Outbound, type OutboundConfig, OutboundError, type QuotaResponse, RateLimitError, type RecountContactListResponse, type RejectedEmail, type RequestOverrides, type ResolvedConfig, type SearchSuppressionsParams, type SearchSuppressionsResponse, type SendEmailParams, type SendEmailResponse, ServerError, type Suppression, type SuppressionReason, type SuppressionResponse, type Template, type TemplateBulkRecipient, type TemplateBulkSendParams, type TemplateBulkSendResponse, type TemplatePreviewParams, type TemplatePreviewResponse, type TemplateResponse, type TemplateSendParams, type TemplateStatsResponse, TimeoutError, type UpdateContactListParams, type UpdateContactParams, type UpdateTemplateParams, type UpdateWebhookParams, type UpdateWebhookResponse, type VariableMapping, type Webhook, type WebhookEvent, type WebhookEventDetails, type WebhookEventStatus, type WebhookRetryStrategy };
package/dist/index.js CHANGED
@@ -247,6 +247,17 @@ var EmailResource = class {
247
247
  async send(params, overrides) {
248
248
  return this.http.post("/v1/email/send", params, overrides?.apiKey);
249
249
  }
250
+ /**
251
+ * Send to many recipients at once.
252
+ *
253
+ * Three shapes, discriminated by what you pass:
254
+ * - `emails[]` with a full `htmlBody` each — the original behaviour, unchanged.
255
+ * - `emails[]` + a shared `htmlBody` + per-recipient `variables` — MAIL MERGE.
256
+ * The template crosses the wire once instead of once per recipient.
257
+ * - `listId` (+ `mapping`) — a saved contact list, with NO recipient cap. Returns
258
+ * a `ListSendResponse` with `mode: 'list'` and a job id you can poll; the
259
+ * server materializes the recipients in the background.
260
+ */
250
261
  async bulk(params, overrides) {
251
262
  return this.http.post("/v1/email/bulk", params, overrides?.apiKey);
252
263
  }
@@ -333,6 +344,13 @@ var TemplatesResource = class {
333
344
  async send(params, overrides) {
334
345
  return this.http.post("/v1/email-templates/send", params, overrides?.apiKey);
335
346
  }
347
+ /**
348
+ * Send a template to many recipients.
349
+ *
350
+ * Pass `recipients[]` (up to 1000) for an inline send, or `listId` (+ `mapping`)
351
+ * to send to a whole saved contact list with no recipient cap — the server
352
+ * expands the list in the background and returns `mode: 'list'` with a job id.
353
+ */
336
354
  async bulkSend(params, overrides) {
337
355
  return this.http.post("/v1/email-templates/bulk", params, overrides?.apiKey);
338
356
  }
@@ -489,6 +507,136 @@ var CampaignsResource = class {
489
507
  }
490
508
  };
491
509
 
510
+ // src/resources/contactLists.ts
511
+ var ContactListsResource = class {
512
+ constructor(http) {
513
+ this.http = http;
514
+ }
515
+ async create(params, overrides) {
516
+ return this.http.post("/v1/contact-lists", params, overrides?.apiKey);
517
+ }
518
+ async list(params, overrides) {
519
+ return this.http.get("/v1/contact-lists", params, overrides?.apiKey);
520
+ }
521
+ /** Auto-paginating iterator over every contact list, mirroring templates.listAll(). */
522
+ async *listAll(params, overrides) {
523
+ const limit = params?.limit ?? 100;
524
+ let page = 1;
525
+ for (; ; ) {
526
+ const result = await this.list({ ...params, page, limit }, overrides);
527
+ for (const item of result.lists) yield item;
528
+ if (result.lists.length < limit) return;
529
+ page += 1;
530
+ }
531
+ }
532
+ /** A list plus its per-status breakdown (how many are sendable, invalid, bounced...). */
533
+ async get(listId, overrides) {
534
+ return this.http.get(`/v1/contact-lists/${encodeURIComponent(listId)}`, void 0, overrides?.apiKey);
535
+ }
536
+ async update(listId, params, overrides) {
537
+ return this.http.patch(`/v1/contact-lists/${encodeURIComponent(listId)}`, params, overrides?.apiKey);
538
+ }
539
+ /**
540
+ * Delete a list. Returns 202: the contacts are removed by a background job in
541
+ * chunks, so a multi-million-row list does not block on one enormous transaction.
542
+ * Fails with a conflict while a send is still expanding this list.
543
+ */
544
+ async delete(listId, overrides) {
545
+ return this.http.delete(`/v1/contact-lists/${encodeURIComponent(listId)}`, overrides?.apiKey);
546
+ }
547
+ /**
548
+ * Import a chunk of contacts (max 1000 rows per call).
549
+ *
550
+ * Pass the same `importId` with an incrementing `chunkIndex` for every chunk of
551
+ * one file: that pair makes a retried chunk a no-op instead of a double-insert,
552
+ * so a network failure mid-upload is safe to retry blindly.
553
+ *
554
+ * `onExisting: 'update'` merges attributes into contacts already in the list. It
555
+ * never re-subscribes someone who bounced, complained or opted out.
556
+ */
557
+ async importContacts(listId, params, overrides) {
558
+ return this.http.post(
559
+ `/v1/contact-lists/${encodeURIComponent(listId)}/contacts/import`,
560
+ params,
561
+ overrides?.apiKey
562
+ );
563
+ }
564
+ /**
565
+ * List contacts. KEYSET paginated: pass the previous response's
566
+ * `pagination.nextCursor` as `cursor` to advance. There is no page number,
567
+ * because offset paging degrades badly past a few hundred pages.
568
+ */
569
+ async contacts(listId, params, overrides) {
570
+ return this.http.get(
571
+ `/v1/contact-lists/${encodeURIComponent(listId)}/contacts`,
572
+ params,
573
+ overrides?.apiKey
574
+ );
575
+ }
576
+ /** Auto-paginating iterator over every contact in a list. */
577
+ async *contactsAll(listId, params, overrides) {
578
+ let cursor;
579
+ for (; ; ) {
580
+ const result = await this.contacts(listId, { ...params, cursor }, overrides);
581
+ for (const contact of result.contacts) yield contact;
582
+ if (!result.pagination.hasMore || !result.pagination.nextCursor) return;
583
+ cursor = result.pagination.nextCursor;
584
+ }
585
+ }
586
+ async addContact(listId, params, overrides) {
587
+ return this.http.post(`/v1/contact-lists/${encodeURIComponent(listId)}/contacts`, params, overrides?.apiKey);
588
+ }
589
+ /** `attributes` REPLACES the stored object wholesale here; import merges instead. */
590
+ async updateContact(listId, contactId, params, overrides) {
591
+ return this.http.patch(
592
+ `/v1/contact-lists/${encodeURIComponent(listId)}/contacts/${encodeURIComponent(contactId)}`,
593
+ params,
594
+ overrides?.apiKey
595
+ );
596
+ }
597
+ async deleteContact(listId, contactId, overrides) {
598
+ await this.http.delete(
599
+ `/v1/contact-lists/${encodeURIComponent(listId)}/contacts/${encodeURIComponent(contactId)}`,
600
+ overrides?.apiKey
601
+ );
602
+ }
603
+ /**
604
+ * Delete many contacts. Pass `contactIds` or `emails` (up to 1000), or a FILTER
605
+ * — `{ status: 'invalid' }` or `{ all: true }` — which is how you clear tens of
606
+ * thousands of dead addresses in one small request. Filtered deletes return 202
607
+ * and run in the background.
608
+ */
609
+ async bulkDeleteContacts(listId, params, overrides) {
610
+ return this.http.post(
611
+ `/v1/contact-lists/${encodeURIComponent(listId)}/contacts/bulk-delete`,
612
+ params,
613
+ overrides?.apiKey
614
+ );
615
+ }
616
+ /**
617
+ * The column names available for send-time variable mapping, with a sample value
618
+ * for each. This is what tells you which `mapping` keys a list will accept.
619
+ */
620
+ async fields(listId, options, overrides) {
621
+ return this.http.get(
622
+ `/v1/contact-lists/${encodeURIComponent(listId)}/fields`,
623
+ options?.refresh ? { refresh: "true" } : void 0,
624
+ overrides?.apiKey
625
+ );
626
+ }
627
+ /**
628
+ * Re-run validation and re-sync suppression across the whole list. Useful after a
629
+ * bulk suppression import, or to refresh a list that has been sitting. Returns 202.
630
+ */
631
+ async revalidate(listId, overrides) {
632
+ return this.http.post(`/v1/contact-lists/${encodeURIComponent(listId)}/revalidate`, {}, overrides?.apiKey);
633
+ }
634
+ /** Reconcile the denormalized contact count with an exact COUNT. */
635
+ async recount(listId, overrides) {
636
+ return this.http.post(`/v1/contact-lists/${encodeURIComponent(listId)}/recount`, {}, overrides?.apiKey);
637
+ }
638
+ };
639
+
492
640
  // src/client.ts
493
641
  var BASE_URL = "https://outbound-api.unionstack.in";
494
642
  var Outbound = class {
@@ -499,6 +647,7 @@ var Outbound = class {
499
647
  dashboard;
500
648
  quota;
501
649
  campaigns;
650
+ contactLists;
502
651
  constructor(config) {
503
652
  const resolved = {
504
653
  apiKey: config?.apiKey,
@@ -515,6 +664,7 @@ var Outbound = class {
515
664
  this.dashboard = new DashboardResource(http);
516
665
  this.quota = new QuotaResource(http);
517
666
  this.campaigns = new CampaignsResource(http);
667
+ this.contactLists = new ContactListsResource(http);
518
668
  }
519
669
  /**
520
670
  * Verify a webhook signature using HMAC-SHA256.