From b244d4806072b2fdce34b6200a5a770f03129dc8 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Sat, 1 Nov 2025 13:05:45 +0400 Subject: [PATCH 1/9] types: add ContactExportResponse and related interfaces for contact export functionality --- src/types/api/contact-exports.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/types/api/contact-exports.ts diff --git a/src/types/api/contact-exports.ts b/src/types/api/contact-exports.ts new file mode 100644 index 0000000..562d537 --- /dev/null +++ b/src/types/api/contact-exports.ts @@ -0,0 +1,23 @@ +export interface ContactExportResponse { + id: number; + status: "started" | "created" | "finished"; + created_at: string; + updated_at: string; + url: string | null; +} + +export interface CreateContactExportFilter { + name: string; // e.g. "list_id", "subscription_status", "email" + operator: + | "equal" + | "not_equal" + | "contains" + | "not_contains" + | "is_empty" + | "is_not_empty"; + value: string | number | boolean | string[] | number[]; +} + +export interface CreateContactExportParams { + filters: CreateContactExportFilter[]; +} From d8125e492c3d0daafc6a25e9d0bf2f8d4ff20ad5 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Sat, 1 Nov 2025 13:06:31 +0400 Subject: [PATCH 2/9] feat: implement ContactExportsApi for managing contact export operations --- src/lib/api/resources/ContactExports.ts | 43 +++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/lib/api/resources/ContactExports.ts diff --git a/src/lib/api/resources/ContactExports.ts b/src/lib/api/resources/ContactExports.ts new file mode 100644 index 0000000..367b06a --- /dev/null +++ b/src/lib/api/resources/ContactExports.ts @@ -0,0 +1,43 @@ +import { AxiosInstance } from "axios"; + +import CONFIG from "../../../config"; + +import { + ContactExportResponse, + CreateContactExportParams, +} from "../../../types/api/contact-exports"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +export default class ContactExportsApi { + private client: AxiosInstance; + + private contactExportsURL: string; + + constructor(client: AxiosInstance, accountId: number) { + this.client = client; + this.contactExportsURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports`; + } + + /** + * Get a contact export by `exportId`. + */ + public async get(exportId: number) { + const url = `${this.contactExportsURL}/${exportId}`; + + return this.client.get(url); + } + + /** + * Export contacts. + */ + public async create(params: CreateContactExportParams) { + const url = `${this.contactExportsURL}`; + + return this.client.post( + url, + params + ); + } +} From d43d1be1ef0bc48d40a8749bd504e75d46a3c248 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Sat, 1 Nov 2025 13:06:47 +0400 Subject: [PATCH 3/9] feat: add ContactExportsBaseAPI to facilitate interaction with ContactExportsApi --- src/lib/api/ContactExports.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/lib/api/ContactExports.ts diff --git a/src/lib/api/ContactExports.ts b/src/lib/api/ContactExports.ts new file mode 100644 index 0000000..2e41f21 --- /dev/null +++ b/src/lib/api/ContactExports.ts @@ -0,0 +1,15 @@ +import { AxiosInstance } from "axios"; + +import ContactExportsApi from "./resources/ContactExports"; + +export default class ContactExportsBaseAPI { + public create: ContactExportsApi["create"]; + + public get: ContactExportsApi["get"]; + + constructor(client: AxiosInstance, accountId: number) { + const contactExports = new ContactExportsApi(client, accountId); + this.create = contactExports.create.bind(contactExports); + this.get = contactExports.get.bind(contactExports); + } +} From 4d2a5a27e9922e43390cd39e83f79b2b6601efce Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Sat, 1 Nov 2025 13:07:26 +0400 Subject: [PATCH 4/9] feat: add getter for Contact Exports API in MailtrapClient --- src/lib/MailtrapClient.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index baa6bb2..f1a8f7b 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -10,8 +10,9 @@ import MailtrapError from "./MailtrapError"; import GeneralAPI from "./api/General"; import TestingAPI from "./api/Testing"; import ContactsBaseAPI from "./api/Contacts"; -import ContactListsBaseAPI from "./api/ContactLists"; +import ContactExportsBaseAPI from "./api/ContactExports"; import ContactFieldsBaseAPI from "./api/ContactFields"; +import ContactListsBaseAPI from "./api/ContactLists"; import TemplatesBaseAPI from "./api/Templates"; import SuppressionsBaseAPI from "./api/Suppressions"; import SendingDomainsBaseAPI from "./api/SendingDomains"; @@ -130,6 +131,14 @@ export default class MailtrapClient { return new ContactsBaseAPI(this.axios, accountId); } + /** + * Getter for Contact Exports API. + */ + get contactExports() { + const accountId = this.validateAccountIdPresence(); + return new ContactExportsBaseAPI(this.axios, accountId); + } + /** * Getter for Contact Lists API. */ From 138bd4875370f7d36f49a7f0dd7021a9e6c6531b Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Sat, 1 Nov 2025 13:07:38 +0400 Subject: [PATCH 5/9] test: add unit tests for contactExports 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..8d67c18 100644 --- a/src/__tests__/lib/mailtrap-client.test.ts +++ b/src/__tests__/lib/mailtrap-client.test.ts @@ -11,6 +11,7 @@ import GeneralAPI from "../../lib/api/General"; import TestingAPI from "../../lib/api/Testing"; import ContactLists from "../../lib/api/ContactLists"; import Contacts from "../../lib/api/Contacts"; +import ContactExportsBaseAPI from "../../lib/api/ContactExports"; import TemplatesBaseAPI from "../../lib/api/Templates"; import SuppressionsBaseAPI from "../../lib/api/Suppressions"; import SendingDomainsBaseAPI from "../../lib/api/SendingDomains"; @@ -823,6 +824,32 @@ describe("lib/mailtrap-client: ", () => { }); }); + describe("get contactExports(): ", () => { + it("rejects with Mailtrap error, when `accountId` is missing.", () => { + const client = new MailtrapClient({ + token: "MY_API_TOKEN", + }); + expect.assertions(1); + + try { + client.contactExports; + } catch (error) { + expect(error).toEqual(new MailtrapError(ACCOUNT_ID_MISSING)); + } + }); + + it("returns contact exports API object when accountId is provided.", () => { + const client = new MailtrapClient({ + token: "MY_API_TOKEN", + accountId: 10, + }); + expect.assertions(1); + + const contactExportsClient = client.contactExports; + expect(contactExportsClient).toBeInstanceOf(ContactExportsBaseAPI); + }); + }); + describe("get templates(): ", () => { it("rejects with Mailtrap error, when `accountId` is missing.", () => { const client = new MailtrapClient({ From fa1fb879e84b0b9801176aa614e6f8555533706f Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Sat, 1 Nov 2025 13:07:58 +0400 Subject: [PATCH 6/9] feat: add example script for creating and fetching contact exports using MailtrapClient --- examples/contact-exports/everything.ts | 45 ++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 examples/contact-exports/everything.ts diff --git a/examples/contact-exports/everything.ts b/examples/contact-exports/everything.ts new file mode 100644 index 0000000..3005bca --- /dev/null +++ b/examples/contact-exports/everything.ts @@ -0,0 +1,45 @@ +import { MailtrapClient } from "mailtrap"; + +const TOKEN = ""; +const ACCOUNT_ID = ""; + +const client = new MailtrapClient({ + token: TOKEN, + accountId: Number(ACCOUNT_ID), +}); + +async function createContactExport() { + try { + // Get contact lists and use first one if available + const lists = await client.contactLists.getList(); + const listId = Array.isArray(lists) && lists.length > 0 ? lists[0].id : undefined; + + // Create filters array per API docs: + // - Use list_id filter with array of list IDs if list available + // - Add subscription_status filter to export only subscribed contacts + const filters = listId + ? [ + { name: "list_id", operator: "equal" as const, value: [listId] }, + { name: "subscription_status", operator: "equal" as const, value: "subscribed" }, + ] + : [ + { name: "subscription_status", operator: "equal" as const, value: "subscribed" }, + ]; + + const created = await client.contactExports.create({ filters }); + console.log("Export created:", JSON.stringify(created, null, 2)); + + // Fetch export to check status and get download URL when finished + const fetched = await client.contactExports.get(created.id); + console.log("Export fetched:", JSON.stringify(fetched, null, 2)); + } catch (error) { + console.error( + "Error creating contact export:", + error instanceof Error ? error.message : String(error) + ); + } +} + +createContactExport(); + + From c3f423b33de6d784f03c69ae12a3a7dafe1e0359 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Sat, 1 Nov 2025 13:33:46 +0400 Subject: [PATCH 7/9] test: add unit tests for ContactExportsApi methods including create and get --- .../lib/api/resources/ContactExports.test.ts | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 src/__tests__/lib/api/resources/ContactExports.test.ts diff --git a/src/__tests__/lib/api/resources/ContactExports.test.ts b/src/__tests__/lib/api/resources/ContactExports.test.ts new file mode 100644 index 0000000..140b8c3 --- /dev/null +++ b/src/__tests__/lib/api/resources/ContactExports.test.ts @@ -0,0 +1,192 @@ +import axios from "axios"; +import AxiosMockAdapter from "axios-mock-adapter"; + +import ContactExportsApi from "../../../../lib/api/resources/ContactExports"; +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/ContactExports: ", () => { + let mock: AxiosMockAdapter; + const accountId = 100; + const contactExportsAPI = new ContactExportsApi(axios, accountId); + + const createContactExportRequest = { + filters: [ + { name: "list_id", operator: "equal" as const, value: [101, 102] }, + { + name: "subscription_status", + operator: "equal" as const, + value: "subscribed", + }, + ], + }; + + const createContactExportResponse: { + id: number; + status: "started" | "created" | "finished"; + created_at: string; + updated_at: string; + url: string | null; + } = { + id: 69, + status: "created", + created_at: "2025-11-01T06:29:00.848Z", + updated_at: "2025-11-01T06:29:00.848Z", + url: null, + }; + + const getContactExportResponse: { + id: number; + status: "started" | "created" | "finished"; + created_at: string; + updated_at: string; + url: string | null; + } = { + id: 69, + status: "finished", + created_at: "2025-11-01T06:29:00.848Z", + updated_at: "2025-11-01T06:29:01.053Z", + url: "https://mailsend-us-mailtrap-tmp-uploads.s3.amazonaws.com/data_exports/export.csv.gz", + }; + + describe("class ContactExportsApi(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(contactExportsAPI).toHaveProperty("create"); + expect(contactExportsAPI).toHaveProperty("get"); + }); + }); + }); + + 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(): ", () => { + it("successfully creates a contact export.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports`; + const expectedResponseData = createContactExportResponse; + + expect.assertions(2); + + mock + .onPost(endpoint, createContactExportRequest) + .reply(200, expectedResponseData); + const result = await contactExportsAPI.create(createContactExportRequest); + + expect(mock.history.post[0].url).toEqual(endpoint); + expect(result).toEqual(expectedResponseData); + }); + + it("fails with error when filters are invalid.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports`; + const expectedErrorMessage = { + errors: { + filters: "invalid", + }, + }; + + expect.assertions(2); + + mock.onPost(endpoint).reply(422, expectedErrorMessage); + + try { + await contactExportsAPI.create(createContactExportRequest); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + // axios logger returns "[object Object]" for error objects, so we check for that + expect(error.message).toBe("[object Object]"); + } + } + }); + + it("fails with error when accountId is invalid.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports`; + const expectedErrorMessage = "Account not found"; + + expect.assertions(2); + + mock.onPost(endpoint).reply(404, { error: expectedErrorMessage }); + + try { + await contactExportsAPI.create(createContactExportRequest); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toContain(expectedErrorMessage); + } + } + }); + }); + + describe("get(): ", () => { + const exportId = 69; + + it("successfully gets a contact export by id.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports/${exportId}`; + const expectedResponseData = getContactExportResponse; + + expect.assertions(2); + + mock.onGet(endpoint).reply(200, expectedResponseData); + const result = await contactExportsAPI.get(exportId); + + expect(mock.history.get[0].url).toEqual(endpoint); + expect(result).toEqual(expectedResponseData); + }); + + it("fails with error when export not found.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports/${exportId}`; + const expectedErrorMessage = "Export not found"; + + expect.assertions(2); + + mock.onGet(endpoint).reply(404, { error: expectedErrorMessage }); + + try { + await contactExportsAPI.get(exportId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toContain(expectedErrorMessage); + } + } + }); + + it("fails with error when exportId is invalid.", async () => { + const invalidExportId = 999; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/exports/${invalidExportId}`; + const expectedErrorMessage = "Export not found"; + + expect.assertions(2); + + mock.onGet(endpoint).reply(404, { error: expectedErrorMessage }); + + try { + await contactExportsAPI.get(invalidExportId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toContain(expectedErrorMessage); + } + } + }); + }); +}); From af1dd71f36791fe2af968ace3d41418b01640cb0 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Sat, 1 Nov 2025 13:36:42 +0400 Subject: [PATCH 8/9] docs: update README to include Contact Exports API section and example --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index edecd5a..edc0a51 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 Exports - 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 Exports API + +- [Contact Exports](examples/contact-exports/everything.ts) + ### Sending API - [Advanced](examples/sending/everything.ts) From 9d85c006285110118c430c7d4ff32ee2228212c5 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Tue, 4 Nov 2025 13:01:29 +0400 Subject: [PATCH 9/9] Update examples/contact-exports/everything.ts Co-authored-by: Marcin Klocek --- examples/contact-exports/everything.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/contact-exports/everything.ts b/examples/contact-exports/everything.ts index 3005bca..8876821 100644 --- a/examples/contact-exports/everything.ts +++ b/examples/contact-exports/everything.ts @@ -42,4 +42,3 @@ async function createContactExport() { createContactExport(); -