add campaign api (#274)

This commit is contained in:
KM Koushik
2025-10-18 10:31:43 +11:00
committed by GitHub
parent e631f16c85
commit a5ca3b2f87
31 changed files with 2093 additions and 187 deletions
+31 -1
View File
@@ -37,6 +37,29 @@ resp, _ = client.emails.send(payload={
"html": "<strong>Hi!</strong>",
})
# 3) Campaigns
campaign_payload: types.CampaignCreate = {
"name": "Welcome Series",
"subject": "Welcome to our service!",
"html": "<p>Thanks for joining us!</p>",
"from": "welcome@example.com",
"contactBookId": "cb_1234567890",
}
campaign_resp, _ = client.campaigns.create(payload=campaign_payload)
# Schedule a campaign
schedule_payload: types.CampaignSchedule = {
"scheduledAt": "2024-12-01T10:00:00Z",
}
schedule_resp, _ = client.campaigns.schedule(
campaign_id=campaign_resp["id"],
payload=schedule_payload
)
# Pause/resume campaigns
client.campaigns.pause(campaign_id="campaign_123")
client.campaigns.resume(campaign_id="campaign_123")
# Toggle behavior if desired:
# - raise_on_error=False: return (None, error_dict) instead of raising
# No model parsing occurs; methods return plain dicts following the typed shapes.
@@ -55,7 +78,14 @@ This package is managed with Poetry. Models are maintained in-repo under
It is published as `usesend` on PyPI.
## Available Resources
- **Emails**: `client.emails.send()`, `client.emails.get()`
- **Contacts**: `client.contacts.create()`, `client.contacts.get()`, `client.contacts.list()`
- **Domains**: `client.domains.create()`, `client.domains.get()`, `client.domains.verify()`
- **Campaigns**: `client.campaigns.create()`, `client.campaigns.get()`, `client.campaigns.schedule()`, `client.campaigns.pause()`, `client.campaigns.resume()`
Notes
- Human-friendly models are available under `usesend.types` (e.g., `EmailCreate`, `Contact`, `APIError`).
- Human-friendly models are available under `usesend.types` (e.g., `EmailCreate`, `CampaignCreate`, `Contact`, `APIError`).
- Endpoint methods accept TypedDict payloads or plain dicts via the `payload=` keyword.
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "usesend"
version = "0.2.6"
version = "0.2.7"
description = "Python SDK for the UseSend API"
authors = ["UseSend"]
license = "MIT"
+2 -1
View File
@@ -2,6 +2,7 @@
from .usesend import UseSend, UseSendHTTPError
from .domains import Domains # type: ignore
from .campaigns import Campaigns # type: ignore
from . import types
__all__ = ["UseSend", "UseSendHTTPError", "types", "Domains"]
__all__ = ["UseSend", "UseSendHTTPError", "types", "Domains", "Campaigns"]
+68
View File
@@ -0,0 +1,68 @@
"""Campaign resource client using TypedDict shapes (no Pydantic)."""
from __future__ import annotations
from typing import Any, Dict, Optional, Tuple
from .types import (
APIError,
Campaign,
CampaignCreate,
CampaignCreateResponse,
CampaignSchedule,
CampaignScheduleResponse,
CampaignActionResponse,
)
class Campaigns:
"""Client for `/campaigns` endpoints."""
def __init__(self, usesend: "UseSend") -> None:
self.usesend = usesend
def create(
self, payload: CampaignCreate
) -> Tuple[Optional[CampaignCreateResponse], Optional[APIError]]:
data, err = self.usesend.post(
"/campaigns",
payload,
)
return (data, err) # type: ignore[return-value]
def get(
self, campaign_id: str
) -> Tuple[Optional[Campaign], Optional[APIError]]:
data, err = self.usesend.get(
f"/campaigns/{campaign_id}"
)
return (data, err) # type: ignore[return-value]
def schedule(
self, campaign_id: str, payload: CampaignSchedule
) -> Tuple[Optional[CampaignScheduleResponse], Optional[APIError]]:
data, err = self.usesend.post(
f"/campaigns/{campaign_id}/schedule",
payload,
)
return (data, err) # type: ignore[return-value]
def pause(
self, campaign_id: str
) -> Tuple[Optional[CampaignActionResponse], Optional[APIError]]:
data, err = self.usesend.post(
f"/campaigns/{campaign_id}/pause",
{},
)
return (data, err) # type: ignore[return-value]
def resume(
self, campaign_id: str
) -> Tuple[Optional[CampaignActionResponse], Optional[APIError]]:
data, err = self.usesend.post(
f"/campaigns/{campaign_id}/resume",
{},
)
return (data, err) # type: ignore[return-value]
from .usesend import UseSend # noqa: E402 pylint: disable=wrong-import-position
+202 -96
View File
@@ -15,14 +15,14 @@ from typing_extensions import NotRequired, Required, Literal
# ---------------------------------------------------------------------------
DomainStatus = Literal[
'NOT_STARTED',
'PENDING',
'SUCCESS',
'FAILED',
'TEMPORARY_FAILURE',
"NOT_STARTED",
"PENDING",
"SUCCESS",
"FAILED",
"TEMPORARY_FAILURE",
]
DNSRecordType = Literal['MX', 'TXT']
DNSRecordType = Literal["MX", "TXT"]
class DNSRecord(TypedDict, total=False):
@@ -99,24 +99,25 @@ class DomainDeleteResponse(TypedDict):
success: bool
message: str
# ---------------------------------------------------------------------------
# Emails
# ---------------------------------------------------------------------------
EmailEventStatus = Literal[
'SCHEDULED',
'QUEUED',
'SENT',
'DELIVERY_DELAYED',
'BOUNCED',
'REJECTED',
'RENDERING_FAILURE',
'DELIVERED',
'OPENED',
'CLICKED',
'COMPLAINED',
'FAILED',
'CANCELLED',
"SCHEDULED",
"QUEUED",
"SENT",
"DELIVERY_DELAYED",
"BOUNCED",
"REJECTED",
"RENDERING_FAILURE",
"DELIVERED",
"OPENED",
"CLICKED",
"COMPLAINED",
"FAILED",
"CANCELLED",
]
@@ -128,22 +129,22 @@ class EmailEvent(TypedDict, total=False):
Email = TypedDict(
'Email',
"Email",
{
'id': str,
'teamId': float,
'to': Union[str, List[str]],
'replyTo': NotRequired[Union[str, List[str]]],
'cc': NotRequired[Union[str, List[str]]],
'bcc': NotRequired[Union[str, List[str]]],
'from': str,
'subject': str,
'html': str,
'text': str,
'createdAt': str,
'updatedAt': str,
'emailEvents': List[EmailEvent],
}
"id": str,
"teamId": float,
"to": Union[str, List[str]],
"replyTo": NotRequired[Union[str, List[str]]],
"cc": NotRequired[Union[str, List[str]]],
"bcc": NotRequired[Union[str, List[str]]],
"from": str,
"subject": str,
"html": str,
"text": str,
"createdAt": str,
"updatedAt": str,
"emailEvents": List[EmailEvent],
},
)
@@ -157,40 +158,40 @@ class EmailUpdateResponse(TypedDict, total=False):
EmailLatestStatus = Literal[
'SCHEDULED',
'QUEUED',
'SENT',
'DELIVERY_DELAYED',
'BOUNCED',
'REJECTED',
'RENDERING_FAILURE',
'DELIVERED',
'OPENED',
'CLICKED',
'COMPLAINED',
'FAILED',
'CANCELLED',
"SCHEDULED",
"QUEUED",
"SENT",
"DELIVERY_DELAYED",
"BOUNCED",
"REJECTED",
"RENDERING_FAILURE",
"DELIVERED",
"OPENED",
"CLICKED",
"COMPLAINED",
"FAILED",
"CANCELLED",
]
EmailListItem = TypedDict(
'EmailListItem',
"EmailListItem",
{
'id': str,
'to': Union[str, List[str]],
'replyTo': NotRequired[Union[str, List[str]]],
'cc': NotRequired[Union[str, List[str]]],
'bcc': NotRequired[Union[str, List[str]]],
'from': str,
'subject': str,
'html': str,
'text': str,
'createdAt': str,
'updatedAt': str,
'latestStatus': EmailLatestStatus,
'scheduledAt': str,
'domainId': float,
}
"id": str,
"to": Union[str, List[str]],
"replyTo": NotRequired[Union[str, List[str]]],
"cc": NotRequired[Union[str, List[str]]],
"bcc": NotRequired[Union[str, List[str]]],
"from": str,
"subject": str,
"html": str,
"text": str,
"createdAt": str,
"updatedAt": str,
"latestStatus": EmailLatestStatus,
"scheduledAt": str,
"domainId": float,
},
)
@@ -205,23 +206,23 @@ class Attachment(TypedDict):
EmailCreate = TypedDict(
'EmailCreate',
"EmailCreate",
{
'to': Required[Union[str, List[str]]],
'from': Required[str],
'subject': NotRequired[str],
'templateId': NotRequired[str],
'variables': NotRequired[Dict[str, str]],
'replyTo': NotRequired[Union[str, List[str]]],
'cc': NotRequired[Union[str, List[str]]],
'bcc': NotRequired[Union[str, List[str]]],
'text': NotRequired[str],
'html': NotRequired[str],
'attachments': NotRequired[List[Attachment]],
'scheduledAt': NotRequired[Union[datetime, str]],
'inReplyToId': NotRequired[str],
'headers': NotRequired[Dict[str, str]],
}
"to": Required[Union[str, List[str]]],
"from": Required[str],
"subject": NotRequired[str],
"templateId": NotRequired[str],
"variables": NotRequired[Dict[str, str]],
"replyTo": NotRequired[Union[str, List[str]]],
"cc": NotRequired[Union[str, List[str]]],
"bcc": NotRequired[Union[str, List[str]]],
"text": NotRequired[str],
"html": NotRequired[str],
"attachments": NotRequired[List[Attachment]],
"scheduledAt": NotRequired[Union[datetime, str]],
"inReplyToId": NotRequired[str],
"headers": NotRequired[Dict[str, str]],
},
)
@@ -230,23 +231,23 @@ class EmailCreateResponse(TypedDict, total=False):
EmailBatchItem = TypedDict(
'EmailBatchItem',
"EmailBatchItem",
{
'to': Required[Union[str, List[str]]],
'from': Required[str],
'subject': NotRequired[str],
'templateId': NotRequired[str],
'variables': NotRequired[Dict[str, str]],
'replyTo': NotRequired[Union[str, List[str]]],
'cc': NotRequired[Union[str, List[str]]],
'bcc': NotRequired[Union[str, List[str]]],
'text': NotRequired[str],
'html': NotRequired[str],
'attachments': NotRequired[List[Attachment]],
'scheduledAt': NotRequired[Union[datetime, str]],
'inReplyToId': NotRequired[str],
'headers': NotRequired[Dict[str, str]],
}
"to": Required[Union[str, List[str]]],
"from": Required[str],
"subject": NotRequired[str],
"templateId": NotRequired[str],
"variables": NotRequired[Dict[str, str]],
"replyTo": NotRequired[Union[str, List[str]]],
"cc": NotRequired[Union[str, List[str]]],
"bcc": NotRequired[Union[str, List[str]]],
"text": NotRequired[str],
"html": NotRequired[str],
"attachments": NotRequired[List[Attachment]],
"scheduledAt": NotRequired[Union[datetime, str]],
"inReplyToId": NotRequired[str],
"headers": NotRequired[Dict[str, str]],
},
)
@@ -269,6 +270,7 @@ class EmailCancelResponse(TypedDict, total=False):
# Contacts
# ---------------------------------------------------------------------------
class ContactCreate(TypedDict, total=False):
email: str
firstName: Optional[str]
@@ -335,11 +337,115 @@ class ContactDeleteResponse(TypedDict):
success: bool
# ---------------------------------------------------------------------------
# Campaigns
# ---------------------------------------------------------------------------
Campaign = TypedDict(
"Campaign",
{
"id": str,
"name": str,
"from": str,
"subject": str,
"previewText": Optional[str],
"contactBookId": Optional[str],
"html": Optional[str],
"content": Optional[str],
"status": str,
"scheduledAt": Optional[str],
"batchSize": int,
"batchWindowMinutes": int,
"total": int,
"sent": int,
"delivered": int,
"opened": int,
"clicked": int,
"unsubscribed": int,
"bounced": int,
"hardBounced": int,
"complained": int,
"replyTo": List[str],
"cc": List[str],
"bcc": List[str],
"createdAt": str,
"updatedAt": str,
},
)
CampaignCreate = TypedDict(
"CampaignCreate",
{
"name": Required[str],
"from": Required[str],
"subject": Required[str],
"previewText": NotRequired[str],
"contactBookId": Required[str],
"content": NotRequired[str],
"html": NotRequired[str],
"replyTo": NotRequired[Union[str, List[str]]],
"cc": NotRequired[Union[str, List[str]]],
"bcc": NotRequired[Union[str, List[str]]],
"sendNow": NotRequired[bool],
"scheduledAt": NotRequired[str],
"batchSize": NotRequired[int],
},
)
CampaignCreateResponse = TypedDict(
"CampaignCreateResponse",
{
"id": str,
"name": str,
"from": str,
"subject": str,
"previewText": Optional[str],
"contactBookId": Optional[str],
"html": Optional[str],
"content": Optional[str],
"status": str,
"scheduledAt": Optional[str],
"batchSize": int,
"batchWindowMinutes": int,
"total": int,
"sent": int,
"delivered": int,
"opened": int,
"clicked": int,
"unsubscribed": int,
"bounced": int,
"hardBounced": int,
"complained": int,
"replyTo": List[str],
"cc": List[str],
"bcc": List[str],
"createdAt": str,
"updatedAt": str,
},
)
class CampaignSchedule(TypedDict, total=False):
scheduledAt: Optional[str]
batchSize: Optional[int]
sendNow: Optional[bool]
class CampaignScheduleResponse(TypedDict, total=False):
success: bool
class CampaignActionResponse(TypedDict, total=False):
success: bool
# ---------------------------------------------------------------------------
# Common
# ---------------------------------------------------------------------------
class APIError(TypedDict):
code: str
message: str
+2
View File
@@ -72,6 +72,7 @@ class UseSend:
self.emails = Emails(self)
self.contacts = Contacts(self)
self.domains = Domains(self)
self.campaigns = Campaigns(self)
# ------------------------------------------------------------------
# Internal request helper
@@ -125,3 +126,4 @@ class UseSend:
from .emails import Emails # noqa: E402 pylint: disable=wrong-import-position
from .contacts import Contacts # noqa: E402 pylint: disable=wrong-import-position
from .domains import Domains # type: ignore # noqa: E402
from .campaigns import Campaigns # type: ignore # noqa: E402
+35
View File
@@ -49,3 +49,38 @@ usesend.emails.send({
text: "useSend is the best open source product to send emails",
});
```
## Campaigns
Create and manage email campaigns:
```javascript
import { UseSend } from "usesend";
const usesend = new UseSend("us_12345");
// Create a campaign
const campaign = await usesend.campaigns.create({
name: "Welcome Series",
from: "hello@company.com",
subject: "Welcome to our platform!",
contactBookId: "cb_12345",
html: "<h1>Welcome!</h1><p>Thanks for joining us.</p>",
sendNow: false,
});
// Schedule a campaign
await usesend.campaigns.schedule(campaign.data.id, {
scheduledAt: "2024-12-01T09:00:00Z",
batchSize: 1000,
});
// Get campaign details
const details = await usesend.campaigns.get(campaign.data.id);
// Pause a campaign
await usesend.campaigns.pause(campaign.data.id);
// Resume a campaign
await usesend.campaigns.resume(campaign.data.id);
```
+1
View File
@@ -1,2 +1,3 @@
export { UseSend } from "./src/usesend";
export { UseSend as Unsend } from "./src/usesend"; // deprecated alias
export { Campaigns } from "./src/campaign";
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "usesend-js",
"version": "1.5.5",
"version": "1.5.6",
"description": "",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
+94
View File
@@ -0,0 +1,94 @@
import { UseSend } from "./usesend";
import { paths } from "../types/schema";
import { ErrorResponse } from "../types";
type CreateCampaignPayload =
paths["/v1/campaigns"]["post"]["requestBody"]["content"]["application/json"];
type CreateCampaignResponse = {
data: CreateCampaignResponseSuccess | null;
error: ErrorResponse | null;
};
type CreateCampaignResponseSuccess =
paths["/v1/campaigns"]["post"]["responses"]["200"]["content"]["application/json"];
type GetCampaignResponseSuccess =
paths["/v1/campaigns/{campaignId}"]["get"]["responses"]["200"]["content"]["application/json"];
type GetCampaignResponse = {
data: GetCampaignResponseSuccess | null;
error: ErrorResponse | null;
};
type ScheduleCampaignPayload =
paths["/v1/campaigns/{campaignId}/schedule"]["post"]["requestBody"]["content"]["application/json"];
type ScheduleCampaignResponseSuccess =
paths["/v1/campaigns/{campaignId}/schedule"]["post"]["responses"]["200"]["content"]["application/json"];
type ScheduleCampaignResponse = {
data: ScheduleCampaignResponseSuccess | null;
error: ErrorResponse | null;
};
type CampaignActionResponseSuccess = { success: boolean };
type CampaignActionResponse = {
data: CampaignActionResponseSuccess | null;
error: ErrorResponse | null;
};
export class Campaigns {
constructor(private readonly usesend: UseSend) {
this.usesend = usesend;
}
async create(
payload: CreateCampaignPayload,
): Promise<CreateCampaignResponse> {
const data = await this.usesend.post<CreateCampaignResponseSuccess>(
`/campaigns`,
payload,
);
return data;
}
async get(campaignId: string): Promise<GetCampaignResponse> {
const data = await this.usesend.get<GetCampaignResponseSuccess>(
`/campaigns/${campaignId}`,
);
return data;
}
async schedule(
campaignId: string,
payload: ScheduleCampaignPayload,
): Promise<ScheduleCampaignResponse> {
const data = await this.usesend.post<ScheduleCampaignResponseSuccess>(
`/campaigns/${campaignId}/schedule`,
payload,
);
return data;
}
async pause(campaignId: string): Promise<CampaignActionResponse> {
const data = await this.usesend.post<CampaignActionResponseSuccess>(
`/campaigns/${campaignId}/pause`,
{},
);
return data;
}
async resume(campaignId: string): Promise<CampaignActionResponse> {
const data = await this.usesend.post<CampaignActionResponseSuccess>(
`/campaigns/${campaignId}/resume`,
{},
);
return data;
}
}
+2
View File
@@ -2,6 +2,7 @@ import { ErrorResponse } from "../types";
import { Contacts } from "./contact";
import { Emails } from "./email";
import { Domains } from "./domain";
import { Campaigns } from "./campaign";
const defaultBaseUrl = "https://app.usesend.com";
// eslint-disable-next-line turbo/no-undeclared-env-vars
@@ -18,6 +19,7 @@ export class UseSend {
readonly emails = new Emails(this);
readonly domains = new Domains(this);
readonly contacts = new Contacts(this);
readonly campaigns = new Campaigns(this);
url = baseUrl;
constructor(
+276
View File
@@ -959,6 +959,282 @@ export interface paths {
};
trace?: never;
};
"/v1/campaigns": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": {
name: string;
from: string;
subject: string;
previewText?: string;
contactBookId: string;
content?: string;
html?: string;
replyTo?: string | string[];
cc?: string | string[];
bcc?: string | string[];
sendNow?: boolean;
/** @description Timestamp in ISO 8601 format or natural language (e.g., 'tomorrow 9am', 'next monday 10:30') */
scheduledAt?: string;
batchSize?: number;
};
};
};
responses: {
/** @description Create a 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;
};
};
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/campaigns/{campaignId}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: {
parameters: {
query?: never;
header?: never;
path: {
campaignId: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Get campaign details */
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;
};
};
};
};
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/campaigns/{campaignId}/schedule": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: {
parameters: {
query?: never;
header?: never;
path: {
campaignId: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": {
/** @description Timestamp in ISO 8601 format or natural language (e.g., 'tomorrow 9am', 'next monday 10:30') */
scheduledAt?: string;
batchSize?: number;
};
};
};
responses: {
/** @description Schedule a campaign */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
success: boolean;
};
};
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/campaigns/{campaignId}/pause": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: {
parameters: {
query?: never;
header?: never;
path: {
campaignId: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Pause a campaign */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
success: boolean;
};
};
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/campaigns/{campaignId}/resume": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: {
parameters: {
query?: never;
header?: never;
path: {
campaignId: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Resume a campaign */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
success: boolean;
};
};
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {