From a6e3502aee185efe8e33e2144d5ad2de0f42bab8 Mon Sep 17 00:00:00 2001 From: Igor Dobryn Date: Thu, 30 Apr 2026 10:26:55 +0300 Subject: [PATCH 01/14] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3b74a4a..2900538 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ dist node_modules yarn-error.log coverage +.idea/ From 78ae0f6c58e9a613a337c0b62e02a2d3975dfdea Mon Sep 17 00:00:00 2001 From: Igor Dobryn Date: Thu, 30 Apr 2026 10:36:23 +0300 Subject: [PATCH 02/14] Add ApiTokensApi#create --- src/__tests__/lib/api/General.test.ts | 11 +++ .../lib/api/resources/ApiTokens.test.ts | 98 +++++++++++++++++++ src/lib/api/General.ts | 15 +++ src/lib/api/resources/ApiTokens.ts | 31 ++++++ src/types/api/api-tokens.ts | 30 ++++++ 5 files changed, 185 insertions(+) create mode 100644 src/__tests__/lib/api/resources/ApiTokens.test.ts create mode 100644 src/lib/api/resources/ApiTokens.ts create mode 100644 src/types/api/api-tokens.ts diff --git a/src/__tests__/lib/api/General.test.ts b/src/__tests__/lib/api/General.test.ts index 74cb18f..51928e2 100644 --- a/src/__tests__/lib/api/General.test.ts +++ b/src/__tests__/lib/api/General.test.ts @@ -14,6 +14,7 @@ describe("lib/api/General: ", () => { expect(generalAPI).toHaveProperty("accounts"); expect(generalAPI).toHaveProperty("permissions"); expect(generalAPI).toHaveProperty("billing"); + expect(generalAPI).toHaveProperty("apiTokens"); }); it("lazily instantiates account-specific APIs via getters when accountId is provided.", () => { @@ -21,6 +22,7 @@ describe("lib/api/General: ", () => { expect(generalAPI.permissions).toBeDefined(); expect(generalAPI.billing).toBeDefined(); expect(generalAPI.accounts).toBeDefined(); + expect(generalAPI.apiTokens).toBeDefined(); }); }); @@ -67,6 +69,15 @@ describe("lib/api/General: ", () => { "Account ID is required for this operation. Please provide accountId when creating GeneralAPI instance." ); }); + + it("throws error when accessing apiTokens without accountId.", () => { + expect(() => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + generalAPI.apiTokens; + }).toThrow( + "Account ID is required for this operation. Please provide accountId when creating GeneralAPI instance." + ); + }); }); describe("account discovery functionality: ", () => { diff --git a/src/__tests__/lib/api/resources/ApiTokens.test.ts b/src/__tests__/lib/api/resources/ApiTokens.test.ts new file mode 100644 index 0000000..42218cf --- /dev/null +++ b/src/__tests__/lib/api/resources/ApiTokens.test.ts @@ -0,0 +1,98 @@ +import axios from "axios"; +import AxiosMockAdapter from "axios-mock-adapter"; + +import ApiTokensApi from "../../../../lib/api/resources/ApiTokens"; +import handleSendingError from "../../../../lib/axios-logger"; +import MailtrapError from "../../../../lib/MailtrapError"; + +import CONFIG from "../../../../config"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +describe("lib/api/resources/ApiTokens: ", () => { + let mock: AxiosMockAdapter; + const accountId = 100; + const apiTokensAPI = new ApiTokensApi(axios, accountId); + + describe("class ApiTokensApi(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(apiTokensAPI).toHaveProperty("create"); + }); + }); + }); + + beforeAll(() => { + /** + * Init Axios interceptors for handling response.data, errors. + */ + axios.interceptors.response.use( + (response) => response.data, + handleSendingError + ); + mock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + mock.reset(); + }); + + describe("create(): ", () => { + const params = { + name: "My API Token", + resources: [ + { + resource_type: "account" as const, + resource_id: 3229, + access_level: 100 as const, + }, + ], + }; + + const responseData = { + id: 12345, + name: "My API Token", + last_4_digits: "x7k9", + created_by: "user@example.com", + expires_at: null, + resources: [ + { + resource_type: "account", + resource_id: 3229, + access_level: 100, + }, + ], + token: "a1b2c3d4e5f6", + }; + + it("creates an API token and returns the full token value.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens`; + + expect.assertions(3); + + mock.onPost(endpoint, params).reply(200, responseData); + const result = await apiTokensAPI.create(params); + + expect(mock.history.post[0].url).toEqual(endpoint); + expect(JSON.parse(mock.history.post[0].data)).toEqual(params); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await apiTokensAPI.create(params); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); +}); diff --git a/src/lib/api/General.ts b/src/lib/api/General.ts index 2ac485d..0a20e07 100644 --- a/src/lib/api/General.ts +++ b/src/lib/api/General.ts @@ -2,6 +2,7 @@ import { AxiosInstance } from "axios"; import AccountAccessesApi from "./resources/AccountAccesses"; import AccountsApi from "./resources/Accounts"; +import ApiTokensApi from "./resources/ApiTokens"; import BillingApi from "./resources/Billing"; import PermissionsApi from "./resources/Permissions"; @@ -18,6 +19,8 @@ export default class GeneralAPI { private billingInstance: BillingApi | null = null; + private apiTokensInstance: ApiTokensApi | null = null; + constructor(client: AxiosInstance, accountId?: number) { this.client = client; this.accountId = accountId ?? null; @@ -83,4 +86,16 @@ export default class GeneralAPI { return this.billingInstance; } + + /** + * Singleton getter for API Tokens API. + */ + public get apiTokens(): ApiTokensApi { + if (this.apiTokensInstance === null) { + const accountId = this.checkAccountIdPresence(); + this.apiTokensInstance = new ApiTokensApi(this.client, accountId); + } + + return this.apiTokensInstance; + } } diff --git a/src/lib/api/resources/ApiTokens.ts b/src/lib/api/resources/ApiTokens.ts new file mode 100644 index 0000000..d3a1e52 --- /dev/null +++ b/src/lib/api/resources/ApiTokens.ts @@ -0,0 +1,31 @@ +import { AxiosInstance } from "axios"; + +import CONFIG from "../../../config"; +import { + ApiTokenWithToken, + CreateApiTokenRequest, +} from "../../../types/api/api-tokens"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +export default class ApiTokensApi { + private client: AxiosInstance; + + private apiTokensURL: string; + + constructor(client: AxiosInstance, accountId: number) { + this.client = client; + this.apiTokensURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens`; + } + + /** + * Create a new API token for the account with the given name and resource permissions. + * The full token value is returned only in the response of this call — store it securely. + */ + public async create(params: CreateApiTokenRequest) { + const url = this.apiTokensURL; + + return this.client.post(url, params); + } +} diff --git a/src/types/api/api-tokens.ts b/src/types/api/api-tokens.ts new file mode 100644 index 0000000..7a41792 --- /dev/null +++ b/src/types/api/api-tokens.ts @@ -0,0 +1,30 @@ +export type ResourceType = "account" | "project" | "inbox" | "sending_domain"; + +export type AccessLevel = 10 | 100; + +export type ResourcePermissionInput = { + resource_type: ResourceType; + resource_id: number | string; + access_level: AccessLevel; +}; + +export type ResourcePermission = { + resource_type: ResourceType; + resource_id: number | string; + access_level: AccessLevel; +}; + +export type CreateApiTokenRequest = { + name: string; + resources?: ResourcePermissionInput[]; +}; + +export type ApiTokenWithToken = { + id: number; + name: string; + last_4_digits: string; + created_by: string; + expires_at: string | null; + resources: ResourcePermission[]; + token: string; +}; From d0fa4afd0faa76b0b8d223e965262d83faa7fe53 Mon Sep 17 00:00:00 2001 From: Igor Dobryn Date: Thu, 30 Apr 2026 10:44:23 +0300 Subject: [PATCH 03/14] Add ApiTokensApi#get --- .../lib/api/resources/ApiTokens.test.ts | 47 +++++++++++++++++++ src/lib/api/resources/ApiTokens.ts | 11 +++++ src/types/api/api-tokens.ts | 5 +- 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/__tests__/lib/api/resources/ApiTokens.test.ts b/src/__tests__/lib/api/resources/ApiTokens.test.ts index 42218cf..41d6f1f 100644 --- a/src/__tests__/lib/api/resources/ApiTokens.test.ts +++ b/src/__tests__/lib/api/resources/ApiTokens.test.ts @@ -19,6 +19,7 @@ describe("lib/api/resources/ApiTokens: ", () => { describe("init: ", () => { it("initializes with all necessary params.", () => { expect(apiTokensAPI).toHaveProperty("create"); + expect(apiTokensAPI).toHaveProperty("get"); }); }); }); @@ -95,4 +96,50 @@ describe("lib/api/resources/ApiTokens: ", () => { } }); }); + + describe("get(): ", () => { + const tokenId = 12345; + const responseData = { + id: tokenId, + name: "My API Token", + last_4_digits: "x7k9", + created_by: "user@example.com", + expires_at: null, + resources: [ + { + resource_type: "account", + resource_id: 3229, + access_level: 100, + }, + ], + }; + + it("gets an API token by id.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens/${tokenId}`; + + expect.assertions(2); + + mock.onGet(endpoint).reply(200, responseData); + const result = await apiTokensAPI.get(tokenId); + + expect(mock.history.get[0].url).toEqual(endpoint); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await apiTokensAPI.get(tokenId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); }); diff --git a/src/lib/api/resources/ApiTokens.ts b/src/lib/api/resources/ApiTokens.ts index d3a1e52..5ccc400 100644 --- a/src/lib/api/resources/ApiTokens.ts +++ b/src/lib/api/resources/ApiTokens.ts @@ -2,6 +2,7 @@ import { AxiosInstance } from "axios"; import CONFIG from "../../../config"; import { + ApiToken, ApiTokenWithToken, CreateApiTokenRequest, } from "../../../types/api/api-tokens"; @@ -28,4 +29,14 @@ export default class ApiTokensApi { return this.client.post(url, params); } + + /** + * Get a single API token by ID. The full token value is not returned — + * only `last_4_digits` is available outside of create/reset responses. + */ + public async get(id: number) { + const url = `${this.apiTokensURL}/${id}`; + + return this.client.get(url); + } } diff --git a/src/types/api/api-tokens.ts b/src/types/api/api-tokens.ts index 7a41792..472b0f2 100644 --- a/src/types/api/api-tokens.ts +++ b/src/types/api/api-tokens.ts @@ -19,12 +19,15 @@ export type CreateApiTokenRequest = { resources?: ResourcePermissionInput[]; }; -export type ApiTokenWithToken = { +export type ApiToken = { id: number; name: string; last_4_digits: string; created_by: string; expires_at: string | null; resources: ResourcePermission[]; +}; + +export type ApiTokenWithToken = ApiToken & { token: string; }; From f77afa6c77181e26cbc60d5a872a02ad944cf9b0 Mon Sep 17 00:00:00 2001 From: Igor Dobryn Date: Thu, 30 Apr 2026 10:46:57 +0300 Subject: [PATCH 04/14] Add ApiTokensApi#reset --- .../lib/api/resources/ApiTokens.test.ts | 48 +++++++++++++++++++ src/lib/api/resources/ApiTokens.ts | 11 +++++ 2 files changed, 59 insertions(+) diff --git a/src/__tests__/lib/api/resources/ApiTokens.test.ts b/src/__tests__/lib/api/resources/ApiTokens.test.ts index 41d6f1f..09c678d 100644 --- a/src/__tests__/lib/api/resources/ApiTokens.test.ts +++ b/src/__tests__/lib/api/resources/ApiTokens.test.ts @@ -20,6 +20,7 @@ describe("lib/api/resources/ApiTokens: ", () => { it("initializes with all necessary params.", () => { expect(apiTokensAPI).toHaveProperty("create"); expect(apiTokensAPI).toHaveProperty("get"); + expect(apiTokensAPI).toHaveProperty("reset"); }); }); }); @@ -142,4 +143,51 @@ describe("lib/api/resources/ApiTokens: ", () => { } }); }); + + describe("reset(): ", () => { + const tokenId = 12345; + const responseData = { + id: tokenId, + name: "My API Token", + last_4_digits: "p3q4", + created_by: "user@example.com", + expires_at: null, + resources: [ + { + resource_type: "account", + resource_id: 3229, + access_level: 100, + }, + ], + token: "newtokenvalue", + }; + + it("resets an API token and returns the new token value.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens/${tokenId}/reset`; + + expect.assertions(2); + + mock.onPost(endpoint).reply(200, responseData); + const result = await apiTokensAPI.reset(tokenId); + + expect(mock.history.post[0].url).toEqual(endpoint); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await apiTokensAPI.reset(tokenId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); }); diff --git a/src/lib/api/resources/ApiTokens.ts b/src/lib/api/resources/ApiTokens.ts index 5ccc400..afb6b4c 100644 --- a/src/lib/api/resources/ApiTokens.ts +++ b/src/lib/api/resources/ApiTokens.ts @@ -39,4 +39,15 @@ export default class ApiTokensApi { return this.client.get(url); } + + /** + * Reset an API token: expires the existing token and returns a new one with + * the same permissions. The new token value is returned only in this response — + * store it securely. Only tokens that have not already been reset can be reset. + */ + public async reset(id: number) { + const url = `${this.apiTokensURL}/${id}/reset`; + + return this.client.post(url); + } } From 999533a3dd1478b73b9f85ca9ecf35c1789d7660 Mon Sep 17 00:00:00 2001 From: Igor Dobryn Date: Thu, 30 Apr 2026 10:56:20 +0300 Subject: [PATCH 05/14] Add ApiTokensApi#delete --- .../lib/api/resources/ApiTokens.test.ts | 32 +++++++++++++++++++ src/lib/api/resources/ApiTokens.ts | 9 ++++++ 2 files changed, 41 insertions(+) diff --git a/src/__tests__/lib/api/resources/ApiTokens.test.ts b/src/__tests__/lib/api/resources/ApiTokens.test.ts index 09c678d..854c6eb 100644 --- a/src/__tests__/lib/api/resources/ApiTokens.test.ts +++ b/src/__tests__/lib/api/resources/ApiTokens.test.ts @@ -21,6 +21,7 @@ describe("lib/api/resources/ApiTokens: ", () => { expect(apiTokensAPI).toHaveProperty("create"); expect(apiTokensAPI).toHaveProperty("get"); expect(apiTokensAPI).toHaveProperty("reset"); + expect(apiTokensAPI).toHaveProperty("delete"); }); }); }); @@ -190,4 +191,35 @@ describe("lib/api/resources/ApiTokens: ", () => { } }); }); + + describe("delete(): ", () => { + const tokenId = 12345; + + it("deletes an API token by id.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens/${tokenId}`; + + expect.assertions(1); + + mock.onDelete(endpoint).reply(204); + await apiTokensAPI.delete(tokenId); + + expect(mock.history.delete[0].url).toEqual(endpoint); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await apiTokensAPI.delete(tokenId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); }); diff --git a/src/lib/api/resources/ApiTokens.ts b/src/lib/api/resources/ApiTokens.ts index afb6b4c..3355628 100644 --- a/src/lib/api/resources/ApiTokens.ts +++ b/src/lib/api/resources/ApiTokens.ts @@ -50,4 +50,13 @@ export default class ApiTokensApi { return this.client.post(url); } + + /** + * Permanently delete an API token by ID. + */ + public async delete(id: number) { + const url = `${this.apiTokensURL}/${id}`; + + return this.client.delete(url); + } } From a422528fd6b2da0b7adceda165bc33ab86fa8372 Mon Sep 17 00:00:00 2001 From: Igor Dobryn Date: Thu, 30 Apr 2026 11:00:49 +0300 Subject: [PATCH 06/14] Add ApiTokensApi#getList --- .../lib/api/resources/ApiTokens.test.ts | 48 +++++++++++++++++++ src/lib/api/resources/ApiTokens.ts | 10 ++++ 2 files changed, 58 insertions(+) diff --git a/src/__tests__/lib/api/resources/ApiTokens.test.ts b/src/__tests__/lib/api/resources/ApiTokens.test.ts index 854c6eb..bc127f5 100644 --- a/src/__tests__/lib/api/resources/ApiTokens.test.ts +++ b/src/__tests__/lib/api/resources/ApiTokens.test.ts @@ -18,6 +18,7 @@ describe("lib/api/resources/ApiTokens: ", () => { describe("class ApiTokensApi(): ", () => { describe("init: ", () => { it("initializes with all necessary params.", () => { + expect(apiTokensAPI).toHaveProperty("getList"); expect(apiTokensAPI).toHaveProperty("create"); expect(apiTokensAPI).toHaveProperty("get"); expect(apiTokensAPI).toHaveProperty("reset"); @@ -41,6 +42,53 @@ describe("lib/api/resources/ApiTokens: ", () => { mock.reset(); }); + describe("getList(): ", () => { + const responseData = [ + { + id: 12345, + name: "My API Token", + last_4_digits: "x7k9", + created_by: "user@example.com", + expires_at: null, + resources: [ + { + resource_type: "account", + resource_id: 3229, + access_level: 100, + }, + ], + }, + ]; + + it("gets the list of API tokens.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens`; + + expect.assertions(2); + + mock.onGet(endpoint).reply(200, responseData); + const result = await apiTokensAPI.getList(); + + expect(mock.history.get[0].url).toEqual(endpoint); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await apiTokensAPI.getList(); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + describe("create(): ", () => { const params = { name: "My API Token", diff --git a/src/lib/api/resources/ApiTokens.ts b/src/lib/api/resources/ApiTokens.ts index 3355628..205c30e 100644 --- a/src/lib/api/resources/ApiTokens.ts +++ b/src/lib/api/resources/ApiTokens.ts @@ -20,6 +20,16 @@ export default class ApiTokensApi { this.apiTokensURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens`; } + /** + * List all API tokens visible to the current API token. + * The full token value is never returned here — only `last_4_digits`. + */ + public async getList() { + const url = this.apiTokensURL; + + return this.client.get(url); + } + /** * Create a new API token for the account with the given name and resource permissions. * The full token value is returned only in the response of this call — store it securely. From efb5b2c9065736bd25529d6037b1ab908ba7e91a Mon Sep 17 00:00:00 2001 From: Igor Dobryn Date: Thu, 30 Apr 2026 11:48:47 +0300 Subject: [PATCH 07/14] Add WebhooksBaseAPI#getList --- src/__tests__/lib/api/Webhooks.test.ts | 16 ++++ .../lib/api/resources/Webhooks.test.ts | 92 +++++++++++++++++++ src/lib/MailtrapClient.ts | 9 ++ src/lib/api/Webhooks.ts | 15 +++ src/lib/api/resources/Webhooks.ts | 27 ++++++ src/types/api/webhooks.ts | 31 +++++++ 6 files changed, 190 insertions(+) create mode 100644 src/__tests__/lib/api/Webhooks.test.ts create mode 100644 src/__tests__/lib/api/resources/Webhooks.test.ts create mode 100644 src/lib/api/Webhooks.ts create mode 100644 src/lib/api/resources/Webhooks.ts create mode 100644 src/types/api/webhooks.ts diff --git a/src/__tests__/lib/api/Webhooks.test.ts b/src/__tests__/lib/api/Webhooks.test.ts new file mode 100644 index 0000000..8ff1fb8 --- /dev/null +++ b/src/__tests__/lib/api/Webhooks.test.ts @@ -0,0 +1,16 @@ +import axios from "axios"; + +import WebhooksBaseAPI from "../../../lib/api/Webhooks"; + +describe("lib/api/Webhooks: ", () => { + const accountId = 100; + const webhooksAPI = new WebhooksBaseAPI(axios, accountId); + + describe("class WebhooksBaseAPI(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(webhooksAPI).toHaveProperty("getList"); + }); + }); + }); +}); diff --git a/src/__tests__/lib/api/resources/Webhooks.test.ts b/src/__tests__/lib/api/resources/Webhooks.test.ts new file mode 100644 index 0000000..5f3c6a1 --- /dev/null +++ b/src/__tests__/lib/api/resources/Webhooks.test.ts @@ -0,0 +1,92 @@ +import axios from "axios"; +import AxiosMockAdapter from "axios-mock-adapter"; + +import WebhooksApi from "../../../../lib/api/resources/Webhooks"; +import handleSendingError from "../../../../lib/axios-logger"; +import MailtrapError from "../../../../lib/MailtrapError"; + +import CONFIG from "../../../../config"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +describe("lib/api/resources/Webhooks: ", () => { + let mock: AxiosMockAdapter; + const accountId = 100; + const webhooksAPI = new WebhooksApi(axios, accountId); + + describe("class WebhooksApi(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(webhooksAPI).toHaveProperty("getList"); + }); + }); + }); + + beforeAll(() => { + /** + * Init Axios interceptors for handling response.data, errors. + */ + axios.interceptors.response.use( + (response) => response.data, + handleSendingError + ); + mock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + mock.reset(); + }); + + describe("getList(): ", () => { + const responseData = { + data: [ + { + id: 1, + url: "https://example.com/mailtrap/webhooks", + active: true, + webhook_type: "email_sending", + payload_format: "json", + sending_stream: "transactional", + domain_id: 435, + event_types: ["delivery", "bounce"], + }, + { + id: 2, + url: "https://example.com/mailtrap/webhooks", + active: true, + webhook_type: "audit_log", + payload_format: "json", + }, + ], + }; + + it("gets the list of webhooks.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/webhooks`; + + expect.assertions(2); + + mock.onGet(endpoint).reply(200, responseData); + const result = await webhooksAPI.getList(); + + expect(mock.history.get[0].url).toEqual(endpoint); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await webhooksAPI.getList(); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); +}); diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index e5037c1..8c1c0fe 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -20,6 +20,7 @@ import StatsBaseAPI from "./api/Stats"; import SuppressionsBaseAPI from "./api/Suppressions"; import TemplatesBaseAPI from "./api/Templates"; import TestingAPI from "./api/Testing"; +import WebhooksBaseAPI from "./api/Webhooks"; import CONFIG from "../config"; @@ -215,6 +216,14 @@ export default class MailtrapClient { return new EmailLogsBaseAPI(this.axios, accountId); } + /** + * Getter for Webhooks API. + */ + get webhooks() { + const accountId = this.validateAccountIdPresence(); + return new WebhooksBaseAPI(this.axios, accountId); + } + /** * Returns configured host. Checks if `bulk` and `sandbox` modes are activated simultaneously, * then reject with Mailtrap Error. diff --git a/src/lib/api/Webhooks.ts b/src/lib/api/Webhooks.ts new file mode 100644 index 0000000..2bc4e9e --- /dev/null +++ b/src/lib/api/Webhooks.ts @@ -0,0 +1,15 @@ +import { AxiosInstance } from "axios"; + +import WebhooksApi from "./resources/Webhooks"; + +export default class WebhooksBaseAPI { + private client: AxiosInstance; + + public getList: WebhooksApi["getList"]; + + constructor(client: AxiosInstance, accountId: number) { + this.client = client; + const webhooks = new WebhooksApi(this.client, accountId); + this.getList = webhooks.getList.bind(webhooks); + } +} diff --git a/src/lib/api/resources/Webhooks.ts b/src/lib/api/resources/Webhooks.ts new file mode 100644 index 0000000..398e62d --- /dev/null +++ b/src/lib/api/resources/Webhooks.ts @@ -0,0 +1,27 @@ ++import { AxiosInstance } from "axios"; + +import CONFIG from "../../../config"; +import { ListWebhooksResponse } from "../../../types/api/webhooks"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +export default class WebhooksApi { + private client: AxiosInstance; + + private webhooksURL: string; + + constructor(client: AxiosInstance, accountId: number) { + this.client = client; + this.webhooksURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/webhooks`; + } + + /** + * Returns all webhooks for the account. + */ + public async getList() { + const url = this.webhooksURL; + + return this.client.get(url); + } +} diff --git a/src/types/api/webhooks.ts b/src/types/api/webhooks.ts new file mode 100644 index 0000000..fdbe41b --- /dev/null +++ b/src/types/api/webhooks.ts @@ -0,0 +1,31 @@ +export type WebhookType = "email_sending" | "audit_log"; + +export type PayloadFormat = "json" | "jsonlines"; + +export type SendingStream = "transactional" | "bulk"; + +export type WebhookEventType = + | "delivery" + | "soft_bounce" + | "bounce" + | "suspension" + | "unsubscribe" + | "open" + | "spam_complaint" + | "click" + | "reject"; + +export type Webhook = { + id: number; + url: string; + active: boolean; + webhook_type: WebhookType; + payload_format: PayloadFormat; + sending_stream?: SendingStream | null; + domain_id?: number | null; + event_types?: WebhookEventType[]; +}; + +export type ListWebhooksResponse = { + data: Webhook[]; +}; From be336daa78af3eb06542bf370112c22154106655 Mon Sep 17 00:00:00 2001 From: Igor Dobryn Date: Thu, 30 Apr 2026 11:53:55 +0300 Subject: [PATCH 08/14] Add WebhooksBaseAPI#create --- src/__tests__/lib/api/Webhooks.test.ts | 1 + .../lib/api/resources/Webhooks.test.ts | 56 +++++++++++++++++++ src/lib/api/Webhooks.ts | 3 + src/lib/api/resources/Webhooks.ts | 23 +++++++- src/types/api/webhooks.ts | 18 ++++++ 5 files changed, 99 insertions(+), 2 deletions(-) diff --git a/src/__tests__/lib/api/Webhooks.test.ts b/src/__tests__/lib/api/Webhooks.test.ts index 8ff1fb8..31a2a38 100644 --- a/src/__tests__/lib/api/Webhooks.test.ts +++ b/src/__tests__/lib/api/Webhooks.test.ts @@ -10,6 +10,7 @@ describe("lib/api/Webhooks: ", () => { describe("init: ", () => { it("initializes with all necessary params.", () => { expect(webhooksAPI).toHaveProperty("getList"); + expect(webhooksAPI).toHaveProperty("create"); }); }); }); diff --git a/src/__tests__/lib/api/resources/Webhooks.test.ts b/src/__tests__/lib/api/resources/Webhooks.test.ts index 5f3c6a1..119e402 100644 --- a/src/__tests__/lib/api/resources/Webhooks.test.ts +++ b/src/__tests__/lib/api/resources/Webhooks.test.ts @@ -19,6 +19,7 @@ describe("lib/api/resources/Webhooks: ", () => { describe("init: ", () => { it("initializes with all necessary params.", () => { expect(webhooksAPI).toHaveProperty("getList"); + expect(webhooksAPI).toHaveProperty("create"); }); }); }); @@ -89,4 +90,59 @@ describe("lib/api/resources/Webhooks: ", () => { } }); }); + + describe("create(): ", () => { + const params = { + url: "https://example.com/mailtrap/webhooks", + webhook_type: "email_sending" as const, + payload_format: "json" as const, + sending_stream: "transactional" as const, + event_types: ["delivery" as const, "bounce" as const], + domain_id: 435, + }; + + const responseData = { + data: { + id: 1, + url: "https://example.com/mailtrap/webhooks", + active: true, + webhook_type: "email_sending", + payload_format: "json", + sending_stream: "transactional", + domain_id: 435, + event_types: ["delivery", "bounce"], + signing_secret: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", + }, + }; + + it("creates a webhook and returns the signing_secret.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/webhooks`; + const expectedBody = { webhook: params }; + + expect.assertions(3); + + mock.onPost(endpoint, expectedBody).reply(200, responseData); + const result = await webhooksAPI.create(params); + + expect(mock.history.post[0].url).toEqual(endpoint); + expect(JSON.parse(mock.history.post[0].data)).toEqual(expectedBody); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await webhooksAPI.create(params); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); }); diff --git a/src/lib/api/Webhooks.ts b/src/lib/api/Webhooks.ts index 2bc4e9e..0df07c6 100644 --- a/src/lib/api/Webhooks.ts +++ b/src/lib/api/Webhooks.ts @@ -7,9 +7,12 @@ export default class WebhooksBaseAPI { public getList: WebhooksApi["getList"]; + public create: WebhooksApi["create"]; + constructor(client: AxiosInstance, accountId: number) { this.client = client; const webhooks = new WebhooksApi(this.client, accountId); this.getList = webhooks.getList.bind(webhooks); + this.create = webhooks.create.bind(webhooks); } } diff --git a/src/lib/api/resources/Webhooks.ts b/src/lib/api/resources/Webhooks.ts index 398e62d..ad8802a 100644 --- a/src/lib/api/resources/Webhooks.ts +++ b/src/lib/api/resources/Webhooks.ts @@ -1,7 +1,11 @@ -+import { AxiosInstance } from "axios"; +import { AxiosInstance } from "axios"; import CONFIG from "../../../config"; -import { ListWebhooksResponse } from "../../../types/api/webhooks"; +import { + CreateWebhookParams, + CreateWebhookResponse, + ListWebhooksResponse, +} from "../../../types/api/webhooks"; const { CLIENT_SETTINGS } = CONFIG; const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; @@ -24,4 +28,19 @@ export default class WebhooksApi { return this.client.get(url); } + + /** + * Create a new webhook for the account. The response includes a + * `signing_secret` that is used to verify webhook payload signatures — + * it is only returned on creation, store it securely. + */ + public async create(params: CreateWebhookParams) { + const url = this.webhooksURL; + const data = { webhook: params }; + + return this.client.post( + url, + data + ); + } } diff --git a/src/types/api/webhooks.ts b/src/types/api/webhooks.ts index fdbe41b..9d65c3a 100644 --- a/src/types/api/webhooks.ts +++ b/src/types/api/webhooks.ts @@ -29,3 +29,21 @@ export type Webhook = { export type ListWebhooksResponse = { data: Webhook[]; }; + +export type CreateWebhookParams = { + url: string; + webhook_type: WebhookType; + active?: boolean; + payload_format?: PayloadFormat; + sending_stream?: SendingStream; + event_types?: WebhookEventType[]; + domain_id?: number; +}; + +export type WebhookWithSigningSecret = Webhook & { + signing_secret: string; +}; + +export type CreateWebhookResponse = { + data: WebhookWithSigningSecret; +}; From 81b895cb949b9640b7a375dbb823cf8b1bb80e91 Mon Sep 17 00:00:00 2001 From: Igor Dobryn Date: Thu, 30 Apr 2026 12:00:08 +0300 Subject: [PATCH 09/14] Add WebhooksBaseAPI#get --- src/__tests__/lib/api/Webhooks.test.ts | 1 + .../lib/api/resources/Webhooks.test.ts | 45 +++++++++++++++++++ src/lib/api/Webhooks.ts | 3 ++ src/lib/api/resources/Webhooks.ts | 11 +++++ src/types/api/webhooks.ts | 4 ++ 5 files changed, 64 insertions(+) diff --git a/src/__tests__/lib/api/Webhooks.test.ts b/src/__tests__/lib/api/Webhooks.test.ts index 31a2a38..8d0df50 100644 --- a/src/__tests__/lib/api/Webhooks.test.ts +++ b/src/__tests__/lib/api/Webhooks.test.ts @@ -11,6 +11,7 @@ describe("lib/api/Webhooks: ", () => { it("initializes with all necessary params.", () => { expect(webhooksAPI).toHaveProperty("getList"); expect(webhooksAPI).toHaveProperty("create"); + expect(webhooksAPI).toHaveProperty("get"); }); }); }); diff --git a/src/__tests__/lib/api/resources/Webhooks.test.ts b/src/__tests__/lib/api/resources/Webhooks.test.ts index 119e402..050f44c 100644 --- a/src/__tests__/lib/api/resources/Webhooks.test.ts +++ b/src/__tests__/lib/api/resources/Webhooks.test.ts @@ -20,6 +20,7 @@ describe("lib/api/resources/Webhooks: ", () => { it("initializes with all necessary params.", () => { expect(webhooksAPI).toHaveProperty("getList"); expect(webhooksAPI).toHaveProperty("create"); + expect(webhooksAPI).toHaveProperty("get"); }); }); }); @@ -145,4 +146,48 @@ describe("lib/api/resources/Webhooks: ", () => { } }); }); + + describe("get(): ", () => { + const webhookId = 1; + const responseData = { + data: { + id: webhookId, + url: "https://example.com/mailtrap/webhooks", + active: true, + webhook_type: "email_sending", + payload_format: "json", + sending_stream: "transactional", + domain_id: 435, + event_types: ["delivery", "bounce"], + }, + }; + + it("gets a webhook by id.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/webhooks/${webhookId}`; + + expect.assertions(2); + + mock.onGet(endpoint).reply(200, responseData); + const result = await webhooksAPI.get(webhookId); + + expect(mock.history.get[0].url).toEqual(endpoint); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await webhooksAPI.get(webhookId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); }); diff --git a/src/lib/api/Webhooks.ts b/src/lib/api/Webhooks.ts index 0df07c6..3392e40 100644 --- a/src/lib/api/Webhooks.ts +++ b/src/lib/api/Webhooks.ts @@ -9,10 +9,13 @@ export default class WebhooksBaseAPI { public create: WebhooksApi["create"]; + public get: WebhooksApi["get"]; + constructor(client: AxiosInstance, accountId: number) { this.client = client; const webhooks = new WebhooksApi(this.client, accountId); this.getList = webhooks.getList.bind(webhooks); this.create = webhooks.create.bind(webhooks); + this.get = webhooks.get.bind(webhooks); } } diff --git a/src/lib/api/resources/Webhooks.ts b/src/lib/api/resources/Webhooks.ts index ad8802a..b8fd7ab 100644 --- a/src/lib/api/resources/Webhooks.ts +++ b/src/lib/api/resources/Webhooks.ts @@ -4,6 +4,7 @@ import CONFIG from "../../../config"; import { CreateWebhookParams, CreateWebhookResponse, + GetWebhookResponse, ListWebhooksResponse, } from "../../../types/api/webhooks"; @@ -43,4 +44,14 @@ export default class WebhooksApi { data ); } + + /** + * Get a single webhook by ID. The `signing_secret` is not returned here — + * it is only available in the create response. + */ + public async get(id: number) { + const url = `${this.webhooksURL}/${id}`; + + return this.client.get(url); + } } diff --git a/src/types/api/webhooks.ts b/src/types/api/webhooks.ts index 9d65c3a..9b4c84d 100644 --- a/src/types/api/webhooks.ts +++ b/src/types/api/webhooks.ts @@ -30,6 +30,10 @@ export type ListWebhooksResponse = { data: Webhook[]; }; +export type GetWebhookResponse = { + data: Webhook; +}; + export type CreateWebhookParams = { url: string; webhook_type: WebhookType; From c5dae9dbdfd150d84767638847926334c1bc541c Mon Sep 17 00:00:00 2001 From: Igor Dobryn Date: Thu, 30 Apr 2026 12:03:07 +0300 Subject: [PATCH 10/14] Add WebhooksBaseAPI#update --- src/__tests__/lib/api/Webhooks.test.ts | 1 + .../lib/api/resources/Webhooks.test.ts | 56 +++++++++++++++++++ src/lib/api/Webhooks.ts | 3 + src/lib/api/resources/Webhooks.ts | 17 ++++++ src/types/api/webhooks.ts | 11 ++++ 5 files changed, 88 insertions(+) diff --git a/src/__tests__/lib/api/Webhooks.test.ts b/src/__tests__/lib/api/Webhooks.test.ts index 8d0df50..47edd10 100644 --- a/src/__tests__/lib/api/Webhooks.test.ts +++ b/src/__tests__/lib/api/Webhooks.test.ts @@ -12,6 +12,7 @@ describe("lib/api/Webhooks: ", () => { expect(webhooksAPI).toHaveProperty("getList"); expect(webhooksAPI).toHaveProperty("create"); expect(webhooksAPI).toHaveProperty("get"); + expect(webhooksAPI).toHaveProperty("update"); }); }); }); diff --git a/src/__tests__/lib/api/resources/Webhooks.test.ts b/src/__tests__/lib/api/resources/Webhooks.test.ts index 050f44c..68dfe79 100644 --- a/src/__tests__/lib/api/resources/Webhooks.test.ts +++ b/src/__tests__/lib/api/resources/Webhooks.test.ts @@ -21,6 +21,7 @@ describe("lib/api/resources/Webhooks: ", () => { expect(webhooksAPI).toHaveProperty("getList"); expect(webhooksAPI).toHaveProperty("create"); expect(webhooksAPI).toHaveProperty("get"); + expect(webhooksAPI).toHaveProperty("update"); }); }); }); @@ -190,4 +191,59 @@ describe("lib/api/resources/Webhooks: ", () => { } }); }); + + describe("update(): ", () => { + const webhookId = 1; + const params = { + active: false, + event_types: [ + "delivery" as const, + "bounce" as const, + "unsubscribe" as const, + ], + }; + + const responseData = { + data: { + id: webhookId, + url: "https://example.com/mailtrap/webhooks", + active: false, + webhook_type: "email_sending", + payload_format: "json", + sending_stream: "transactional", + domain_id: 435, + event_types: ["delivery", "bounce", "unsubscribe"], + }, + }; + + it("updates a webhook.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/webhooks/${webhookId}`; + const expectedBody = { webhook: params }; + + expect.assertions(3); + + mock.onPatch(endpoint, expectedBody).reply(200, responseData); + const result = await webhooksAPI.update(webhookId, params); + + expect(mock.history.patch[0].url).toEqual(endpoint); + expect(JSON.parse(mock.history.patch[0].data)).toEqual(expectedBody); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await webhooksAPI.update(webhookId, params); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); }); diff --git a/src/lib/api/Webhooks.ts b/src/lib/api/Webhooks.ts index 3392e40..8573e1b 100644 --- a/src/lib/api/Webhooks.ts +++ b/src/lib/api/Webhooks.ts @@ -11,11 +11,14 @@ export default class WebhooksBaseAPI { public get: WebhooksApi["get"]; + public update: WebhooksApi["update"]; + constructor(client: AxiosInstance, accountId: number) { this.client = client; const webhooks = new WebhooksApi(this.client, accountId); this.getList = webhooks.getList.bind(webhooks); this.create = webhooks.create.bind(webhooks); this.get = webhooks.get.bind(webhooks); + this.update = webhooks.update.bind(webhooks); } } diff --git a/src/lib/api/resources/Webhooks.ts b/src/lib/api/resources/Webhooks.ts index b8fd7ab..0d8c5ce 100644 --- a/src/lib/api/resources/Webhooks.ts +++ b/src/lib/api/resources/Webhooks.ts @@ -6,6 +6,8 @@ import { CreateWebhookResponse, GetWebhookResponse, ListWebhooksResponse, + UpdateWebhookParams, + UpdateWebhookResponse, } from "../../../types/api/webhooks"; const { CLIENT_SETTINGS } = CONFIG; @@ -54,4 +56,19 @@ export default class WebhooksApi { return this.client.get(url); } + + /** + * Update an existing webhook. Only `url`, `active`, `payload_format`, and + * `event_types` can be changed; `webhook_type`, `sending_stream`, and + * `domain_id` are immutable after creation. + */ + public async update(id: number, params: UpdateWebhookParams) { + const url = `${this.webhooksURL}/${id}`; + const data = { webhook: params }; + + return this.client.patch( + url, + data + ); + } } diff --git a/src/types/api/webhooks.ts b/src/types/api/webhooks.ts index 9b4c84d..d69af49 100644 --- a/src/types/api/webhooks.ts +++ b/src/types/api/webhooks.ts @@ -51,3 +51,14 @@ export type WebhookWithSigningSecret = Webhook & { export type CreateWebhookResponse = { data: WebhookWithSigningSecret; }; + +export type UpdateWebhookParams = { + url?: string; + active?: boolean; + payload_format?: PayloadFormat; + event_types?: WebhookEventType[]; +}; + +export type UpdateWebhookResponse = { + data: Webhook; +}; From bb486eeef8209c915a2e133f357d3d7aa6a34b3b Mon Sep 17 00:00:00 2001 From: Igor Dobryn Date: Thu, 30 Apr 2026 12:07:56 +0300 Subject: [PATCH 11/14] Add WebhooksBaseAPI#delete --- src/__tests__/lib/api/Webhooks.test.ts | 1 + .../lib/api/resources/Webhooks.test.ts | 42 +++++++++++++++++++ src/lib/api/Webhooks.ts | 3 ++ src/lib/api/resources/Webhooks.ts | 13 ++++++ src/types/api/webhooks.ts | 4 ++ 5 files changed, 63 insertions(+) diff --git a/src/__tests__/lib/api/Webhooks.test.ts b/src/__tests__/lib/api/Webhooks.test.ts index 47edd10..52d248a 100644 --- a/src/__tests__/lib/api/Webhooks.test.ts +++ b/src/__tests__/lib/api/Webhooks.test.ts @@ -13,6 +13,7 @@ describe("lib/api/Webhooks: ", () => { expect(webhooksAPI).toHaveProperty("create"); expect(webhooksAPI).toHaveProperty("get"); expect(webhooksAPI).toHaveProperty("update"); + expect(webhooksAPI).toHaveProperty("delete"); }); }); }); diff --git a/src/__tests__/lib/api/resources/Webhooks.test.ts b/src/__tests__/lib/api/resources/Webhooks.test.ts index 68dfe79..6af83f6 100644 --- a/src/__tests__/lib/api/resources/Webhooks.test.ts +++ b/src/__tests__/lib/api/resources/Webhooks.test.ts @@ -22,6 +22,7 @@ describe("lib/api/resources/Webhooks: ", () => { expect(webhooksAPI).toHaveProperty("create"); expect(webhooksAPI).toHaveProperty("get"); expect(webhooksAPI).toHaveProperty("update"); + expect(webhooksAPI).toHaveProperty("delete"); }); }); }); @@ -246,4 +247,45 @@ describe("lib/api/resources/Webhooks: ", () => { } }); }); + + describe("delete(): ", () => { + const webhookId = 1; + const responseData = { + data: { + id: webhookId, + url: "https://example.com/mailtrap/webhooks", + active: true, + webhook_type: "audit_log", + payload_format: "json", + }, + }; + + it("deletes a webhook and returns it.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/webhooks/${webhookId}`; + + expect.assertions(2); + + mock.onDelete(endpoint).reply(200, responseData); + const result = await webhooksAPI.delete(webhookId); + + expect(mock.history.delete[0].url).toEqual(endpoint); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await webhooksAPI.delete(webhookId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); }); diff --git a/src/lib/api/Webhooks.ts b/src/lib/api/Webhooks.ts index 8573e1b..8a6be01 100644 --- a/src/lib/api/Webhooks.ts +++ b/src/lib/api/Webhooks.ts @@ -13,6 +13,8 @@ export default class WebhooksBaseAPI { public update: WebhooksApi["update"]; + public delete: WebhooksApi["delete"]; + constructor(client: AxiosInstance, accountId: number) { this.client = client; const webhooks = new WebhooksApi(this.client, accountId); @@ -20,5 +22,6 @@ export default class WebhooksBaseAPI { this.create = webhooks.create.bind(webhooks); this.get = webhooks.get.bind(webhooks); this.update = webhooks.update.bind(webhooks); + this.delete = webhooks.delete.bind(webhooks); } } diff --git a/src/lib/api/resources/Webhooks.ts b/src/lib/api/resources/Webhooks.ts index 0d8c5ce..d636a65 100644 --- a/src/lib/api/resources/Webhooks.ts +++ b/src/lib/api/resources/Webhooks.ts @@ -4,6 +4,7 @@ import CONFIG from "../../../config"; import { CreateWebhookParams, CreateWebhookResponse, + DeleteWebhookResponse, GetWebhookResponse, ListWebhooksResponse, UpdateWebhookParams, @@ -71,4 +72,16 @@ export default class WebhooksApi { data ); } + + /** + * Permanently delete a webhook by ID. The deleted webhook is returned in + * the response. + */ + public async delete(id: number) { + const url = `${this.webhooksURL}/${id}`; + + return this.client.delete( + url + ); + } } diff --git a/src/types/api/webhooks.ts b/src/types/api/webhooks.ts index d69af49..d8a1919 100644 --- a/src/types/api/webhooks.ts +++ b/src/types/api/webhooks.ts @@ -62,3 +62,7 @@ export type UpdateWebhookParams = { export type UpdateWebhookResponse = { data: Webhook; }; + +export type DeleteWebhookResponse = { + data: Webhook; +}; From e2e853018da5360df3e9c2c7122e9fe14dd71d29 Mon Sep 17 00:00:00 2001 From: Igor Dobryn Date: Thu, 30 Apr 2026 12:37:26 +0300 Subject: [PATCH 12/14] Add SubAccountsApi#getList --- src/__tests__/lib/api/Organizations.test.ts | 17 +++++ .../lib/api/resources/SubAccounts.test.ts | 75 +++++++++++++++++++ src/config/index.ts | 2 + src/lib/MailtrapClient.ts | 31 +++++++- src/lib/api/Organizations.ts | 11 +++ src/lib/api/resources/SubAccounts.ts | 28 +++++++ src/types/api/sub-accounts.ts | 4 + src/types/mailtrap.ts | 1 + 8 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/lib/api/Organizations.test.ts create mode 100644 src/__tests__/lib/api/resources/SubAccounts.test.ts create mode 100644 src/lib/api/Organizations.ts create mode 100644 src/lib/api/resources/SubAccounts.ts create mode 100644 src/types/api/sub-accounts.ts diff --git a/src/__tests__/lib/api/Organizations.test.ts b/src/__tests__/lib/api/Organizations.test.ts new file mode 100644 index 0000000..f971c3e --- /dev/null +++ b/src/__tests__/lib/api/Organizations.test.ts @@ -0,0 +1,17 @@ +import axios from "axios"; + +import OrganizationsBaseAPI from "../../../lib/api/Organizations"; + +describe("lib/api/Organizations: ", () => { + const organizationId = 1001; + const organizationsAPI = new OrganizationsBaseAPI(axios, organizationId); + + describe("class OrganizationsBaseAPI(): ", () => { + describe("init: ", () => { + it("exposes subAccounts resource.", () => { + expect(organizationsAPI).toHaveProperty("subAccounts"); + expect(typeof organizationsAPI.subAccounts.getList).toBe("function"); + }); + }); + }); +}); diff --git a/src/__tests__/lib/api/resources/SubAccounts.test.ts b/src/__tests__/lib/api/resources/SubAccounts.test.ts new file mode 100644 index 0000000..24599b1 --- /dev/null +++ b/src/__tests__/lib/api/resources/SubAccounts.test.ts @@ -0,0 +1,75 @@ +import axios from "axios"; +import AxiosMockAdapter from "axios-mock-adapter"; + +import SubAccountsApi from "../../../../lib/api/resources/SubAccounts"; +import handleSendingError from "../../../../lib/axios-logger"; +import MailtrapError from "../../../../lib/MailtrapError"; + +import CONFIG from "../../../../config"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +describe("lib/api/resources/SubAccounts: ", () => { + let mock: AxiosMockAdapter; + const organizationId = 1001; + const subAccountsAPI = new SubAccountsApi(axios, organizationId); + + describe("class SubAccountsApi(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(subAccountsAPI).toHaveProperty("getList"); + }); + }); + }); + + beforeAll(() => { + /** + * Init Axios interceptors for handling response.data, errors. + */ + axios.interceptors.response.use( + (response) => response.data, + handleSendingError + ); + mock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + mock.reset(); + }); + + describe("getList(): ", () => { + const responseData = [ + { id: 12345, name: "Development Team Account" }, + { id: 12346, name: "QA Team Account" }, + ]; + + it("gets the list of sub accounts.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/organizations/${organizationId}/sub_accounts`; + + expect.assertions(2); + + mock.onGet(endpoint).reply(200, responseData); + const result = await subAccountsAPI.getList(); + + expect(mock.history.get[0].url).toEqual(endpoint); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await subAccountsAPI.getList(); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); +}); diff --git a/src/config/index.ts b/src/config/index.ts index e03712b..6f5d0e1 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -9,6 +9,8 @@ export default { TEST_INBOX_ID_MISSING: "testInboxId is missing, testing API will not work.", ACCOUNT_ID_MISSING: "accountId is missing, please provide a valid accountId.", + ORGANIZATION_ID_MISSING: + "organizationId is missing, please provide a valid organizationId.", BULK_SANDBOX_INCOMPATIBLE: "Bulk mode is not applicable for sandbox API.", }, CLIENT_SETTINGS: { diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index 8c1c0fe..9428b5a 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -18,6 +18,7 @@ import GeneralAPI from "./api/General"; import SendingDomainsBaseAPI from "./api/SendingDomains"; import StatsBaseAPI from "./api/Stats"; import SuppressionsBaseAPI from "./api/Suppressions"; +import OrganizationsBaseAPI from "./api/Organizations"; import TemplatesBaseAPI from "./api/Templates"; import TestingAPI from "./api/Testing"; import WebhooksBaseAPI from "./api/Webhooks"; @@ -41,8 +42,12 @@ const { BULK_ENDPOINT, USER_AGENT, } = CLIENT_SETTINGS; -const { ACCOUNT_ID_MISSING, BULK_SANDBOX_INCOMPATIBLE, TEST_INBOX_ID_MISSING } = - ERRORS; +const { + ACCOUNT_ID_MISSING, + BULK_SANDBOX_INCOMPATIBLE, + ORGANIZATION_ID_MISSING, + TEST_INBOX_ID_MISSING, +} = ERRORS; /** * Mailtrap client class. Initializes instance with available methods. @@ -54,6 +59,8 @@ export default class MailtrapClient { private accountId?: number; + private organizationId?: number; + private bulk: boolean; private sandbox: boolean; @@ -65,6 +72,7 @@ export default class MailtrapClient { token, testInboxId, accountId, + organizationId, bulk = false, sandbox = false, userAgent, @@ -89,6 +97,7 @@ export default class MailtrapClient { this.testInboxId = testInboxId; this.accountId = accountId; + this.organizationId = organizationId; this.bulk = bulk; this.sandbox = sandbox; } @@ -103,6 +112,16 @@ export default class MailtrapClient { return this.accountId; } + /** + * Validates that organization ID is present, throws MailtrapError if missing. + */ + private validateOrganizationIdPresence(): number { + if (!this.organizationId) { + throw new MailtrapError(ORGANIZATION_ID_MISSING); + } + return this.organizationId; + } + /** * Validates that test inbox ID is present, throws MailtrapError if missing. */ @@ -224,6 +243,14 @@ export default class MailtrapClient { return new WebhooksBaseAPI(this.axios, accountId); } + /** + * Getter for Organizations API. Requires `organizationId` in config. + */ + get organizations() { + const organizationId = this.validateOrganizationIdPresence(); + return new OrganizationsBaseAPI(this.axios, organizationId); + } + /** * Returns configured host. Checks if `bulk` and `sandbox` modes are activated simultaneously, * then reject with Mailtrap Error. diff --git a/src/lib/api/Organizations.ts b/src/lib/api/Organizations.ts new file mode 100644 index 0000000..b0a4884 --- /dev/null +++ b/src/lib/api/Organizations.ts @@ -0,0 +1,11 @@ +import { AxiosInstance } from "axios"; + +import SubAccountsApi from "./resources/SubAccounts"; + +export default class OrganizationsBaseAPI { + public subAccounts: SubAccountsApi; + + constructor(client: AxiosInstance, organizationId: number) { + this.subAccounts = new SubAccountsApi(client, organizationId); + } +} diff --git a/src/lib/api/resources/SubAccounts.ts b/src/lib/api/resources/SubAccounts.ts new file mode 100644 index 0000000..4a23dfc --- /dev/null +++ b/src/lib/api/resources/SubAccounts.ts @@ -0,0 +1,28 @@ +import { AxiosInstance } from "axios"; + +import CONFIG from "../../../config"; +import { SubAccount } from "../../../types/api/sub-accounts"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +export default class SubAccountsApi { + private client: AxiosInstance; + + private subAccountsURL: string; + + constructor(client: AxiosInstance, organizationId: number) { + this.client = client; + this.subAccountsURL = `${GENERAL_ENDPOINT}/api/organizations/${organizationId}/sub_accounts`; + } + + /** + * Get a list of sub accounts for the organization. Requires sub-account + * management permissions. + */ + public async getList() { + const url = this.subAccountsURL; + + return this.client.get(url); + } +} diff --git a/src/types/api/sub-accounts.ts b/src/types/api/sub-accounts.ts new file mode 100644 index 0000000..c36dbe3 --- /dev/null +++ b/src/types/api/sub-accounts.ts @@ -0,0 +1,4 @@ +export type SubAccount = { + id: number; + name: string; +}; diff --git a/src/types/mailtrap.ts b/src/types/mailtrap.ts index 649c25d..ecce670 100644 --- a/src/types/mailtrap.ts +++ b/src/types/mailtrap.ts @@ -72,6 +72,7 @@ export type MailtrapClientConfig = { token: string; testInboxId?: number; accountId?: number; + organizationId?: number; bulk?: boolean; sandbox?: boolean; userAgent?: string; From aee33d15fc121f21b46e39da3caa1980ae0ef2eb Mon Sep 17 00:00:00 2001 From: Igor Dobryn Date: Thu, 30 Apr 2026 12:42:30 +0300 Subject: [PATCH 13/14] Add SubAccountsApi#create --- src/__tests__/lib/api/Organizations.test.ts | 1 + .../lib/api/resources/SubAccounts.test.ts | 36 +++++++++++++++++++ src/lib/api/resources/SubAccounts.ts | 16 ++++++++- src/types/api/sub-accounts.ts | 4 +++ 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/__tests__/lib/api/Organizations.test.ts b/src/__tests__/lib/api/Organizations.test.ts index f971c3e..c32a445 100644 --- a/src/__tests__/lib/api/Organizations.test.ts +++ b/src/__tests__/lib/api/Organizations.test.ts @@ -11,6 +11,7 @@ describe("lib/api/Organizations: ", () => { it("exposes subAccounts resource.", () => { expect(organizationsAPI).toHaveProperty("subAccounts"); expect(typeof organizationsAPI.subAccounts.getList).toBe("function"); + expect(typeof organizationsAPI.subAccounts.create).toBe("function"); }); }); }); diff --git a/src/__tests__/lib/api/resources/SubAccounts.test.ts b/src/__tests__/lib/api/resources/SubAccounts.test.ts index 24599b1..00bdddb 100644 --- a/src/__tests__/lib/api/resources/SubAccounts.test.ts +++ b/src/__tests__/lib/api/resources/SubAccounts.test.ts @@ -19,6 +19,7 @@ describe("lib/api/resources/SubAccounts: ", () => { describe("init: ", () => { it("initializes with all necessary params.", () => { expect(subAccountsAPI).toHaveProperty("getList"); + expect(subAccountsAPI).toHaveProperty("create"); }); }); }); @@ -72,4 +73,39 @@ describe("lib/api/resources/SubAccounts: ", () => { } }); }); + + describe("create(): ", () => { + const params = { name: "New Team Account" }; + const responseData = { id: 12347, name: "New Team Account" }; + + it("creates a sub account.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/organizations/${organizationId}/sub_accounts`; + const expectedBody = { account: params }; + + expect.assertions(3); + + mock.onPost(endpoint, expectedBody).reply(200, responseData); + const result = await subAccountsAPI.create(params); + + expect(mock.history.post[0].url).toEqual(endpoint); + expect(JSON.parse(mock.history.post[0].data)).toEqual(expectedBody); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await subAccountsAPI.create(params); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); }); diff --git a/src/lib/api/resources/SubAccounts.ts b/src/lib/api/resources/SubAccounts.ts index 4a23dfc..da5bb57 100644 --- a/src/lib/api/resources/SubAccounts.ts +++ b/src/lib/api/resources/SubAccounts.ts @@ -1,7 +1,10 @@ import { AxiosInstance } from "axios"; import CONFIG from "../../../config"; -import { SubAccount } from "../../../types/api/sub-accounts"; +import { + CreateSubAccountParams, + SubAccount, +} from "../../../types/api/sub-accounts"; const { CLIENT_SETTINGS } = CONFIG; const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; @@ -25,4 +28,15 @@ export default class SubAccountsApi { return this.client.get(url); } + + /** + * Create a new sub account under the organization. Requires sub-account + * management permissions. + */ + public async create(params: CreateSubAccountParams) { + const url = this.subAccountsURL; + const data = { account: params }; + + return this.client.post(url, data); + } } diff --git a/src/types/api/sub-accounts.ts b/src/types/api/sub-accounts.ts index c36dbe3..83e7a7d 100644 --- a/src/types/api/sub-accounts.ts +++ b/src/types/api/sub-accounts.ts @@ -2,3 +2,7 @@ export type SubAccount = { id: number; name: string; }; + +export type CreateSubAccountParams = { + name: string; +}; From 35d7ee4bef2cdaf80493c3f3f97a14c331f56c18 Mon Sep 17 00:00:00 2001 From: Igor Dobryn Date: Thu, 30 Apr 2026 12:44:43 +0300 Subject: [PATCH 14/14] Add .envrc with env variable refs for running api calls --- .envrc | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .envrc diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..8b808bb --- /dev/null +++ b/.envrc @@ -0,0 +1,4 @@ +export MAILTRAP_ACCOUNT_ID="op://Mailtrap Dev/Mailtrap SDK Dev API Key/account_id" +export MAILTRAP_ORGANIZATION_ID="op://Mailtrap Dev/Mailtrap SDK Dev API Key/organization_id" +export MAILTRAP_API_KEY="op://Mailtrap Dev/Mailtrap SDK Dev API Key/account_api_token" +export MAILTRAP_ORGANIZATION_API_KEY="op://Mailtrap Dev/Mailtrap SDK Dev API Key/organization_api_token"