Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,18 @@ await anypost.suppressions.create({
await anypost.suppressions.delete("alice@example.com", "marketing");
```

Add up to 1,000 at a time with `createBatch`, for example when moving a list over from another provider:

```ts
const result = await anypost.suppressions.createBatch([
{ email: "alice@example.com", reason: "hard bounce" },
{ email: "bob@example.com", topic: "marketing", reason: "unsubscribed" },
]);
// { imported: 2, already_suppressed: 0, invalid: [] }
```

Addresses already suppressed for that topic are counted in `already_suppressed` and left unchanged, so a batch is safe to retry. Invalid addresses come back in `invalid` with their position instead of failing the batch.

`get`, `list` (with `email_contains`, `topic`, `reason`, and `origin` filters), `listForEmail`, and `deleteForEmail` round out the resource. See [Suppressions](https://anypost.com/docs/reference/suppressions) for scoping and the automatic-suppression rules for bounces and complaints.

## Webhooks
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "anypost",
"version": "1.4.0",
"version": "1.5.0",
"description": "Official TypeScript SDK for the Anypost email API.",
"license": "MIT",
"homepage": "https://anypost.com",
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ export type {

export type {
Suppression,
SuppressionBatchInvalid,
SuppressionBatchItem,
SuppressionBatchResult,
SuppressionCreateParams,
SuppressionListParams,
SuppressionOrigin,
Expand Down
23 changes: 22 additions & 1 deletion src/resources/suppressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Page, type PageResponse } from "../pagination.js";
import type { RequestOptions } from "../types/common.js";
import type {
Suppression,
SuppressionBatchItem,
SuppressionBatchResult,
SuppressionCreateParams,
SuppressionListParams,
} from "../types/suppression.js";
Expand All @@ -12,7 +14,7 @@ export class Suppressions {
constructor(private readonly http: HttpClient) {}

/**
* List the team's suppressions, newest-first. Expired rows are filtered out.
* List the team's suppressions, newest-first.
* Returns one {@link Page}; iterate it with `for await` to walk every page.
* Filter with `email_contains`, `topic`, `reason`, and `origin`.
*/
Expand All @@ -39,6 +41,25 @@ export class Suppressions {
});
}

/**
* Add up to 1,000 suppressions in one call. An address already suppressed for
* the same topic is counted in `already_suppressed` and left unchanged, so a
* batch is safe to retry; an invalid address comes back in `invalid` instead
* of failing the batch. Throws `validation_error` (and adds nothing) if the
* batch would take the team past its suppression limit.
*/
createBatch(
suppressions: SuppressionBatchItem[],
options?: RequestOptions,
): Promise<SuppressionBatchResult> {
return this.http.request<SuppressionBatchResult>({
method: "POST",
path: "/suppressions/batch",
body: { suppressions },
...(options ? { options } : {}),
});
}

/**
* Retrieve the suppression for an `(email, topic)` pair. Use `*` as the topic
* for the global row. Throws `not_found` if the pair isn't suppressed.
Expand Down
33 changes: 32 additions & 1 deletion src/types/suppression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface Suppression {
note: string | null;
/** When the suppression was first observed. */
suppressed_at: string;
/** When it stops applying. `null` means never. Permanent bounces roll off after 90 days. */
/** Always `null`: suppressions do not expire. Kept for compatibility; removed in a future major version. */
expires_at: string | null;
created_at: string;
}
Expand All @@ -51,3 +51,34 @@ export interface SuppressionCreateParams {
/** Optional internal annotation, preserved across automatic re-suppressions. */
note?: string | null;
}

/** One entry for `suppressions.createBatch`. */
export interface SuppressionBatchItem {
email: string;
/** Topic to scope the suppression. Omit or `*` to block every topic. */
topic?: string;
/**
* `permanent_bounce`, `complaint`, `unsubscribed` or `manual`. Common
* equivalents (`hard bounce`, `spam report`, `opt-out`) are accepted;
* anything else, or no value, records `manual`.
*/
reason?: string | null;
/** Optional internal annotation, preserved across automatic re-suppressions. */
note?: string | null;
}

/** An entry `suppressions.createBatch` rejected because its `email` is not a valid address. */
export interface SuppressionBatchInvalid {
/** Position of the entry in the array sent. */
index: number;
email: string | null;
}

/** Response from `suppressions.createBatch`. The three outcomes add up to the entries sent. */
export interface SuppressionBatchResult {
/** Suppressions this call created. */
imported: number;
/** Entries already suppressed for that topic, including repeats within the batch. */
already_suppressed: number;
invalid: SuppressionBatchInvalid[];
}
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Generated by scripts/sync-version.mjs — do not edit by hand.
// The version is single-sourced from package.json; run `bun run sync-version`.
export const SDK_VERSION = "1.4.0";
export const SDK_VERSION = "1.5.0";
27 changes: 27 additions & 0 deletions test/suppressions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,33 @@ describe("suppressions", () => {
expect(req.body).toEqual({ email: "alice@example.com", topic: "marketing" });
});

test("createBatch wraps the entries and returns the outcome counts", async () => {
const m = mockFetch([
{
status: 200,
body: { imported: 1, already_suppressed: 0, invalid: [{ index: 1, email: "not-an-address" }] },
},
]);
const anypost = new Anypost("ap_test", { fetch: m.fetch });

const result = await anypost.suppressions.createBatch([
{ email: "alice@example.com", topic: "marketing", reason: "hard bounce" },
{ email: "not-an-address" },
]);

expect(result.imported).toBe(1);
expect(result.invalid).toEqual([{ index: 1, email: "not-an-address" }]);
const req = m.calls[0]!;
expect(req.method).toBe("POST");
expect(req.url).toBe("https://api.anypost.com/v1/suppressions/batch");
expect(req.body).toEqual({
suppressions: [
{ email: "alice@example.com", topic: "marketing", reason: "hard bounce" },
{ email: "not-an-address" },
],
});
});

test("list passes filters as query params", async () => {
const m = mockFetch([
{ status: 200, body: { data: [suppression()], has_more: false, next_cursor: null } },
Expand Down
Loading