@unsent/sdk 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,7 +38,7 @@ bun add @unsent/sdk
38
38
  ```typescript
39
39
  import { unsent } from "@unsent/sdk";
40
40
 
41
- const client = new unsent("us_12345");
41
+ const client = new unsent("un_xxxx");
42
42
  ```
43
43
 
44
44
  ### Environment Variables
@@ -60,8 +60,8 @@ const { data, error } = await client.emails.send({
60
60
  to: "hello@acme.com",
61
61
  from: "hello@company.com",
62
62
  subject: "unsent email",
63
- html: "<p>unsent is the best open source product to send emails</p>",
64
- text: "unsent is the best open source product to send emails",
63
+ html: "<p>unsent is the best email service provider to send emails</p>",
64
+ text: "unsent is the best email service provider to send emails",
65
65
  });
66
66
 
67
67
  if (error) {
@@ -143,6 +143,60 @@ if (error) {
143
143
  }
144
144
  ```
145
145
 
146
+ #### Idempotency Keys
147
+
148
+ Safely retry email sends with an idempotency key to prevent duplicate sends. This is especially useful for critical emails like signup confirmations or password resets.
149
+
150
+ ```typescript
151
+ // Safely retry sends with an idempotency key
152
+ const { data, error } = await client.emails.send(
153
+ {
154
+ to: "hello@acme.com",
155
+ from: "hello@company.com",
156
+ subject: "unsent email",
157
+ html: "<p>unsent is the best open source product to send emails</p>",
158
+ },
159
+ { idempotencyKey: "signup-123" },
160
+ );
161
+
162
+ if (error) {
163
+ console.error("Error:", error);
164
+ } else {
165
+ console.log("Email sent! ID:", data.id);
166
+ }
167
+ ```
168
+
169
+ Idempotency keys also work for batch sends:
170
+
171
+ ```typescript
172
+ // Works for bulk sends too
173
+ const { data, error } = await client.emails.batch(
174
+ [
175
+ {
176
+ to: "a@example.com",
177
+ from: "hello@company.com",
178
+ subject: "Welcome",
179
+ html: "<p>Hello A</p>",
180
+ },
181
+ {
182
+ to: "b@example.com",
183
+ from: "hello@company.com",
184
+ subject: "Welcome",
185
+ html: "<p>Hello B</p>",
186
+ },
187
+ ],
188
+ { idempotencyKey: "bulk-welcome-1" },
189
+ );
190
+
191
+ if (error) {
192
+ console.error("Error:", error);
193
+ } else {
194
+ console.log(`Sent ${data.length} emails`);
195
+ }
196
+ ```
197
+
198
+ > **Note:** Reusing the same idempotency key with a different payload will return HTTP 409 (Conflict). This ensures that retries are safe and prevents accidental duplicate sends with modified content.
199
+
146
200
  ### Managing Emails
147
201
 
148
202
  #### Get Email Details
@@ -249,7 +303,7 @@ Create and manage email campaigns:
249
303
  ```typescript
250
304
  import { unsent } from "@unsent/sdk";
251
305
 
252
- const client = new unsent("us_12345");
306
+ const client = new unsent("un_xxxx");
253
307
 
254
308
  // Create a campaign
255
309
  const campaign = await client.campaigns.create({
@@ -336,7 +390,7 @@ The SDK is fully typed with TypeScript:
336
390
  ```typescript
337
391
  import { unsent } from "@unsent/sdk";
338
392
 
339
- const client = new unsent("us_12345");
393
+ const client = new unsent("un_xxxx");
340
394
 
341
395
  // Full type inference and autocomplete
342
396
  const result = await client.emails.send({
package/dist/index.d.mts CHANGED
@@ -1312,6 +1312,12 @@ type BatchEmailPayload = paths["/v1/emails/batch"]["post"]["requestBody"]["conte
1312
1312
  * Successful response schema for batch email send.
1313
1313
  */
1314
1314
  type BatchEmailResponseSuccess = paths["/v1/emails/batch"]["post"]["responses"]["200"]["content"]["application/json"];
1315
+ /**
1316
+ * Options for email requests.
1317
+ */
1318
+ type EmailRequestOptions = {
1319
+ idempotencyKey?: string;
1320
+ };
1315
1321
  /**
1316
1322
  * Response structure for the batch method.
1317
1323
  */
@@ -1322,15 +1328,15 @@ type BatchEmailResponse = {
1322
1328
  declare class Emails {
1323
1329
  private readonly unsent;
1324
1330
  constructor(unsent: unsent);
1325
- send(payload: SendEmailPayload): Promise<CreateEmailResponse>;
1326
- create(payload: SendEmailPayload): Promise<CreateEmailResponse>;
1331
+ send(payload: SendEmailPayload, options?: EmailRequestOptions): Promise<CreateEmailResponse>;
1332
+ create(payload: SendEmailPayload, options?: EmailRequestOptions): Promise<CreateEmailResponse>;
1327
1333
  /**
1328
1334
  * Send up to 100 emails in a single request.
1329
1335
  *
1330
1336
  * @param payload An array of email payloads. Max 100 emails.
1331
1337
  * @returns A promise that resolves to the list of created email IDs or an error.
1332
1338
  */
1333
- batch(payload: BatchEmailPayload): Promise<BatchEmailResponse>;
1339
+ batch(payload: BatchEmailPayload, options?: EmailRequestOptions): Promise<BatchEmailResponse>;
1334
1340
  get(id: string): Promise<GetEmailResponse>;
1335
1341
  update(id: string, payload: UpdateEmailPayload): Promise<UpdateEmailResponse>;
1336
1342
  cancel(id: string): Promise<CancelEmailResponse>;
@@ -1419,7 +1425,9 @@ declare class unsent {
1419
1425
  data: T | null;
1420
1426
  error: ErrorResponse | null;
1421
1427
  }>;
1422
- post<T>(path: string, body: unknown): Promise<{
1428
+ post<T>(path: string, body: unknown, options?: {
1429
+ headers?: Record<string, string>;
1430
+ }): Promise<{
1423
1431
  data: T | null;
1424
1432
  error: ErrorResponse | null;
1425
1433
  }>;
package/dist/index.d.ts CHANGED
@@ -1312,6 +1312,12 @@ type BatchEmailPayload = paths["/v1/emails/batch"]["post"]["requestBody"]["conte
1312
1312
  * Successful response schema for batch email send.
1313
1313
  */
1314
1314
  type BatchEmailResponseSuccess = paths["/v1/emails/batch"]["post"]["responses"]["200"]["content"]["application/json"];
1315
+ /**
1316
+ * Options for email requests.
1317
+ */
1318
+ type EmailRequestOptions = {
1319
+ idempotencyKey?: string;
1320
+ };
1315
1321
  /**
1316
1322
  * Response structure for the batch method.
1317
1323
  */
@@ -1322,15 +1328,15 @@ type BatchEmailResponse = {
1322
1328
  declare class Emails {
1323
1329
  private readonly unsent;
1324
1330
  constructor(unsent: unsent);
1325
- send(payload: SendEmailPayload): Promise<CreateEmailResponse>;
1326
- create(payload: SendEmailPayload): Promise<CreateEmailResponse>;
1331
+ send(payload: SendEmailPayload, options?: EmailRequestOptions): Promise<CreateEmailResponse>;
1332
+ create(payload: SendEmailPayload, options?: EmailRequestOptions): Promise<CreateEmailResponse>;
1327
1333
  /**
1328
1334
  * Send up to 100 emails in a single request.
1329
1335
  *
1330
1336
  * @param payload An array of email payloads. Max 100 emails.
1331
1337
  * @returns A promise that resolves to the list of created email IDs or an error.
1332
1338
  */
1333
- batch(payload: BatchEmailPayload): Promise<BatchEmailResponse>;
1339
+ batch(payload: BatchEmailPayload, options?: EmailRequestOptions): Promise<BatchEmailResponse>;
1334
1340
  get(id: string): Promise<GetEmailResponse>;
1335
1341
  update(id: string, payload: UpdateEmailPayload): Promise<UpdateEmailResponse>;
1336
1342
  cancel(id: string): Promise<CancelEmailResponse>;
@@ -1419,7 +1425,9 @@ declare class unsent {
1419
1425
  data: T | null;
1420
1426
  error: ErrorResponse | null;
1421
1427
  }>;
1422
- post<T>(path: string, body: unknown): Promise<{
1428
+ post<T>(path: string, body: unknown, options?: {
1429
+ headers?: Record<string, string>;
1430
+ }): Promise<{
1423
1431
  data: T | null;
1424
1432
  error: ErrorResponse | null;
1425
1433
  }>;
package/dist/index.js CHANGED
@@ -73,17 +73,18 @@ var Emails = class {
73
73
  this.unsent = unsent2;
74
74
  this.unsent = unsent2;
75
75
  }
76
- async send(payload) {
77
- return this.create(payload);
76
+ async send(payload, options) {
77
+ return this.create(payload, options);
78
78
  }
79
- async create(payload) {
79
+ async create(payload, options) {
80
80
  if (payload.react) {
81
81
  payload.html = await (0, import_render.render)(payload.react);
82
82
  delete payload.react;
83
83
  }
84
84
  const data = await this.unsent.post(
85
85
  "/emails",
86
- payload
86
+ payload,
87
+ options?.idempotencyKey ? { headers: { "Idempotency-Key": options.idempotencyKey } } : void 0
87
88
  );
88
89
  return data;
89
90
  }
@@ -93,10 +94,11 @@ var Emails = class {
93
94
  * @param payload An array of email payloads. Max 100 emails.
94
95
  * @returns A promise that resolves to the list of created email IDs or an error.
95
96
  */
96
- async batch(payload) {
97
+ async batch(payload, options) {
97
98
  const response = await this.unsent.post(
98
99
  "/emails/batch",
99
- payload
100
+ payload,
101
+ options?.idempotencyKey ? { headers: { "Idempotency-Key": options.idempotencyKey } } : void 0
100
102
  );
101
103
  return {
102
104
  data: response.data ? response.data.data : null,
@@ -220,12 +222,12 @@ var unsent = class {
220
222
  }
221
223
  if (!this.key) {
222
224
  throw new Error(
223
- 'Missing API key. Pass it to the constructor `new unsent("us_123")`'
225
+ 'Missing API key. Pass it to the constructor `new unsent("un_xxxx")`'
224
226
  );
225
227
  }
226
228
  }
227
229
  if (url) {
228
- this.url = `${url}/api/v1`;
230
+ this.url = `${url}/v1`;
229
231
  }
230
232
  this.headers = new Headers({
231
233
  Authorization: `Bearer ${this.key}`,
@@ -239,7 +241,8 @@ var unsent = class {
239
241
  campaigns = new Campaigns(this);
240
242
  url = baseUrl;
241
243
  async fetchRequest(path, options = {}) {
242
- const response = await fetch(`${this.url}${path}`, options);
244
+ const fullUrl = `${this.url}${path}`;
245
+ const response = await fetch(fullUrl, options);
243
246
  const defaultError = {
244
247
  code: "INTERNAL_SERVER_ERROR",
245
248
  message: response.statusText
@@ -261,13 +264,32 @@ var unsent = class {
261
264
  return { data: null, error: defaultError };
262
265
  }
263
266
  }
264
- const data = await response.json();
265
- return { data, error: null };
267
+ try {
268
+ const data = await response.json();
269
+ return { data, error: null };
270
+ } catch (err) {
271
+ if (err instanceof Error && err.message.includes("JSON")) {
272
+ return {
273
+ data: null,
274
+ error: {
275
+ code: "INTERNAL_SERVER_ERROR",
276
+ message: "Invalid JSON response from server"
277
+ }
278
+ };
279
+ }
280
+ throw err;
281
+ }
266
282
  }
267
- async post(path, body) {
283
+ async post(path, body, options) {
284
+ const headers = new Headers(this.headers);
285
+ if (options?.headers) {
286
+ Object.entries(options.headers).forEach(([key, value]) => {
287
+ headers.set(key, value);
288
+ });
289
+ }
268
290
  const requestOptions = {
269
291
  method: "POST",
270
- headers: this.headers,
292
+ headers,
271
293
  body: JSON.stringify(body)
272
294
  };
273
295
  return this.fetchRequest(path, requestOptions);
package/dist/index.mjs CHANGED
@@ -46,17 +46,18 @@ var Emails = class {
46
46
  this.unsent = unsent2;
47
47
  this.unsent = unsent2;
48
48
  }
49
- async send(payload) {
50
- return this.create(payload);
49
+ async send(payload, options) {
50
+ return this.create(payload, options);
51
51
  }
52
- async create(payload) {
52
+ async create(payload, options) {
53
53
  if (payload.react) {
54
54
  payload.html = await render(payload.react);
55
55
  delete payload.react;
56
56
  }
57
57
  const data = await this.unsent.post(
58
58
  "/emails",
59
- payload
59
+ payload,
60
+ options?.idempotencyKey ? { headers: { "Idempotency-Key": options.idempotencyKey } } : void 0
60
61
  );
61
62
  return data;
62
63
  }
@@ -66,10 +67,11 @@ var Emails = class {
66
67
  * @param payload An array of email payloads. Max 100 emails.
67
68
  * @returns A promise that resolves to the list of created email IDs or an error.
68
69
  */
69
- async batch(payload) {
70
+ async batch(payload, options) {
70
71
  const response = await this.unsent.post(
71
72
  "/emails/batch",
72
- payload
73
+ payload,
74
+ options?.idempotencyKey ? { headers: { "Idempotency-Key": options.idempotencyKey } } : void 0
73
75
  );
74
76
  return {
75
77
  data: response.data ? response.data.data : null,
@@ -193,12 +195,12 @@ var unsent = class {
193
195
  }
194
196
  if (!this.key) {
195
197
  throw new Error(
196
- 'Missing API key. Pass it to the constructor `new unsent("us_123")`'
198
+ 'Missing API key. Pass it to the constructor `new unsent("un_xxxx")`'
197
199
  );
198
200
  }
199
201
  }
200
202
  if (url) {
201
- this.url = `${url}/api/v1`;
203
+ this.url = `${url}/v1`;
202
204
  }
203
205
  this.headers = new Headers({
204
206
  Authorization: `Bearer ${this.key}`,
@@ -212,7 +214,8 @@ var unsent = class {
212
214
  campaigns = new Campaigns(this);
213
215
  url = baseUrl;
214
216
  async fetchRequest(path, options = {}) {
215
- const response = await fetch(`${this.url}${path}`, options);
217
+ const fullUrl = `${this.url}${path}`;
218
+ const response = await fetch(fullUrl, options);
216
219
  const defaultError = {
217
220
  code: "INTERNAL_SERVER_ERROR",
218
221
  message: response.statusText
@@ -234,13 +237,32 @@ var unsent = class {
234
237
  return { data: null, error: defaultError };
235
238
  }
236
239
  }
237
- const data = await response.json();
238
- return { data, error: null };
240
+ try {
241
+ const data = await response.json();
242
+ return { data, error: null };
243
+ } catch (err) {
244
+ if (err instanceof Error && err.message.includes("JSON")) {
245
+ return {
246
+ data: null,
247
+ error: {
248
+ code: "INTERNAL_SERVER_ERROR",
249
+ message: "Invalid JSON response from server"
250
+ }
251
+ };
252
+ }
253
+ throw err;
254
+ }
239
255
  }
240
- async post(path, body) {
256
+ async post(path, body, options) {
257
+ const headers = new Headers(this.headers);
258
+ if (options?.headers) {
259
+ Object.entries(options.headers).forEach(([key, value]) => {
260
+ headers.set(key, value);
261
+ });
262
+ }
241
263
  const requestOptions = {
242
264
  method: "POST",
243
- headers: this.headers,
265
+ headers,
244
266
  body: JSON.stringify(body)
245
267
  };
246
268
  return this.fetchRequest(path, requestOptions);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unsent/sdk",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "TypeScript SDK for the Unsent API - Send transactional emails with ease",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -31,7 +31,7 @@
31
31
  "@types/node": "^24.7.0",
32
32
  "@types/react": "^19.1.2",
33
33
  "openapi-typescript": "^7.6.1",
34
- "tsup": "^8.4.0",
34
+ "tsup": "^8.5.0",
35
35
  "typescript": "^5.8.3",
36
36
  "@unsent/typescript-config": "0.0.0"
37
37
  },