migma 1.3.3 → 1.4.0

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
@@ -97,7 +97,7 @@ await migma.sending.send({
97
97
 
98
98
  ## Features
99
99
 
100
- - Full coverage of all Migma API v1 endpoints (80+ methods across 15 resources)
100
+ - Full coverage of all Migma API v1 endpoints (80+ methods across 16 resources)
101
101
  - TypeScript-first with complete type definitions
102
102
  - `{ data, error }` return pattern — methods never throw
103
103
  - Automatic retries with exponential backoff on 5xx/429
@@ -126,6 +126,7 @@ Every method returns `{ data, error }` instead of throwing. TypeScript narrows t
126
126
  ```typescript
127
127
  const { data, error } = await migma.contacts.create({
128
128
  email: 'john@example.com',
129
+ status: 'subscribed', // only for contacts with marketing consent
129
130
  projectId: 'proj_abc123',
130
131
  });
131
132
 
@@ -206,7 +207,7 @@ const { data: contact } = await migma.contacts.create({
206
207
  // List with filters
207
208
  const { data: contacts } = await migma.contacts.list({
208
209
  projectId: 'proj_abc123',
209
- status: 'active',
210
+ status: 'subscribed',
210
211
  tags: 'newsletter',
211
212
  limit: 50,
212
213
  });
@@ -219,6 +220,40 @@ const { data: result } = await migma.contacts.bulkImport({
219
220
  ],
220
221
  projectId: 'proj_abc123',
221
222
  });
223
+
224
+ // Batch delete by email (up to 1000; unmatched emails come back in notFound)
225
+ const { data: deletion } = await migma.contacts.bulkDeleteByEmail({
226
+ emails: ['a@example.com', 'b@example.com'],
227
+ projectId: 'proj_abc123',
228
+ });
229
+
230
+ // Import contacts from a CSV file (multipart upload, processed in the background).
231
+ // `file` accepts a Blob/File, a Buffer, or a ReadableStream.
232
+ import { readFileSync } from 'node:fs';
233
+
234
+ const { data: job } = await migma.contacts.imports.create({
235
+ file: readFileSync('./contacts.csv'),
236
+ projectId: 'proj_abc123',
237
+ columnMap: {
238
+ email: 'Email',
239
+ firstName: 'First Name',
240
+ customFields: { plan: 'Plan' },
241
+ },
242
+ onConflict: 'upsert',
243
+ tags: ['newsletter'],
244
+ });
245
+
246
+ // Poll for status + counts until the import finishes.
247
+ const { data: status } = await migma.contacts.imports.get(job!.id);
248
+ // status.status → 'pending' | 'processing' | 'completed' | 'failed'
249
+ // status.counts → { total, created, updated, skipped, failed }
250
+
251
+ // Safe retries: pass an Idempotency-Key so a retried write never duplicates.
252
+ // Same key + same body within 24h replays the original response.
253
+ await migma.contacts.create(
254
+ { email: 'jane@example.com', projectId: 'proj_abc123' },
255
+ { idempotencyKey: 'signup-jane@example.com-1779292800' },
256
+ );
222
257
  ```
223
258
 
224
259
  [Contacts API Reference](https://docs.migma.ai/api-reference/contacts/get-contact)
@@ -273,6 +308,23 @@ await migma.sending.send({
273
308
 
274
309
  [Sending API Reference](https://docs.migma.ai/api-reference/sending/send-email)
275
310
 
311
+ ### Sending Metrics
312
+
313
+ Read account sending totals, delivery outcomes, and remaining daily and monthly capacity.
314
+
315
+ ```typescript
316
+ const { data: metrics } = await migma.metrics.sending({ months: 6 });
317
+
318
+ console.log(metrics?.period, metrics?.monthly.sent, metrics?.remainingMonth);
319
+ console.log(metrics?.today.sent, metrics?.today.remainingToday);
320
+
321
+ for (const month of metrics?.history ?? []) {
322
+ console.log(month.period, month.sent, month.delivered, month.bounced, month.complained);
323
+ }
324
+ ```
325
+
326
+ Omit `months` when you only need the current month and today's capacity.
327
+
276
328
  ### Generated Series and Editing
277
329
 
278
330
  Generate a series with `count`, then use each email's `emailId` to fetch, prompt-edit, or send one email.
@@ -297,6 +349,123 @@ await migma.emails.edit('email_abc123', {
297
349
  });
298
350
  ```
299
351
 
352
+ ### Campaigns
353
+
354
+ Create a campaign from a generated email, send or schedule it, then read its
355
+ engagement stats and per-recipient logs.
356
+
357
+ ```typescript
358
+ // Create from a generated email
359
+ const { data: campaign } = await migma.campaigns.create({
360
+ projectId: 'proj_abc123',
361
+ name: 'Spring Launch',
362
+ conversationId: 'conv_abc123',
363
+ emailId: 'email_abc123',
364
+ subject: 'Spring is here',
365
+ from: 'hello@yourdomain.com',
366
+ fromName: 'Your Brand',
367
+ recipientType: 'audience',
368
+ recipientId: 'aud_abc123',
369
+ });
370
+
371
+ // Send now (idempotency key recommended — see below)
372
+ await migma.campaigns.send(campaign.id, {
373
+ idempotencyKey: `campaign-send-${campaign.id}`,
374
+ });
375
+
376
+ // Or schedule for later
377
+ await migma.campaigns.schedule(
378
+ campaign.id,
379
+ { scheduledAt: '2026-07-01T15:00:00Z', scheduledTimezone: 'America/New_York' },
380
+ { idempotencyKey: `campaign-send-${campaign.id}` },
381
+ );
382
+
383
+ // Aggregated engagement metrics (cached, may be slightly stale)
384
+ const { data: stats } = await migma.campaigns.stats(campaign.id);
385
+ console.log(stats?.openRate, stats?.clickRate, stats?.lastUpdated);
386
+
387
+ // Per-recipient logs, cursor-paginated and optionally filtered by status
388
+ const { data: logs } = await migma.campaigns.logs(campaign.id, {
389
+ status: 'opened',
390
+ limit: 50,
391
+ });
392
+ for (const row of logs?.emails ?? []) {
393
+ console.log(row.to_email, row.status, row.opened_at);
394
+ }
395
+ if (logs?.hasMore) {
396
+ await migma.campaigns.logs(campaign.id, { cursor: logs.nextCursor ?? undefined });
397
+ }
398
+ ```
399
+
400
+ Campaign stats are sourced from the email tracking worker and cached on the
401
+ campaign, so they can lag live activity. `botOpens`, `botClicks`, and `mppOpens`
402
+ reflect Apple Mail Privacy Protection and bot detection when that data is
403
+ available. Log rows come straight from the tracking store, so their field names
404
+ are `snake_case` (`to_email`, `opened_at`, `bounce_type`) unlike the rest of the
405
+ API.
406
+
407
+ [Campaigns Guide](https://docs.migma.ai/campaigns/overview)
408
+
409
+ ### Idempotency
410
+
411
+ Write methods accept an optional trailing `CallOptions` argument carrying an
412
+ `idempotencyKey`. The key is sent as the `Idempotency-Key` header (max 100
413
+ characters) and is scoped to your API key for 24 hours:
414
+
415
+ - **Same key + same body** within 24h replays the original response (the server
416
+ marks the replay with an `Idempotent-Replayed: true` header).
417
+ - **Same key + different body** returns `409 IDEMPOTENCY_CONFLICT`.
418
+ - `429` and `5xx` responses are never cached, so they stay safe to retry — and
419
+ the SDK reuses your key across its own automatic retries, so a retried call
420
+ never duplicates.
421
+
422
+ ```typescript
423
+ // Sending — key on the recipient + send time
424
+ await migma.sending.send(
425
+ {
426
+ recipientType: 'email',
427
+ recipientEmail: 'user@example.com',
428
+ from: 'hello@yourdomain.com',
429
+ fromName: 'Your Brand',
430
+ subject: 'Welcome!',
431
+ emailId: 'email_abc123',
432
+ },
433
+ { idempotencyKey: 'send-email_abc123-user@example.com-now' },
434
+ );
435
+
436
+ // Campaign send — one key per campaign send
437
+ await migma.campaigns.send('camp_abc123', {
438
+ idempotencyKey: 'campaign-send-camp_abc123',
439
+ });
440
+
441
+ // Email generation — key on project + the turn/prompt
442
+ await migma.emails.generate(
443
+ { projectId: 'proj_abc123', prompt: 'Welcome email' },
444
+ { idempotencyKey: 'email-gen-proj_abc123-welcome' },
445
+ );
446
+
447
+ // Contact add — key on the email
448
+ await migma.contacts.create(
449
+ { email: 'jane@example.com', projectId: 'proj_abc123' },
450
+ { idempotencyKey: 'contact-jane@example.com' },
451
+ );
452
+ ```
453
+
454
+ **Choose a deterministic key** so the same logical operation produces the same
455
+ key on every attempt:
456
+
457
+ - send: `send-<emailId>-<recipientId|to>-<scheduledAt|"now">`
458
+ - campaign send: `campaign-send-<campaignId>`
459
+ - email generate: `email-gen-<projectId>-<turnOrPromptHash>`
460
+ - contact add: `contact-<email>`
461
+
462
+ **Anti-patterns to avoid:**
463
+
464
+ - Don't generate the key with `uuid()` or `Date.now()` per call — a fresh key
465
+ every attempt defeats deduplication across retries and process restarts.
466
+ - Don't hash the request body to build the key — identical retries you *want*
467
+ deduplicated would each get a different key, and you lose the conflict signal.
468
+
300
469
  ### Email Validation
301
470
 
302
471
  Run preflight checks — compatibility, links, spelling, and deliverability.
@@ -374,6 +543,14 @@ const { data: avail } = await migma.domains.checkAvailability('mycompany');
374
543
  if (avail.available) {
375
544
  await migma.domains.createManaged({ prefix: 'mycompany' });
376
545
  }
546
+
547
+ // Provision a transactional stream (notify.example.com) to isolate
548
+ // transactional reputation from marketing, then publish the returned DNS records
549
+ await migma.domains.provisionStream({ rootDomain: 'example.com', stream: 'transactional' });
550
+
551
+ // Or provision both streams (send. marketing + notify. transactional) in one call
552
+ const { data } = await migma.domains.setup({ rootDomain: 'example.com' });
553
+ // data.marketing, data.transactional
377
554
  ```
378
555
 
379
556
  [Domains API Reference](https://docs.migma.ai/api-reference/domains/list-domains)
@@ -391,7 +568,19 @@ const { data: tag } = await migma.tags.create({
391
568
  // Segments
392
569
  const { data: segment } = await migma.segments.create({
393
570
  name: 'Active US Customers',
394
- filters: { status: 'subscribed', customFields: { country: ['US'] } },
571
+ filters: {
572
+ status: 'subscribed',
573
+ fields: [{ key: 'country', mode: 'is', values: ['US'] }],
574
+ activity: [{ action: 'opened', channel: 'email', mode: 'within', unit: 'days', amount: 30 }],
575
+ },
576
+ projectId: 'proj_abc123',
577
+ });
578
+
579
+ const { data: campaignSegment } = await migma.segments.create({
580
+ name: 'Campaign openers',
581
+ filters: {
582
+ activity: [{ action: 'opened', channel: 'email', mode: 'within', unit: 'days', amount: 14, campaignId: 'cmp_123' }],
583
+ },
395
584
  projectId: 'proj_abc123',
396
585
  });
397
586
 
@@ -450,17 +639,19 @@ await migma.images.updateLogos('proj_abc123', {
450
639
  | Resource | Methods | Docs |
451
640
  |----------|---------|------|
452
641
  | `migma.contacts` | `create` `list` `get` `update` `remove` `bulkImport` `getBulkImportStatus` `changeStatus` | [Contacts](https://docs.migma.ai/api-reference/contacts/get-contact) |
642
+ | `migma.contacts.imports` | `create` `get` | [Contact imports](https://docs.migma.ai/api-reference/contacts/get-contact) |
453
643
  | `migma.tags` | `create` `list` `get` `update` `remove` | [Tags](https://docs.migma.ai/api-reference/tags/list-tags) |
454
644
  | `migma.segments` | `create` `list` `get` `update` `remove` | [Segments](https://docs.migma.ai/api-reference/audiences/list-audiences) |
455
645
  | `migma.topics` | `create` `list` `get` `update` `remove` `subscribe` `unsubscribe` | [Topics](https://docs.migma.ai/api-reference/topics/list-topics) |
456
646
  | `migma.sending` | `send` `getBatchStatus` | [Sending](https://docs.migma.ai/api-reference/sending/send-email) |
647
+ | `migma.metrics` | `sending` | [API Reference](https://docs.migma.ai/api-reference/introduction) |
457
648
  | `migma.projects` | `list` `get` `import` `getImportStatus` `retryImport` `fieldCatalog` `importAndWait` | [Projects](https://docs.migma.ai/api-reference/projects/list-projects) |
458
649
  | `migma.emails` | `generate` `getGenerationStatus` `generateAndWait` `sendTest` `get` `edit` | [Email Generation](https://docs.migma.ai/api-reference/email/generate-email-async) |
459
- | `migma.campaigns` | `list` `create` `get` `send` `schedule` `cancel` `archive` `unarchive` | [Campaigns](https://docs.migma.ai/campaigns/overview) |
650
+ | `migma.campaigns` | `list` `create` `get` `send` `schedule` `cancel` `stats` `logs` `archive` `unarchive` | [Campaigns](https://docs.migma.ai/campaigns/overview) |
460
651
  | `migma.validation` | `all` `compatibility` `links` `spelling` `deliverability` | [Validation](https://docs.migma.ai/api-reference/email-validation/run-all-validation-checks) |
461
652
  | `migma.previews` | `create` `get` `getStatus` `getDevice` `getSupportedDevices` `createAndWait` | [Previews](https://docs.migma.ai/api-reference/email-previews/create-email-preview) |
462
653
  | `migma.export` | `getFormats` `getStatus` `html` `mjml` `pdf` `klaviyo` `mailchimp` `hubspot` | [Export](https://docs.migma.ai/api-reference/export/list-export-formats) |
463
- | `migma.domains` | `create` `list` `get` `verify` `update` `remove` `checkAvailability` `listManaged` `createManaged` `removeManaged` | [Domains](https://docs.migma.ai/api-reference/domains/list-domains) |
654
+ | `migma.domains` | `create` `list` `get` `verify` `update` `remove` `checkAvailability` `listManaged` `createManaged` `removeManaged` `provisionStream` `setup` | [Domains](https://docs.migma.ai/api-reference/domains/list-domains) |
464
655
  | `migma.webhooks` | `create` `list` `get` `update` `remove` `test` `getDeliveries` `getEvents` `getStats` | [Webhooks](https://docs.migma.ai/webhooks) |
465
656
  | `migma.knowledgeBase` | `list` `add` `update` `remove` | [API Ref](https://docs.migma.ai/api-reference/introduction) |
466
657
  | `migma.images` | `add` `update` `remove` `updateLogos` | [API Ref](https://docs.migma.ai/api-reference/introduction) |