Skip to content

Commit 41bc579

Browse files
authored
Contact suppression endpoints (#26)
1 parent ea31c94 commit 41bc579

5 files changed

Lines changed: 383 additions & 5 deletions

File tree

README.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ You can use custom contact properties in API calls. Please make sure to [add cus
9696
- [updateContact()](#updatecontact)
9797
- [findContact()](#findcontact)
9898
- [deleteContact()](#deletecontact)
99+
- [checkContactSuppression()](#checkcontactsuppression)
100+
- [removeContactSuppression()](#removecontactsuppression)
99101
- [createContactProperty()](#createcontactproperty)
100102
- [getContactProperties()](#getcontactproperties)
101103
- [getMailingLists()](#getmailinglists)
@@ -363,6 +365,122 @@ HTTP 404 Not Found
363365

364366
---
365367

368+
### checkContactSuppression()
369+
370+
Check whether a contact is suppressed, either by email address or `userId`.
371+
372+
[API Reference](https://loops.so/docs/api-reference/check-contact-suppression)
373+
374+
#### Parameters
375+
376+
You must use one parameter in the request.
377+
378+
| Name | Type | Required | Notes |
379+
| -------- | ------ | -------- | ----- |
380+
| `email` | string | No | |
381+
| `userId` | string | No | |
382+
383+
#### Example
384+
385+
```javascript
386+
const resp = await loops.checkContactSuppression({ email: "hello@gmail.com" });
387+
388+
const resp = await loops.checkContactSuppression({ userId: "12345" });
389+
```
390+
391+
#### Response
392+
393+
```json
394+
{
395+
"contact": {
396+
"id": "cll6b3i8901a9jx0oyktl2m4u",
397+
"email": "adam@loops.so",
398+
"userId": null
399+
},
400+
"isSuppressed": true,
401+
"removalQuota": {
402+
"limit": 100,
403+
"remaining": 4
404+
}
405+
}
406+
```
407+
408+
Error handling is done through the `APIError` class, which provides `statusCode` and `json` properties containing the API's error response details. For implementation examples, see the [Usage section](#usage).
409+
410+
```json
411+
HTTP 400 Bad Request
412+
{
413+
"success": false,
414+
"message": "An email or userId is required."
415+
}
416+
```
417+
418+
```json
419+
HTTP 404 Not Found
420+
{
421+
"success": false,
422+
"message": "This contact was not found."
423+
}
424+
```
425+
426+
---
427+
428+
### removeContactSuppression()
429+
430+
Remove suppression for a contact, either by email address or `userId`.
431+
432+
[API Reference](https://loops.so/docs/api-reference/remove-contact-suppression)
433+
434+
#### Parameters
435+
436+
You must use one parameter in the request.
437+
438+
| Name | Type | Required | Notes |
439+
| -------- | ------ | -------- | ----- |
440+
| `email` | string | No | |
441+
| `userId` | string | No | |
442+
443+
#### Example
444+
445+
```javascript
446+
const resp = await loops.removeContactSuppression({ email: "hello@gmail.com" });
447+
448+
const resp = await loops.removeContactSuppression({ userId: "12345" });
449+
```
450+
451+
#### Response
452+
453+
```json
454+
{
455+
"success": true,
456+
"message": "Email removed from suppression list.",
457+
"removalQuota": {
458+
"limit": 100,
459+
"remaining": 4
460+
}
461+
}
462+
```
463+
464+
Error handling is done through the `APIError` class, which provides `statusCode` and `json` properties containing the API's error response details. For implementation examples, see the [Usage section](#usage).
465+
466+
```json
467+
HTTP 400 Bad Request
468+
{
469+
"success": false,
470+
"message": "This contact is not suppressed."
471+
}
472+
```
473+
474+
```json
475+
HTTP 404 Not Found
476+
{
477+
"success": false,
478+
"message": "This contact was not found."
479+
}
480+
```
481+
482+
---
483+
366484
### createContactProperty()
367485

368486
Create a new contact property.
@@ -770,6 +888,7 @@ const resp = await loops.getTransactionalEmails({ perPage: 15 });
770888

771889
## Version history
772890

891+
- `v6.3.0` (Apr 8, 2026) - Added [`checkContactSuppression()`](#checkcontactsuppression) and [`removeContactSuppression()`](#removecontactsuppression) methods.
773892
- `v6.2.0` (Feb 9, 2026) - Support for the new arrays feature in sendTransactionalEmail.
774893
- `v6.1.2` (Jan 29, 2026) - Added `rawBody` to `APIError` in the case no JSON is received from the server (thanks to [@leipert](https://github.com/leipert)).
775894
- `v6.0.1` (Oct 15, 2025) - Added `optInStatus` to contact object in [`findContact()`](#findcontact) for the new double opt-in feature.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "loops",
3-
"version": "6.2.1",
3+
"version": "6.3.0",
44
"author": "Dan Rowden <dan@loops.so>",
55
"license": "MIT",
66
"main": "./dist/index.cjs",

src/__tests__/LoopsClient.test.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,156 @@ describe("LoopsClient", () => {
247247
});
248248
});
249249

250+
describe("checkContactSuppression", () => {
251+
it("should check suppression status by email", async () => {
252+
const email = "test@example.com";
253+
const mockResponse = {
254+
contact: {
255+
id: "cll6b3i8901a9jx0oyktl2m4u",
256+
email,
257+
userId: null,
258+
},
259+
isSuppressed: true,
260+
removalQuota: {
261+
limit: 100,
262+
remaining: 10,
263+
},
264+
};
265+
266+
global.fetch = jest.fn().mockResolvedValue({
267+
ok: true,
268+
text: () => Promise.resolve(JSON.stringify(mockResponse)),
269+
});
270+
271+
const result = await client.checkContactSuppression({ email });
272+
273+
expect(result).toEqual(mockResponse);
274+
expect(fetch).toHaveBeenCalledWith(
275+
expect.stringContaining("v1/contacts/suppression?email=test%40example.com"),
276+
expect.objectContaining({
277+
method: "GET",
278+
})
279+
);
280+
});
281+
282+
it("should check suppression status by userId", async () => {
283+
const userId = "user_123";
284+
const mockResponse = {
285+
contact: {
286+
id: "cll6b3i8901a9jx0oyktl2m4u",
287+
email: "test@example.com",
288+
userId,
289+
},
290+
isSuppressed: false,
291+
removalQuota: {
292+
limit: 100,
293+
remaining: 99,
294+
},
295+
};
296+
297+
global.fetch = jest.fn().mockResolvedValue({
298+
ok: true,
299+
text: () => Promise.resolve(JSON.stringify(mockResponse)),
300+
});
301+
302+
const result = await client.checkContactSuppression({ userId });
303+
304+
expect(result).toEqual(mockResponse);
305+
expect(fetch).toHaveBeenCalledWith(
306+
expect.stringContaining("v1/contacts/suppression?userId=user_123"),
307+
expect.objectContaining({
308+
method: "GET",
309+
})
310+
);
311+
});
312+
313+
it("should throw error when both email and userId are provided", async () => {
314+
await expect(
315+
client.checkContactSuppression({
316+
email: "test@example.com",
317+
userId: "user_123",
318+
})
319+
).rejects.toThrow(ValidationError);
320+
});
321+
322+
it("should throw error when neither email nor userId is provided", async () => {
323+
await expect(client.checkContactSuppression({})).rejects.toThrow(
324+
ValidationError
325+
);
326+
});
327+
});
328+
329+
describe("removeContactSuppression", () => {
330+
it("should remove suppression by email", async () => {
331+
const email = "test@example.com";
332+
const mockResponse = {
333+
success: true,
334+
message: "Email removed from suppression list.",
335+
removalQuota: {
336+
limit: 100,
337+
remaining: 9,
338+
},
339+
};
340+
341+
global.fetch = jest.fn().mockResolvedValue({
342+
ok: true,
343+
text: () => Promise.resolve(JSON.stringify(mockResponse)),
344+
});
345+
346+
const result = await client.removeContactSuppression({ email });
347+
348+
expect(result).toEqual(mockResponse);
349+
expect(fetch).toHaveBeenCalledWith(
350+
expect.stringContaining("v1/contacts/suppression?email=test%40example.com"),
351+
expect.objectContaining({
352+
method: "DELETE",
353+
})
354+
);
355+
});
356+
357+
it("should remove suppression by userId", async () => {
358+
const userId = "user_123";
359+
const mockResponse = {
360+
success: true,
361+
message: "User removed from suppression list.",
362+
removalQuota: {
363+
limit: 100,
364+
remaining: 8,
365+
},
366+
};
367+
368+
global.fetch = jest.fn().mockResolvedValue({
369+
ok: true,
370+
text: () => Promise.resolve(JSON.stringify(mockResponse)),
371+
});
372+
373+
const result = await client.removeContactSuppression({ userId });
374+
375+
expect(result).toEqual(mockResponse);
376+
expect(fetch).toHaveBeenCalledWith(
377+
expect.stringContaining("v1/contacts/suppression?userId=user_123"),
378+
expect.objectContaining({
379+
method: "DELETE",
380+
})
381+
);
382+
});
383+
384+
it("should throw error when both email and userId are provided", async () => {
385+
await expect(
386+
client.removeContactSuppression({
387+
email: "test@example.com",
388+
userId: "user_123",
389+
})
390+
).rejects.toThrow(ValidationError);
391+
});
392+
393+
it("should throw error when neither email nor userId is provided", async () => {
394+
await expect(client.removeContactSuppression({})).rejects.toThrow(
395+
ValidationError
396+
);
397+
});
398+
});
399+
250400
describe("createContactProperty", () => {
251401
it("should create contact property successfully", async () => {
252402
const name = "customField";

0 commit comments

Comments
 (0)