Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
142c67a
examples: add example for creating contact events with Mailtrap API
narekhovhannisyan Nov 3, 2025
ef58d1a
lib: add getter for Contact Events API in MailtrapClient
narekhovhannisyan Nov 3, 2025
7437691
api: implement ContactFieldsBaseAPI to facilitate creation of contact…
narekhovhannisyan Nov 3, 2025
e76c748
api: add ContactEventsApi class for managing contact events
narekhovhannisyan Nov 3, 2025
49863b5
types: add ContactEventOptions and ContactEventResponse interfaces fo…
narekhovhannisyan Nov 3, 2025
050c89c
docs: update README to include Contact Events API section and example
narekhovhannisyan Nov 3, 2025
a14af69
test: add unit tests for contactEvents getter in MailtrapClient
narekhovhannisyan Nov 4, 2025
9e49d3f
test: add unit tests for ContactEvents class initialization
narekhovhannisyan Nov 4, 2025
a64da78
test: add unit tests for create method in ContactEventsApi class
narekhovhannisyan Nov 4, 2025
167a072
refactor: rename ContactFieldsBaseAPI to ContactEventsBaseAPI for cla…
narekhovhannisyan Nov 4, 2025
975a54d
refactor: rename contactEventsURL to contactsURL for consistency in C…
narekhovhannisyan Nov 4, 2025
ab0a77d
Update examples/contact-events/everything.ts
narekhovhannisyan Nov 4, 2025
6465533
Update examples/contact-events/everything.ts
narekhovhannisyan Nov 4, 2025
ef7b573
Merge branch 'main' into create-contact-event
narekhovhannisyan Nov 6, 2025
f1aa2ce
Merge branch 'main' into create-contact-event
narekhovhannisyan Nov 6, 2025
ada5480
refactor: remove unused ContactListsBaseAPI import and clean up comme…
narekhovhannisyan Nov 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,22 @@ Quickly integrate Mailtrap with your Node.js app.

Currently, with this SDK you can:
- Email API/SMTP
- Send an email (Transactional and Bulk streams)
- Send an email
- Send an email with a template
- Send a batch of emails (Transactional and Bulk streams)
- Send a batch of emails
- Email Sandbox (Testing)
- Send an email
- Send an email with a template
- Message management
- Inbox management
- Project management
- Contact management
- Contacts
- Contact Events
- Contact Exports
- Contact Fields
- Contact Imports
- Contact Lists
- Contacts
- General
- Templates
- Suppressions management
Expand Down Expand Up @@ -116,25 +117,30 @@ See transport usage below:

Refer to the [`examples`](examples) folder for the source code of this and other advanced examples.

### Contacts API
### Contact Events API

- [Contacts](examples/contacts/everything.ts)
- [Contact Events](examples/contact-events/everything.ts)

### Contact Lists API
### Contact Exports API

- [Contact Lists](examples/contact-lists/everything.ts)
- [Contact Exports](examples/contact-exports/everything.ts)

### Contact Fields API

- [Contact Fields](examples/contact-fields/everything.ts)

### Contact Exports API

- [Contact Exports](examples/contact-exports/everything.ts)
### Contact Imports API

- [Contact Imports](examples/contact-imports/everything.ts)

### Contact Lists API

- [Contact Lists](examples/contact-lists/everything.ts)

### Contacts API

- [Contacts](examples/contacts/everything.ts)

### Sending API

- [Advanced](examples/sending/everything.ts)
Expand Down
57 changes: 57 additions & 0 deletions examples/contact-events/everything.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { MailtrapClient } from "mailtrap";

const TOKEN = "<YOUR-TOKEN-HERE>";
const ACCOUNT_ID = "<YOUR-ACCOUNT-ID-HERE>";

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 contact 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();

17 changes: 17 additions & 0 deletions src/__tests__/lib/api/ContactEvents.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
});
});
106 changes: 106 additions & 0 deletions src/__tests__/lib/api/resources/ContactEvents.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
});
});
});
27 changes: 27 additions & 0 deletions src/__tests__/lib/mailtrap-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ContactExportsBaseAPI from "../../lib/api/ContactExports";
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;
Expand Down Expand Up @@ -927,5 +928,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);
});
});
});
});
9 changes: 9 additions & 0 deletions src/lib/MailtrapClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import handleSendingError from "./axios-logger";
import MailtrapError from "./MailtrapError";

import ContactsBaseAPI from "./api/Contacts";
import ContactEventsBaseAPI from "./api/ContactEvents";
import ContactExportsBaseAPI from "./api/ContactExports";
import ContactFieldsBaseAPI from "./api/ContactFields";
import ContactImportsBaseAPI from "./api/ContactImports";
Expand Down Expand Up @@ -132,6 +133,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 Exports API.
*/
Expand Down
12 changes: 12 additions & 0 deletions src/lib/api/ContactEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { AxiosInstance } from "axios";

import ContactEventsApi from "./resources/ContactEvents";

export default class ContactEventsBaseAPI {
public create: ContactEventsApi["create"];

constructor(client: AxiosInstance, accountId: number) {
const contactEvents = new ContactEventsApi(client, accountId);
this.create = contactEvents.create.bind(contactEvents);
}
}
36 changes: 36 additions & 0 deletions src/lib/api/resources/ContactEvents.ts
Original file line number Diff line number Diff line change
@@ -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 contactsURL: string;

constructor(client: AxiosInstance, accountId: number) {
this.client = client;
this.contactsURL = `${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.contactsURL}/${contactIdentifier}/events`;

return this.client.post<ContactEventResponse, ContactEventResponse>(
url,
data
);
}
}
11 changes: 11 additions & 0 deletions src/types/api/contact-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface ContactEventOptions {
name: string;
params: Record<string, string | number | boolean | null>;
}

export interface ContactEventResponse {
contact_id: string;
contact_email: string;
name: string;
params: Record<string, string | number | boolean | null>;
}