From 142c67ace7846ce2d8f14d2f27c6039f703628fc Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 3 Nov 2025 18:02:51 +0400 Subject: [PATCH 01/14] examples: add example for creating contact events with Mailtrap API --- examples/contact-events/everything.ts | 58 +++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 examples/contact-events/everything.ts diff --git a/examples/contact-events/everything.ts b/examples/contact-events/everything.ts new file mode 100644 index 0000000..f6c8cc4 --- /dev/null +++ b/examples/contact-events/everything.ts @@ -0,0 +1,58 @@ +import { MailtrapClient } from "mailtrap"; + +const TOKEN = ""; +const ACCOUNT_ID = ""; + +const client = new MailtrapClient({ + token: TOKEN, + accountId: ACCOUNT_ID, +}); + +async function createContactEvent() { + try { + const email = `john.smith+${Date.now()}@example.com`; + let contactId: string; + // Try to get existing first + try { + const existing = await client.contacts.get(email); + contactId = existing.data.id; + } catch (_getErr) { + // Not found, create minimal contact + try { + const created = await client.contacts.create({ email }); + contactId = created.data.id; + } catch (err: any) { + const cause = err?.cause || err; + const status = cause?.response?.status; + if (status === 409) { + const existing = await client.contacts.get(email); + contactId = existing.data.id; + } else { + throw err; + } + } + } + + const payload = { + name: "purchase_completed", + params: { + order_id: 12345, + amount: 49.99, + currency: "USD", + coupon_used: false, + }, + }; + + const event = await client.contactEvents.create(contactId, payload); + console.log("Contact event created:", JSON.stringify(event, null, 2)); + } catch (error) { + console.error( + "Error creating contact event:", + error instanceof Error ? error.message : String(error) + ); + } +} + +createContactEvent(); + + From ef58d1a9e244e75a96853abcc3bf22a6f2811209 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 3 Nov 2025 18:03:01 +0400 Subject: [PATCH 02/14] lib: add getter for Contact Events API in MailtrapClient --- src/lib/MailtrapClient.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index baa6bb2..6a9ece1 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -10,6 +10,7 @@ import MailtrapError from "./MailtrapError"; import GeneralAPI from "./api/General"; import TestingAPI from "./api/Testing"; import ContactsBaseAPI from "./api/Contacts"; +import ContactEventsBaseAPI from "./api/ContactEvents"; import ContactListsBaseAPI from "./api/ContactLists"; import ContactFieldsBaseAPI from "./api/ContactFields"; import TemplatesBaseAPI from "./api/Templates"; @@ -130,6 +131,14 @@ export default class MailtrapClient { return new ContactsBaseAPI(this.axios, accountId); } + /** + * Getter for Contact Events API. + */ + get contactEvents() { + const accountId = this.validateAccountIdPresence(); + return new ContactEventsBaseAPI(this.axios, accountId); + } + /** * Getter for Contact Lists API. */ From 74376918b6fbf82c8a556341c435213281612ab0 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 3 Nov 2025 18:03:16 +0400 Subject: [PATCH 03/14] api: implement ContactFieldsBaseAPI to facilitate creation of contact events --- src/lib/api/ContactEvents.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/lib/api/ContactEvents.ts diff --git a/src/lib/api/ContactEvents.ts b/src/lib/api/ContactEvents.ts new file mode 100644 index 0000000..a061b28 --- /dev/null +++ b/src/lib/api/ContactEvents.ts @@ -0,0 +1,12 @@ +import { AxiosInstance } from "axios"; + +import ContactEventsApi from "./resources/ContactEvents"; + +export default class ContactFieldsBaseAPI { + public create: ContactEventsApi["create"]; + + constructor(client: AxiosInstance, accountId: number) { + const contactEvents = new ContactEventsApi(client, accountId); + this.create = contactEvents.create.bind(contactEvents); + } +} From e76c7480704f54534e4ff645cd0948fe7a54ffc8 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 3 Nov 2025 18:03:24 +0400 Subject: [PATCH 04/14] api: add ContactEventsApi class for managing contact events --- src/lib/api/resources/ContactEvents.ts | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/lib/api/resources/ContactEvents.ts diff --git a/src/lib/api/resources/ContactEvents.ts b/src/lib/api/resources/ContactEvents.ts new file mode 100644 index 0000000..3447062 --- /dev/null +++ b/src/lib/api/resources/ContactEvents.ts @@ -0,0 +1,36 @@ +import { AxiosInstance } from "axios"; + +import CONFIG from "../../../config"; +import { + ContactEventOptions, + ContactEventResponse, +} from "../../../types/api/contact-events"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +export default class ContactEventsApi { + private client: AxiosInstance; + + private contactEventsURL: string; + + constructor(client: AxiosInstance, accountId: number) { + this.client = client; + this.contactEventsURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/`; + } + + /** + * Creates a new contact event for given contact identifier and data. + */ + public async create( + contactIdentifier: number | string, + data: ContactEventOptions + ) { + const url = `${this.contactEventsURL}/${contactIdentifier}/events`; + + return this.client.post( + url, + data + ); + } +} From 49863b55cb0f931144766def8857b5c5af0f650a Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 3 Nov 2025 18:03:37 +0400 Subject: [PATCH 05/14] types: add ContactEventOptions and ContactEventResponse interfaces for contact event management --- src/types/api/contact-events.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/types/api/contact-events.ts diff --git a/src/types/api/contact-events.ts b/src/types/api/contact-events.ts new file mode 100644 index 0000000..ed5ec02 --- /dev/null +++ b/src/types/api/contact-events.ts @@ -0,0 +1,11 @@ +export interface ContactEventOptions { + name: string; + params: Record; +} + +export interface ContactEventResponse { + contact_id: string; + contact_email: string; + name: string; + params: Record; +} From 050c89c95cc8f252e525466f1ba189dae53c1222 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 3 Nov 2025 18:05:57 +0400 Subject: [PATCH 06/14] docs: update README to include Contact Events API section and example --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index edecd5a..939b5eb 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Currently, with this SDK you can: - Contacts CRUD - Lists CRUD - Contact Fields CRUD + - Contact Events - General - Templates CRUD - Suppressions management (find and delete) @@ -126,6 +127,10 @@ Refer to the [`examples`](examples) folder for the source code of this and other - [Contact Fields](examples/contact-fields/everything.ts) +### Contact Events API + +- [Contact Events](examples/contact-events/everything.ts) + ### Sending API - [Advanced](examples/sending/everything.ts) From a14af693d07c2148728a6b1aa696a9306bcbed56 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Tue, 4 Nov 2025 11:42:31 +0400 Subject: [PATCH 07/14] test: add unit tests for contactEvents getter in MailtrapClient --- src/__tests__/lib/mailtrap-client.test.ts | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/__tests__/lib/mailtrap-client.test.ts b/src/__tests__/lib/mailtrap-client.test.ts index c4123a4..e5440f0 100644 --- a/src/__tests__/lib/mailtrap-client.test.ts +++ b/src/__tests__/lib/mailtrap-client.test.ts @@ -14,6 +14,7 @@ import Contacts from "../../lib/api/Contacts"; import TemplatesBaseAPI from "../../lib/api/Templates"; import SuppressionsBaseAPI from "../../lib/api/Suppressions"; import SendingDomainsBaseAPI from "../../lib/api/SendingDomains"; +import ContactEventsBaseAPI from "../../lib/api/ContactEvents"; const { ERRORS, CLIENT_SETTINGS } = CONFIG; const { TESTING_ENDPOINT, BULK_ENDPOINT, SENDING_ENDPOINT } = CLIENT_SETTINGS; @@ -900,5 +901,31 @@ describe("lib/mailtrap-client: ", () => { expect(sendingDomainsClient).toBeInstanceOf(SendingDomainsBaseAPI); }); }); + + describe("get contactEvents(): ", () => { + it("rejects with Mailtrap error, when `accountId` is missing.", () => { + const client = new MailtrapClient({ + token: "MY_API_TOKEN", + }); + expect.assertions(1); + + try { + client.contactEvents; + } catch (error) { + expect(error).toEqual(new MailtrapError(ACCOUNT_ID_MISSING)); + } + }); + + it("returns contact events API object when accountId is provided.", () => { + const client = new MailtrapClient({ + token: "MY_API_TOKEN", + accountId: 10, + }); + expect.assertions(1); + + const contactEventsClient = client.contactEvents; + expect(contactEventsClient).toBeInstanceOf(ContactEventsBaseAPI); + }); + }); }); }); From 9e49d3fd1dc9f6ddf5556f1e3b334695532d62d6 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Tue, 4 Nov 2025 11:42:37 +0400 Subject: [PATCH 08/14] test: add unit tests for ContactEvents class initialization --- src/__tests__/lib/api/ContactEvents.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/__tests__/lib/api/ContactEvents.test.ts diff --git a/src/__tests__/lib/api/ContactEvents.test.ts b/src/__tests__/lib/api/ContactEvents.test.ts new file mode 100644 index 0000000..6ae53c7 --- /dev/null +++ b/src/__tests__/lib/api/ContactEvents.test.ts @@ -0,0 +1,17 @@ +import axios, { AxiosInstance } from "axios"; + +import ContactEventsBaseAPI from "../../../lib/api/ContactEvents"; + +describe("lib/api/ContactEvents: ", () => { + const axiosInstance: AxiosInstance = axios.create(); + const accountId = 100; + const api = new ContactEventsBaseAPI(axiosInstance, accountId); + + describe("class ContactEvents(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(api).toHaveProperty("create"); + }); + }); + }); +}); From a64da78db2ae4e4b44ce307d060572cb3ee7dc73 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Tue, 4 Nov 2025 11:45:41 +0400 Subject: [PATCH 09/14] test: add unit tests for create method in ContactEventsApi class --- .../lib/api/resources/ContactEvents.test.ts | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/__tests__/lib/api/resources/ContactEvents.test.ts diff --git a/src/__tests__/lib/api/resources/ContactEvents.test.ts b/src/__tests__/lib/api/resources/ContactEvents.test.ts new file mode 100644 index 0000000..f89615a --- /dev/null +++ b/src/__tests__/lib/api/resources/ContactEvents.test.ts @@ -0,0 +1,106 @@ +import axios from "axios"; +import AxiosMockAdapter from "axios-mock-adapter"; + +import ContactEventsApi from "../../../../lib/api/resources/ContactEvents"; +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/ContactEvents: ", () => { + let mock: AxiosMockAdapter; + const accountId = 100; + const contactEventsAPI = new ContactEventsApi(axios, accountId); + + const contactIdentifier = "john.smith@example.com"; + const payload = { + name: "purchase_completed", + params: { + order_id: 12345, + amount: 49.99, + currency: "USD", + coupon_used: false, + }, + }; + + const successResponse = { + contact_id: "019a4a05-5924-7d93-86ce-f79293418083", + contact_email: contactIdentifier, + name: payload.name, + params: payload.params, + }; + + describe("class ContactEventsApi(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(contactEventsAPI).toHaveProperty("create"); + }); + }); + }); + + beforeAll(() => { + axios.interceptors.response.use( + (response) => response.data, + handleSendingError + ); + mock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + mock.reset(); + }); + + describe("create(): ", () => { + it("successfully creates a contact event.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/${contactIdentifier}/events`; + const expectedResponseData = successResponse; + + expect.assertions(2); + + mock.onPost(endpoint, payload).reply(200, expectedResponseData); + const result = await contactEventsAPI.create(contactIdentifier, payload); + + expect(mock.history.post[0].url).toEqual(endpoint); + expect(result).toEqual(expectedResponseData); + }); + + it("fails with error when accountId is invalid.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/${contactIdentifier}/events`; + const expectedErrorMessage = "Account not found"; + + expect.assertions(2); + + mock.onPost(endpoint).reply(404, { error: expectedErrorMessage }); + + try { + await contactEventsAPI.create(contactIdentifier, payload); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + + it("fails with error when payload is invalid.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/${contactIdentifier}/events`; + const expectedErrorMessage = "Invalid event name"; + + expect.assertions(2); + + mock.onPost(endpoint).reply(422, { error: expectedErrorMessage }); + + try { + await contactEventsAPI.create(contactIdentifier, payload); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); +}); From 167a072ecac0fa85a3437ad1b7e53a72758ffb01 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Tue, 4 Nov 2025 11:45:47 +0400 Subject: [PATCH 10/14] refactor: rename ContactFieldsBaseAPI to ContactEventsBaseAPI for clarity --- src/lib/api/ContactEvents.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/api/ContactEvents.ts b/src/lib/api/ContactEvents.ts index a061b28..047a0e8 100644 --- a/src/lib/api/ContactEvents.ts +++ b/src/lib/api/ContactEvents.ts @@ -2,7 +2,7 @@ import { AxiosInstance } from "axios"; import ContactEventsApi from "./resources/ContactEvents"; -export default class ContactFieldsBaseAPI { +export default class ContactEventsBaseAPI { public create: ContactEventsApi["create"]; constructor(client: AxiosInstance, accountId: number) { From 975a54d1db377a3490b7dd155f236a3b97cf2726 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Tue, 4 Nov 2025 11:45:56 +0400 Subject: [PATCH 11/14] refactor: rename contactEventsURL to contactsURL for consistency in ContactEventsApi class --- src/lib/api/resources/ContactEvents.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/api/resources/ContactEvents.ts b/src/lib/api/resources/ContactEvents.ts index 3447062..dcea29a 100644 --- a/src/lib/api/resources/ContactEvents.ts +++ b/src/lib/api/resources/ContactEvents.ts @@ -12,11 +12,11 @@ const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; export default class ContactEventsApi { private client: AxiosInstance; - private contactEventsURL: string; + private contactsURL: string; constructor(client: AxiosInstance, accountId: number) { this.client = client; - this.contactEventsURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/`; + this.contactsURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts`; } /** @@ -26,7 +26,7 @@ export default class ContactEventsApi { contactIdentifier: number | string, data: ContactEventOptions ) { - const url = `${this.contactEventsURL}/${contactIdentifier}/events`; + const url = `${this.contactsURL}/${contactIdentifier}/events`; return this.client.post( url, From ab0a77d5e15aa72fbe801589a440155391cddd18 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Tue, 4 Nov 2025 13:01:53 +0400 Subject: [PATCH 12/14] Update examples/contact-events/everything.ts Co-authored-by: Marcin Klocek --- examples/contact-events/everything.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/contact-events/everything.ts b/examples/contact-events/everything.ts index f6c8cc4..38e2a86 100644 --- a/examples/contact-events/everything.ts +++ b/examples/contact-events/everything.ts @@ -12,7 +12,7 @@ async function createContactEvent() { try { const email = `john.smith+${Date.now()}@example.com`; let contactId: string; - // Try to get existing first + // Try to get existing contact first try { const existing = await client.contacts.get(email); contactId = existing.data.id; From 6465533a4cbe3f8025fed85c076447b4235d6e61 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Tue, 4 Nov 2025 13:02:02 +0400 Subject: [PATCH 13/14] Update examples/contact-events/everything.ts Co-authored-by: Marcin Klocek --- examples/contact-events/everything.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/contact-events/everything.ts b/examples/contact-events/everything.ts index 38e2a86..12f0aef 100644 --- a/examples/contact-events/everything.ts +++ b/examples/contact-events/everything.ts @@ -55,4 +55,3 @@ async function createContactEvent() { createContactEvent(); - From ada5480f6e1d2d4c7650f44cd9c6189b808e7b0d Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Thu, 6 Nov 2025 20:36:31 +0400 Subject: [PATCH 14/14] refactor: remove unused ContactListsBaseAPI import and clean up comments in MailtrapClient --- src/lib/MailtrapClient.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index b52577b..08bf646 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -9,7 +9,6 @@ import MailtrapError from "./MailtrapError"; import ContactsBaseAPI from "./api/Contacts"; import ContactEventsBaseAPI from "./api/ContactEvents"; -import ContactListsBaseAPI from "./api/ContactLists"; import ContactExportsBaseAPI from "./api/ContactExports"; import ContactFieldsBaseAPI from "./api/ContactFields"; import ContactImportsBaseAPI from "./api/ContactImports"; @@ -141,8 +140,8 @@ export default class MailtrapClient { const accountId = this.validateAccountIdPresence(); return new ContactEventsBaseAPI(this.axios, accountId); } - - /** + + /** * Getter for Contact Exports API. */ get contactExports() {