Skip to content

Commit 38fd3ca

Browse files
committed
Added headers for events and transactionals
1 parent 5dfd445 commit 38fd3ca

File tree

4 files changed

+115
-5
lines changed

4 files changed

+115
-5
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Thank you for your interest in contributing to Loops JS SDK. We welcome contribu
1313
1. Make your changes in your branch.
1414
2. Follow the coding style and conventions used in the project.
1515
3. Write clear, concise commit messages.
16-
4. Test your changes thoroughly.
16+
4. Test your changes thoroughly. Write or modify existing tests when it makes sense.
1717

1818
## Submitting a Pull Request
1919

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,15 @@ const resp = await loops.sendEvent({
567567
plan: "pro",
568568
},
569569
});
570+
571+
// Example with Idempotency-Key header
572+
const resp = await loops.sendEvent({
573+
574+
eventName: "signup",
575+
headers: {
576+
"Idempotency-Key": "550e8400-e29b-41d4-a716-446655440000",
577+
},
578+
});
570579
```
571580

572581
#### Response
@@ -619,6 +628,18 @@ const resp = await loops.sendTransactionalEmail({
619628
},
620629
});
621630

631+
// Example with Idempotency-Key header
632+
const resp = await loops.sendTransactionalEmail({
633+
transactionalId: "clfq6dinn000yl70fgwwyp82l",
634+
635+
dataVariables: {
636+
loginUrl: "https://myapp.com/login/",
637+
},
638+
headers: {
639+
"Idempotency-Key": "550e8400-e29b-41d4-a716-446655440000",
640+
},
641+
});
642+
622643
// Please contact us to enable attachments on your account.
623644
const resp = await loops.sendTransactionalEmail({
624645
transactionalId: "clfq6dinn000yl70fgwwyp82l",
@@ -733,6 +754,7 @@ const resp = await loops.getTransactionalEmails({ perPage: 15 });
733754

734755
## Version history
735756

757+
- `v5.0.1` (May 13, 2025) - Added a `headers` parameter for [`sendEvent()`](#sendevent) and [`sendTransactionalEmail()`](#sendtransactionalemail), enabling support for the `Idempotency-Key` header.
736758
- `v5.0.0` (Apr 29, 2025)
737759
- Types are now exported so you can use them in your application.
738760
- `ValidationError` is now thrown when paramters are not added correctly.
@@ -770,6 +792,12 @@ const resp = await loops.getTransactionalEmails({ perPage: 15 });
770792

771793
---
772794

795+
## Tests
796+
797+
Run tests with `npm run test`.
798+
799+
---
800+
773801
## Contributing
774802

775803
Bug reports and pull requests are welcome. Please read our [Contributing Guidelines](CONTRIBUTING.md).

src/__tests__/LoopsClient.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,69 @@ describe("LoopsClient", () => {
409409
})
410410
);
411411
});
412+
413+
it("should send event with idempotency key", async () => {
414+
const eventData = {
415+
416+
eventName: "test_event",
417+
headers: {
418+
"Idempotency-Key": "unique_key_123",
419+
},
420+
};
421+
const mockResponse = { success: true };
422+
423+
global.fetch = jest.fn().mockResolvedValue({
424+
ok: true,
425+
json: () => Promise.resolve(mockResponse),
426+
});
427+
428+
const result = await client.sendEvent(eventData);
429+
430+
expect(result).toEqual(mockResponse);
431+
432+
// Get the actual fetch call arguments
433+
const fetchCall = (fetch as jest.Mock).mock.calls[0];
434+
const requestOptions = fetchCall[1];
435+
436+
// Verify headers using Headers object methods
437+
const headers = requestOptions.headers;
438+
expect(headers.get("Idempotency-Key")).toBe("unique_key_123");
439+
440+
// Verify the body doesn't contain idempotency key
441+
expect(requestOptions.body).toBe(
442+
JSON.stringify({
443+
eventName: eventData.eventName,
444+
email: eventData.email,
445+
})
446+
);
447+
});
448+
449+
it("should send event without idempotency key when empty string", async () => {
450+
const eventData = {
451+
452+
eventName: "test_event",
453+
headers: {
454+
"Idempotency-Key": "",
455+
},
456+
};
457+
const mockResponse = { success: true };
458+
459+
global.fetch = jest.fn().mockResolvedValue({
460+
ok: true,
461+
json: () => Promise.resolve(mockResponse),
462+
});
463+
464+
const result = await client.sendEvent(eventData);
465+
466+
expect(result).toEqual(mockResponse);
467+
468+
// Get the actual fetch call arguments
469+
const fetchCall = (fetch as jest.Mock).mock.calls[0];
470+
const requestOptions = fetchCall[1];
471+
472+
// Verify no header is set
473+
expect(requestOptions.headers.get("Idempotency-Key")).toBeNull();
474+
});
412475
});
413476

414477
describe("sendTransactionalEmail", () => {

src/index.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ interface QueryOptions {
33
method?: "GET" | "POST" | "PUT";
44
payload?: Record<string, unknown>;
55
params?: Record<string, string>;
6+
headers?: Record<string, string>;
67
}
78

89
interface ApiKeySuccessResponse {
@@ -280,18 +281,28 @@ class LoopsClient {
280281
* @param {Object} params
281282
* @param {string} params.path Endpoint path
282283
* @param {string} params.method HTTP method
284+
* @param {Object} params.headers Additional headers to send with the request
283285
* @param {Object} params.payload Payload for PUT and POST requests
284286
* @param {Object} params.params URL query parameters
285287
*/
286288
private async _makeQuery<T>({
287289
path,
288290
method = "GET",
291+
headers,
289292
payload,
290293
params,
291294
}: QueryOptions): Promise<T> {
292-
const headers = new Headers();
293-
headers.set("Authorization", `Bearer ${this.apiKey}`);
294-
headers.set("Content-Type", "application/json");
295+
const h = new Headers();
296+
h.set("Authorization", `Bearer ${this.apiKey}`);
297+
h.set("Content-Type", "application/json");
298+
299+
if (headers) {
300+
Object.entries(headers).forEach(([key, value]) => {
301+
if (value !== "" && value !== undefined && value !== null) {
302+
h.set(key, value as string);
303+
}
304+
});
305+
}
295306

296307
const url = new URL(path, this.apiRoot);
297308
if (params && method === "GET") {
@@ -303,7 +314,7 @@ class LoopsClient {
303314
try {
304315
const response = await fetch(url.href, {
305316
method,
306-
headers,
317+
headers: h,
307318
body: payload ? JSON.stringify(payload) : undefined,
308319
});
309320

@@ -525,6 +536,7 @@ class LoopsClient {
525536
* @param {Object} [params.contactProperties] Properties to update the contact with, including custom properties.
526537
* @param {Object} [params.eventProperties] Event properties, made available in emails triggered by the event.
527538
* @param {Object} [params.mailingLists] An object of mailing list IDs and boolean subscription statuses.
539+
* @param {Object} [params.headers] Additional headers to send with the request.
528540
*
529541
* @see https://loops.so/docs/api-reference/send-event
530542
*
@@ -537,13 +549,15 @@ class LoopsClient {
537549
contactProperties,
538550
eventProperties,
539551
mailingLists,
552+
headers,
540553
}: {
541554
email?: string;
542555
userId?: string;
543556
eventName: string;
544557
contactProperties?: ContactProperties;
545558
eventProperties?: EventProperties;
546559
mailingLists?: MailingLists;
560+
headers?: Record<string, string>;
547561
}): Promise<EventSuccessResponse> {
548562
if (!userId && !email)
549563
throw new ValidationError(
@@ -566,6 +580,7 @@ class LoopsClient {
566580
return this._makeQuery({
567581
path: "v1/events/send",
568582
method: "POST",
583+
headers,
569584
payload,
570585
});
571586
}
@@ -579,6 +594,7 @@ class LoopsClient {
579594
* @param {boolean} [params.addToAudience] Create a contact in your audience using the provided email address (if one doesn't already exist).
580595
* @param {Object} [params.dataVariables] Data variables as defined by the transational email template.
581596
* @param {Object[]} [params.attachments] File(s) to be sent along with the email message.
597+
* @param {Object} [params.headers] Additional headers to send with the request.
582598
*
583599
* @see https://loops.so/docs/api-reference/send-transactional-email
584600
*
@@ -590,12 +606,14 @@ class LoopsClient {
590606
addToAudience,
591607
dataVariables,
592608
attachments,
609+
headers,
593610
}: {
594611
transactionalId: string;
595612
email: string;
596613
addToAudience?: boolean;
597614
dataVariables?: TransactionalVariables;
598615
attachments?: Array<TransactionalAttachment>;
616+
headers?: Record<string, string>;
599617
}): Promise<TransactionalSuccess> {
600618
const payload = {
601619
transactionalId,
@@ -607,6 +625,7 @@ class LoopsClient {
607625
return this._makeQuery({
608626
path: "v1/transactional",
609627
method: "POST",
628+
headers,
610629
payload,
611630
});
612631
}

0 commit comments

Comments
 (0)