Skip to content

Commit bfd2002

Browse files
committed
Create segment method
1 parent a80f3fc commit bfd2002

4 files changed

Lines changed: 220 additions & 1 deletion

File tree

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ You can use custom contact properties in API calls. Please make sure to [add cus
116116
- [listDedicatedSendingIps()](#listdedicatedsendingips)
117117
- [listAudienceSegments()](#listaudiencesegments)
118118
- [getAudienceSegment()](#getaudiencesegment)
119+
- [createAudienceSegment()](#createaudiencesegment)
119120
- [listThemes()](#listthemes)
120121
- [getTheme()](#gettheme)
121122
- [createTheme()](#createtheme)
@@ -2027,6 +2028,64 @@ const resp = await loops.getAudienceSegment("clr4c8t0v008yl70x3y4z5a6b");
20272028

20282029
---
20292030

2031+
### createAudienceSegment()
2032+
2033+
Create a new audience segment.
2034+
[API Reference](https://loops.so/docs/api-reference/create-audience-segment)
2035+
2036+
#### Parameters
2037+
2038+
| Name | Type | Required | Notes |
2039+
| ---- | ---- | -------- | ----- |
2040+
| `name` | string | Yes | The name of the audience segment. Must be unique within the team. |
2041+
| `description` | string | No | An optional description of the audience segment. |
2042+
| `filter` | object | Yes | A tree of audience conditions combined with `match` (`all` or `any`). |
2043+
2044+
#### Example
2045+
2046+
```javascript
2047+
const resp = await loops.createAudienceSegment({
2048+
name: "Active users",
2049+
description: "Contacts who opened a recent campaign",
2050+
filter: {
2051+
match: "all",
2052+
conditions: [
2053+
{
2054+
type: "property",
2055+
key: "planName",
2056+
operator: "equals",
2057+
value: "pro",
2058+
},
2059+
],
2060+
},
2061+
});
2062+
```
2063+
2064+
#### Response
2065+
2066+
```json
2067+
{
2068+
"id": "cls6e8g0i2k4m6o8q0s2u4w6",
2069+
"name": "Active users",
2070+
"description": "Contacts who opened a recent campaign",
2071+
"createdAt": "2025-06-29T07:47:39.370Z",
2072+
"updatedAt": "2025-06-29T07:47:39.370Z",
2073+
"filter": {
2074+
"match": "all",
2075+
"conditions": [
2076+
{
2077+
"type": "property",
2078+
"key": "planName",
2079+
"operator": "equals",
2080+
"value": "pro"
2081+
}
2082+
]
2083+
}
2084+
}
2085+
```
2086+
2087+
---
2088+
20302089
### listCampaignGroups()
20312090

20322091
Retrieve a paginated list of campaign groups.
@@ -2968,6 +3027,7 @@ const resp = await loops.completeUpload("clu7f1w3y011yl70m5n6o7p8q");
29683027
- Added event patterns ([`listEventPatterns()`](#listeventpatterns), [`getEventPattern()`](#geteventpattern), [`getEventPatternByName()`](#geteventpatternbyname)).
29693028
- Expanded workflows with create/update/mutate APIs ([`createWorkflow()`](#createworkflow), [`updateWorkflow()`](#updateworkflow), [`changeWorkflowMailingList()`](#changeworkflowmailinglist), [`createWorkflowNode()`](#createworkflownode), [`updateWorkflowNode()`](#updateworkflownode), [`deleteWorkflowNode()`](#deleteworkflownode), [`addWorkflowBranch()`](#addworkflowbranch), [`deleteWorkflowNodesRecursive()`](#deleteworkflownodesrecursive)).
29703029
- Added theme and component write APIs ([`createTheme()`](#createtheme), [`updateTheme()`](#updatetheme), [`createComponent()`](#createcomponent), [`updateComponent()`](#updatecomponent)).
3030+
- Added [`createAudienceSegment()`](#createaudiencesegment).
29713031
- Added [`runEmailMessageGuardian()`](#runemailmessageguardian).
29723032
- Aligned workflow response types with the API (`status`, nullable `workflowRevisionId`; removed unused `emoji`).
29733033
- `v7.0.0` (Jun 24, 2026)

src/__tests__/LoopsClient.test.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,6 +1116,130 @@ describe("LoopsClient", () => {
11161116
});
11171117
});
11181118

1119+
describe("createAudienceSegment", () => {
1120+
it("should create an audience segment", async () => {
1121+
const filter = {
1122+
match: "all" as const,
1123+
conditions: [
1124+
{
1125+
type: "property" as const,
1126+
key: "planName",
1127+
operator: "equals" as const,
1128+
value: "pro",
1129+
},
1130+
],
1131+
};
1132+
const mockResponse = {
1133+
id: "cls6e8g0i2k4m6o8q0s2u4w6",
1134+
name: "Active users",
1135+
description: "Contacts on the pro plan",
1136+
createdAt: "2025-06-29T07:47:39.370Z",
1137+
updatedAt: "2025-06-29T07:47:39.370Z",
1138+
filter,
1139+
};
1140+
1141+
global.fetch = jest.fn().mockResolvedValue({
1142+
ok: true,
1143+
text: () => Promise.resolve(JSON.stringify(mockResponse)),
1144+
});
1145+
1146+
const result = await client.createAudienceSegment({
1147+
name: "Active users",
1148+
description: "Contacts on the pro plan",
1149+
filter,
1150+
});
1151+
1152+
expect(result).toEqual(mockResponse);
1153+
expect(fetch).toHaveBeenCalledWith(
1154+
expect.stringContaining("v1/audience-segments"),
1155+
expect.objectContaining({
1156+
method: "POST",
1157+
body: JSON.stringify({
1158+
name: "Active users",
1159+
filter,
1160+
description: "Contacts on the pro plan",
1161+
}),
1162+
})
1163+
);
1164+
});
1165+
1166+
it("should handle error when segment name already exists", async () => {
1167+
const filter = {
1168+
match: "all" as const,
1169+
conditions: [
1170+
{
1171+
type: "property" as const,
1172+
key: "planName",
1173+
operator: "equals" as const,
1174+
value: "pro",
1175+
},
1176+
],
1177+
};
1178+
const mockResponse = {
1179+
message: "An audience segment with this name already exists",
1180+
};
1181+
1182+
global.fetch = jest.fn().mockResolvedValue({
1183+
ok: false,
1184+
status: 400,
1185+
text: () => Promise.resolve(JSON.stringify(mockResponse)),
1186+
});
1187+
1188+
await expect(
1189+
client.createAudienceSegment({
1190+
name: "Active users",
1191+
filter,
1192+
})
1193+
).rejects.toThrow(APIError);
1194+
1195+
expect(fetch).toHaveBeenCalledWith(
1196+
expect.stringContaining("v1/audience-segments"),
1197+
expect.objectContaining({
1198+
method: "POST",
1199+
body: JSON.stringify({
1200+
name: "Active users",
1201+
filter,
1202+
}),
1203+
})
1204+
);
1205+
});
1206+
1207+
it("should handle error for an invalid filter", async () => {
1208+
const filter = {
1209+
match: "all" as const,
1210+
conditions: [
1211+
{
1212+
type: "property" as const,
1213+
key: "planName",
1214+
operator: "equals" as const,
1215+
value: "pro",
1216+
},
1217+
],
1218+
};
1219+
const mockResponse = {
1220+
message: "Invalid filter",
1221+
};
1222+
1223+
global.fetch = jest.fn().mockResolvedValue({
1224+
ok: false,
1225+
status: 400,
1226+
text: () => Promise.resolve(JSON.stringify(mockResponse)),
1227+
});
1228+
1229+
try {
1230+
await client.createAudienceSegment({
1231+
name: "Active users",
1232+
filter,
1233+
});
1234+
fail("Should have thrown");
1235+
} catch (error) {
1236+
expect(error).toBeInstanceOf(APIError);
1237+
expect((error as APIError).statusCode).toBe(400);
1238+
expect((error as APIError).json).toEqual(mockResponse);
1239+
}
1240+
});
1241+
});
1242+
11191243
describe("listThemes", () => {
11201244
it("should list themes with pagination", async () => {
11211245
const mockResponse = {

src/index.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1540,6 +1540,40 @@ class LoopsClient {
15401540
});
15411541
}
15421542

1543+
/**
1544+
* Create an audience segment.
1545+
*
1546+
* @param {Object} params
1547+
* @param {string} params.name The name of the audience segment. Must be unique within the team.
1548+
* @param {string} [params.description] An optional description of the audience segment.
1549+
* @param {AudienceFilter} params.filter A tree of audience conditions combined with `match`.
1550+
*
1551+
* @see https://loops.so/docs/api-reference/create-audience-segment
1552+
*
1553+
* @returns {Object} Created audience segment (JSON)
1554+
*/
1555+
async createAudienceSegment({
1556+
name,
1557+
description,
1558+
filter,
1559+
}: {
1560+
name: string;
1561+
description?: string;
1562+
filter: AudienceFilter;
1563+
}): Promise<AudienceSegment> {
1564+
const payload: {
1565+
name: string;
1566+
description?: string;
1567+
filter: AudienceFilter;
1568+
} = { name, filter };
1569+
if (description !== undefined) payload.description = description;
1570+
return this._makeQuery({
1571+
path: "v1/audience-segments",
1572+
method: "POST",
1573+
payload,
1574+
});
1575+
}
1576+
15431577
/**
15441578
* List email themes.
15451579
*

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"noUnusedLocals": true,
55
"noUnusedParameters": true,
66
"strict": true,
7-
"target": "ESNext"
7+
"target": "ESNext",
8+
"types": ["jest", "node"]
89
},
910
"include": ["./src/**/*.ts"]
1011
}

0 commit comments

Comments
 (0)