feat: add contactBooks to sdk, add delete campaign public endpoint (#352)

* feat: add contactBooks to sdk, add delete campaign public endpoint

* fix: pr review notes

* refactor: pr feedback

* feat: bulk delete/create contacts

* refactor: rename a few methods for consistency

* refactor: update openapi docs based on pr feedback

* refactor: update open api docs, based on pr feedback

* fix: delete campaign security issue

* refactor: delete campaign requires team id (from context)

* fix: enums
This commit is contained in:
Dave Stockley
2026-03-03 20:10:43 +00:00
committed by GitHub
parent 73416dc481
commit 7428a1fbfa
22 changed files with 1365 additions and 218 deletions
+1
View File
@@ -1,6 +1,7 @@
export { UseSend } from "./src/usesend";
export { UseSend as Unsend } from "./src/usesend"; // deprecated alias
export { Campaigns } from "./src/campaign";
export { ContactBooks } from "./src/contactBook";
export {
Webhooks,
WebhookVerificationError,
+43
View File
@@ -13,6 +13,19 @@ type CreateCampaignResponse = {
type CreateCampaignResponseSuccess =
paths["/v1/campaigns"]["post"]["responses"]["200"]["content"]["application/json"];
type GetAllCampaignsQuery = {
page?: string;
status?: NonNullable<paths["/v1/campaigns"]["get"]["parameters"]["query"]>["status"];
search?: string;
};
type GetAllCampaignsResponseSuccess = paths["/v1/campaigns"]["get"]["responses"]["200"]["content"]["application/json"];
type GetAllCampaignsResponse = {
data: GetAllCampaignsResponseSuccess | null;
error: ErrorResponse | null;
};
type GetCampaignResponseSuccess =
paths["/v1/campaigns/{campaignId}"]["get"]["responses"]["200"]["content"]["application/json"];
@@ -32,6 +45,14 @@ type ScheduleCampaignResponse = {
error: ErrorResponse | null;
};
type DeleteCampaignResponseSuccess =
paths["/v1/campaigns/{campaignId}"]["delete"]["responses"]["200"]["content"]["application/json"];
type DeleteCampaignResponse = {
data: DeleteCampaignResponseSuccess | null;
error: ErrorResponse | null;
};
type CampaignActionResponseSuccess = { success: boolean };
type CampaignActionResponse = {
@@ -55,6 +76,21 @@ export class Campaigns {
return data;
}
async getAll(
query?: GetAllCampaignsQuery,
): Promise<GetAllCampaignsResponse> {
const params = new URLSearchParams();
if (query?.page) params.set("page", query.page);
if (query?.status) params.set("status", query.status);
if (query?.search) params.set("search", query.search);
const queryString = params.toString();
const path = queryString ? `/campaigns?${queryString}` : `/campaigns`;
const data = await this.usesend.get<GetAllCampaignsResponseSuccess>(path);
return data;
}
async get(campaignId: string): Promise<GetCampaignResponse> {
const data = await this.usesend.get<GetCampaignResponseSuccess>(
`/campaigns/${campaignId}`,
@@ -91,4 +127,11 @@ export class Campaigns {
return data;
}
async delete(campaignId: string): Promise<DeleteCampaignResponse> {
const data = await this.usesend.delete<DeleteCampaignResponseSuccess>(
`/campaigns/${campaignId}`,
);
return data;
}
}
+46
View File
@@ -43,6 +43,28 @@ type UpsertContactResponse = {
error: ErrorResponse | null;
};
type BulkCreateContactsPayload =
paths["/v1/contactBooks/{contactBookId}/contacts/bulk"]["post"]["requestBody"]["content"]["application/json"];
type BulkCreateContactsResponseSuccess =
paths["/v1/contactBooks/{contactBookId}/contacts/bulk"]["post"]["responses"]["200"]["content"]["application/json"];
type BulkCreateContactsResponse = {
data: BulkCreateContactsResponseSuccess | null;
error: ErrorResponse | null;
};
type BulkDeleteContactsPayload =
paths["/v1/contactBooks/{contactBookId}/contacts/bulk"]["delete"]["requestBody"]["content"]["application/json"];
type BulkDeleteContactsResponseSuccess =
paths["/v1/contactBooks/{contactBookId}/contacts/bulk"]["delete"]["responses"]["200"]["content"]["application/json"];
type BulkDeleteContactsResponse = {
data: BulkDeleteContactsResponseSuccess | null;
error: ErrorResponse | null;
};
type DeleteContactResponse = {
data: { success: boolean } | null;
error: ErrorResponse | null;
@@ -101,6 +123,30 @@ export class Contacts {
return data;
}
async bulkCreate(
contactBookId: string,
payload: BulkCreateContactsPayload
): Promise<BulkCreateContactsResponse> {
const data = await this.usesend.post<BulkCreateContactsResponseSuccess>(
`/contactBooks/${contactBookId}/contacts/bulk`,
payload
);
return data;
}
async bulkDelete(
contactBookId: string,
payload: BulkDeleteContactsPayload
): Promise<BulkDeleteContactsResponse> {
const data = await this.usesend.delete<BulkDeleteContactsResponseSuccess>(
`/contactBooks/${contactBookId}/contacts/bulk`,
payload
);
return data;
}
async delete(
contactBookId: string,
contactId: string
+97
View File
@@ -0,0 +1,97 @@
import { UseSend } from "./usesend";
import { paths } from "../types/schema";
import { ErrorResponse } from "../types";
type GetAllContactBooksResponseSuccess =
paths["/v1/contactBooks"]["get"]["responses"]["200"]["content"]["application/json"];
type GetAllContactBooksResponse = {
data: GetAllContactBooksResponseSuccess | null;
error: ErrorResponse | null;
};
type CreateContactBookPayload =
paths["/v1/contactBooks"]["post"]["requestBody"]["content"]["application/json"];
type CreateContactBookResponseSuccess =
paths["/v1/contactBooks"]["post"]["responses"]["200"]["content"]["application/json"];
type CreateContactBookResponse = {
data: CreateContactBookResponseSuccess | null;
error: ErrorResponse | null;
};
type GetContactBookResponseSuccess =
paths["/v1/contactBooks/{contactBookId}"]["get"]["responses"]["200"]["content"]["application/json"];
type GetContactBookResponse = {
data: GetContactBookResponseSuccess | null;
error: ErrorResponse | null;
};
type UpdateContactBookPayload =
paths["/v1/contactBooks/{contactBookId}"]["patch"]["requestBody"]["content"]["application/json"];
type UpdateContactBookResponseSuccess =
paths["/v1/contactBooks/{contactBookId}"]["patch"]["responses"]["200"]["content"]["application/json"];
type UpdateContactBookResponse = {
data: UpdateContactBookResponseSuccess | null;
error: ErrorResponse | null;
};
type DeleteContactBookResponseSuccess =
paths["/v1/contactBooks/{contactBookId}"]["delete"]["responses"]["200"]["content"]["application/json"];
type DeleteContactBookResponse = {
data: DeleteContactBookResponseSuccess | null;
error: ErrorResponse | null;
};
export class ContactBooks {
constructor(private readonly usesend: UseSend) {
this.usesend = usesend;
}
async list(): Promise<GetAllContactBooksResponse> {
const data = await this.usesend.get<GetAllContactBooksResponseSuccess>(
`/contactBooks`,
);
return data;
}
async get(contactBookId: string): Promise<GetContactBookResponse> {
const data = await this.usesend.get<GetContactBookResponseSuccess>(
`/contactBooks/${contactBookId}`,
);
return data;
}
async create(
payload: CreateContactBookPayload,
): Promise<CreateContactBookResponse> {
const data = await this.usesend.post<CreateContactBookResponseSuccess>(
`/contactBooks`,
payload,
);
return data;
}
async update(
contactBookId: string,
payload: UpdateContactBookPayload,
): Promise<UpdateContactBookResponse> {
const data = await this.usesend.patch<UpdateContactBookResponseSuccess>(
`/contactBooks/${contactBookId}`,
payload,
);
return data;
}
async delete(contactBookId: string): Promise<DeleteContactBookResponse> {
const data = await this.usesend.delete<DeleteContactBookResponseSuccess>(
`/contactBooks/${contactBookId}`,
);
return data;
}
}
+2
View File
@@ -1,5 +1,6 @@
import { ErrorResponse } from "../types";
import { Contacts } from "./contact";
import { ContactBooks } from "./contactBook";
import { Emails } from "./email";
import { Domains } from "./domain";
import { Campaigns } from "./campaign";
@@ -23,6 +24,7 @@ export class UseSend {
readonly emails = new Emails(this);
readonly domains = new Domains(this);
readonly contacts = new Contacts(this);
readonly contactBooks = new ContactBooks(this);
readonly campaigns = new Campaigns(this);
url = baseUrl;
+485 -5
View File
@@ -561,7 +561,9 @@ export interface paths {
post: {
parameters: {
query?: never;
header?: never;
header?: {
"Idempotency-Key"?: string;
};
path?: never;
cookie?: never;
};
@@ -628,7 +630,9 @@ export interface paths {
post: {
parameters: {
query?: never;
header?: never;
header?: {
"Idempotency-Key"?: string;
};
path?: never;
cookie?: never;
};
@@ -724,6 +728,300 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/contactBooks": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Retrieve contact books accessible by the API key */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/**
* @description The ID of the contact book
* @example clx1234567890
*/
id: string;
/**
* @description The name of the contact book
* @example Newsletter Subscribers
*/
name: string;
/**
* @description The ID of the team
* @example 1
*/
teamId: number;
/**
* @description Custom properties for the contact book
* @example {
* "customField1": "value1"
* }
*/
properties: {
[key: string]: string;
};
/**
* @description The emoji associated with the contact book
* @example 📙
*/
emoji: string;
/** @description The creation timestamp */
createdAt: string;
/** @description The last update timestamp */
updatedAt: string;
_count?: {
/** @description The number of contacts in the contact book */
contacts?: number;
};
}[];
};
};
};
};
put?: never;
post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": {
name: string;
emoji?: string;
properties?: {
[key: string]: string;
};
};
};
};
responses: {
/** @description Create a new contact book */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
id: string;
name: string;
teamId: number;
properties: {
[key: string]: string;
};
emoji: string;
createdAt: string;
updatedAt: string;
};
};
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/contactBooks/{contactBookId}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: {
parameters: {
query?: never;
header?: never;
path: {
contactBookId: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Retrieve the contact book */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
id: string;
name: string;
teamId: number;
properties: {
[key: string]: string;
};
emoji: string;
createdAt: string;
updatedAt: string;
_count?: {
contacts?: number;
};
};
};
};
/** @description Forbidden - API key doesn't have access */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
};
};
/** @description Contact book not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
};
};
};
};
put?: never;
post?: never;
delete: {
parameters: {
query?: never;
header?: never;
path: {
contactBookId: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Contact book deleted successfully */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
id: string;
success: boolean;
message: string;
};
};
};
/** @description Forbidden - API key doesn't have access */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
};
};
/** @description Contact book not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
};
};
};
};
options?: never;
head?: never;
patch: {
parameters: {
query?: never;
header?: never;
path: {
contactBookId: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": {
name?: string;
emoji?: string;
properties?: {
[key: string]: string;
};
};
};
};
responses: {
/** @description Update the contact book */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
id: string;
name: string;
teamId: number;
properties: {
[key: string]: string;
};
emoji: string;
createdAt: string;
updatedAt: string;
};
};
};
/** @description Forbidden - API key doesn't have access */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
};
};
/** @description Contact book not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
};
};
};
};
trace?: never;
};
"/v1/contactBooks/{contactBookId}/contacts": {
parameters: {
query?: never;
@@ -814,6 +1112,88 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/contactBooks/{contactBookId}/contacts/bulk": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: {
parameters: {
query?: never;
header?: never;
path: {
contactBookId: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": {
email: string;
firstName?: string;
lastName?: string;
properties?: {
[key: string]: string;
};
subscribed?: boolean;
}[];
};
};
responses: {
/** @description Bulk add contacts to a contact book */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
message: string;
count: number;
};
};
};
};
};
delete: {
parameters: {
query?: never;
header?: never;
path: {
contactBookId: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": {
contactIds: string[];
};
};
};
responses: {
/** @description Bulk delete contacts from a contact book */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
success: boolean;
count: number;
};
};
};
};
};
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/contactBooks/{contactBookId}/contacts/{contactId}": {
parameters: {
query?: never;
@@ -863,6 +1243,7 @@ export interface paths {
header?: never;
path: {
contactBookId: string;
contactId: string;
};
cookie?: never;
};
@@ -966,7 +1347,53 @@ export interface paths {
path?: never;
cookie?: never;
};
get?: never;
get: {
parameters: {
query?: {
/** @description Page number for pagination (default: 1) */
page?: string;
/** @description Filter campaigns by status */
status?: "DRAFT" | "SCHEDULED" | "RUNNING" | "PAUSED" | "SENT";
/** @description Search campaigns by name or subject */
search?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Get list of campaigns */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
campaigns: {
id: string;
name: string;
from: string;
subject: string;
/** Format: date-time */
createdAt: string;
/** Format: date-time */
updatedAt: string;
/** @enum {string} */
status: "DRAFT" | "SCHEDULED" | "RUNNING" | "PAUSED" | "SENT";
/** Format: date-time */
scheduledAt: string | null;
total: number;
sent: number;
delivered: number;
unsubscribed: number;
}[];
totalPage: number;
};
};
};
};
};
put?: never;
post: {
parameters: {
@@ -1047,7 +1474,9 @@ export interface paths {
parameters: {
query?: never;
header?: never;
path?: never;
path: {
campaignId: string;
};
cookie?: never;
};
get: {
@@ -1104,7 +1533,58 @@ export interface paths {
};
put?: never;
post?: never;
delete?: never;
delete: {
parameters: {
query?: never;
header?: never;
path: {
campaignId: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Delete campaign */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
id: string;
name: string;
from: string;
subject: string;
previewText: string | null;
contactBookId: string | null;
html: string | null;
content: string | null;
status: string;
/** Format: date-time */
scheduledAt: string | null;
batchSize: number;
batchWindowMinutes: number;
total: number;
sent: number;
delivered: number;
opened: number;
clicked: number;
unsubscribed: number;
bounced: number;
hardBounced: number;
complained: number;
replyTo: string[];
cc: string[];
bcc: string[];
/** Format: date-time */
createdAt: string;
/** Format: date-time */
updatedAt: string;
};
};
};
};
};
options?: never;
head?: never;
patch?: never;