{"_id":"@autosend/convex","name":"@autosend/convex","dist-tags":{"latest":"0.4.1"},"versions":{"0.4.1":{"name":"@autosend/convex","description":"Convex component for AutoSend — transactional email queueing, retries, webhook verification, and lifecycle tracking for Convex using AutoSend.","version":"0.4.1","license":"Apache-2.0","author":{"name":"AutoSend","email":"hello@autosend.com","url":"https://autosend.com"},"contributors":[{"name":"mzedstudio","url":"original author, https://github.com/mzedstudio"}],"engines":{"node":">=18.0.0"},"repository":{"type":"git","url":"git+https://github.com/autosendhq/autosend-convex.git"},"homepage":"https://github.com/autosendhq/autosend-convex#readme","bugs":{"url":"https://github.com/autosendhq/autosend-convex/issues"},"publishConfig":{"access":"public"},"keywords":["convex","convex-component","email","autosend","transactional-email","transactional","webhooks","smtp","sendgrid-alternative","resend-alternative"],"type":"module","scripts":{"dev":"npm-run-all --parallel dev:*","dev:backend":"convex dev --typecheck-components","dev:frontend":"cd example && npm run dev","dev:build":"chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npm run build:codegen' --initial","build":"tsc --project ./tsconfig.build.json","build:codegen":"npx convex codegen --component-dir ./src/component && npm run build","build:clean":"rm -rf dist *.tsbuildinfo && npm run build:codegen","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest --clearScreen false","preversion":"npm run build:clean && npm test && npm run typecheck","prepublishOnly":"npm run build:clean && npm test && npm run typecheck"},"main":"./dist/client/index.js","types":"./dist/client/index.d.ts","module":"./dist/client/index.js","exports":{"./package.json":"./package.json",".":{"types":"./dist/client/index.d.ts","default":"./dist/client/index.js"},"./test":"./src/test.ts","./_generated/component.js":{"types":"./dist/component/_generated/component.d.ts"},"./_generated/component":{"types":"./dist/component/_generated/component.d.ts"},"./convex.config.js":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"},"./convex.config":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"}},"peerDependencies":{"convex":"^1.31.7"},"devDependencies":{"@edge-runtime/vm":"^5.0.0","@types/node":"^22.10.5","chokidar-cli":"^3.0.0","convex":"1.31.7","convex-test":"^0.0.41","npm-run-all2":"^8.0.4","typescript":"^5.6.3","vite":"^8.0.10","vitest":"^4.1.7"},"_id":"@autosend/convex@0.4.1","gitHead":"27ec3a82179470bc6fbc2d5ea73350591313876e","_nodeVersion":"20.18.1","_npmVersion":"10.8.2","dist":{"integrity":"sha512-yJuBZJeoNhZ1JTcD4ZnOp4h65TO0RsP/eAdhUhijRZ9YK8oplqRGPFqGm/UlJ43RIDzuWFzchiJ7yTu41klz/w==","shasum":"ba0f54e48d0fe079ee7fa80c059c23ab6a8689ed","tarball":"https://registry.npmjs.org/@autosend/convex/-/convex-0.4.1.tgz","fileCount":110,"unpackedSize":758663,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCj725eeMUuLvq6EDL2dprgha12Ogm7nmsaR495hNVCSgIgGpVV4tIzjwSh6mZ4iBKKuO9RVTdX5LJIbHnfbMY3cFI="}]},"_npmUser":{"name":"yognibende","email":"yogini@peerlist.io"},"directories":{},"maintainers":[{"name":"yognibende","email":"yogini@peerlist.io"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/convex_0.4.1_1779778915698_0.4359056624515154"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-26T07:01:55.508Z","0.4.1":"2026-05-26T07:01:55.864Z","modified":"2026-05-26T07:01:56.084Z"},"maintainers":[{"name":"yognibende","email":"yogini@peerlist.io"}],"description":"Convex component for AutoSend — transactional email queueing, retries, webhook verification, and lifecycle tracking for Convex using AutoSend.","homepage":"https://github.com/autosendhq/autosend-convex#readme","keywords":["convex","convex-component","email","autosend","transactional-email","transactional","webhooks","smtp","sendgrid-alternative","resend-alternative"],"repository":{"type":"git","url":"git+https://github.com/autosendhq/autosend-convex.git"},"contributors":[{"name":"mzedstudio","url":"original author, https://github.com/mzedstudio"}],"author":{"name":"AutoSend","email":"hello@autosend.com","url":"https://autosend.com"},"bugs":{"url":"https://github.com/autosendhq/autosend-convex/issues"},"license":"Apache-2.0","readme":"# AutoSend Convex Component\n\n[![npm version](https://img.shields.io/npm/v/@autosend/convex)](https://www.npmjs.com/package/@autosend/convex)\n[![npm downloads](https://img.shields.io/npm/dw/@autosend/convex)](https://www.npmjs.com/package/@autosend/convex)\n\nA [Convex component](https://docs.convex.dev/components) for transactional email delivery on top of AutoSend, including queueing, retries, idempotency, webhook verification, and delivery lifecycle tracking.\n\n[Live Demo](https://convex-autosend.vercel.app/)\n\n## Features\n\n- Queue-first sending: `sendEmail` and `sendBulk` enqueue email jobs and automatically trigger queue processing.\n- Deterministic idempotency: duplicate requests resolve to the same `emailId`.\n- Retry handling: retryable failures (network, `429`, `5xx`) are retried with configurable backoff.\n- Delivery lifecycle: full status model (`queued`, `sending`, `retrying`, `sent`, `failed`, `canceled`).\n- CC/BCC and recipient names: supports `cc`, `bcc`, `toName`, `fromName`, and `replyToName`.\n- Attachments: inline base64 content or URL-referenced file attachments.\n- Templates: send via `templateId` with `dynamicData` for dynamic content.\n- Unsubscribe groups: optional `unsubscribeGroupId` for suppression list management.\n- Webhook security: HMAC SHA-256 signature validation and timestamp skew protection.\n- Webhook dedupe: duplicate callback deliveries are ignored by `deliveryId`.\n- Status/event persistence: stores webhook events and provider identifiers.\n- Batch status queries: fetch status for multiple emails in a single call via `statusBatch`.\n- Event listing: query webhook events per email via `listEvents`.\n- Safe config reads: `getConfig` returns all non-secret config values (never exposes API key or webhook secret).\n- Test sandbox mode: optional recipient rewriting via `sandboxTo`.\n- Maintenance actions: cleanup for old terminal emails, abandoned sending jobs, and stale webhook delivery records. Supports dry-run preview before executing.\n- Project management: create, list, and delete projects programmatically via Account API Keys (`ASA_` prefix).\n- Contacts management: create, get, upsert, delete, search, and bulk update contacts via provider API.\n- Contact lists: create, list, delete lists; add/remove contacts by ID or email; view list membership.\n\n## Installation\n\n```bash\nnpm install @autosend/convex convex\n```\n\n## Setup\n\n### 1. Register the component\n\n```ts\n// convex/convex.config.ts\nimport { defineApp } from \"convex/server\";\nimport autosend from \"@autosend/convex/convex.config.js\";\n\nconst app = defineApp();\napp.use(autosend, { name: \"autosend\" });\nexport default app;\n```\n\n### 2. Create a client wrapper\n\n```ts\n// convex/email.ts\nimport { AutoSend } from \"@autosend/convex\";\nimport { components } from \"./_generated/api\";\n\nexport const autosend = new AutoSend(components.autosend);\n```\n\n### 3. Configure secrets and runtime settings\n\nSet your environment values in Convex:\n\n```bash\nnpx convex env set AUTOSEND_API_KEY <api-key>\nnpx convex env set AUTOSEND_WEBHOOK_SECRET <webhook-secret>\n```\n\nThen persist component config:\n\n```ts\n// convex/admin.ts\nimport { mutation } from \"./_generated/server\";\nimport { autosend } from \"./email\";\n\nexport const configureAutosend = mutation({\n  args: {},\n  handler: async (ctx) => {\n    await autosend.setConfig(ctx, {\n      config: {\n        autosendApiKey: \"replace-with-your-key\",\n        webhookSecret: \"replace-with-your-webhook-secret\",\n        defaultFrom: \"noreply@example.com\",\n        testMode: true,\n        sandboxTo: [\"sandbox@example.com\"],\n      },\n    });\n  },\n});\n```\n\n### 4. Mount webhook route\n\n```ts\n// convex/http.ts\nimport { httpRouter } from \"convex/server\";\nimport { registerRoutes } from \"@autosend/convex\";\nimport { components } from \"./_generated/api\";\n\nconst http = httpRouter();\nregisterRoutes(http, components.autosend);\nexport default http;\n```\n\nDefault webhook path: `/webhooks/autosend`.\n\n## Usage\n\n### Send an email\n\n```ts\nimport { mutation } from \"./_generated/server\";\nimport { autosend } from \"./email\";\n\nexport const sendWelcome = mutation({\n  args: {},\n  handler: async (ctx) => {\n    return await autosend.sendEmail(ctx, {\n      to: [\"user@example.com\"],\n      toName: \"Jane Doe\",\n      subject: \"Welcome\",\n      html: \"<p>Hello</p>\",\n    });\n  },\n});\n```\n\n`sendEmail` and `sendBulk` enqueue emails and automatically trigger queue processing. The `processQueue` action is available for manual recovery or cron-based sweep, but is not required for normal operation.\n\n### Bulk send\n\n```ts\nawait autosend.sendBulk(ctx, {\n  recipients: [\"a@example.com\", \"b@example.com\"],\n  subject: \"Update\",\n  html: \"<p>News</p>\",\n});\n```\n\n### Bulk send with per-recipient data\n\nUse `recipientData` to interpolate `{{placeholders}}` in `subject`, `html`, and `text` per recipient:\n\n```ts\nawait autosend.sendBulk(ctx, {\n  recipients: [\"alice@example.com\", \"bob@example.com\"],\n  recipientData: {\n    \"alice@example.com\": { name: \"Alice\", role: \"admin\" },\n    \"bob@example.com\": { name: \"Bob\", role: \"member\" },\n  },\n  subject: \"Welcome, {{name}}\",\n  html: \"<p>Hi {{name}}, you are now a {{role}}.</p>\",\n  text: \"Hi {{name}}, you are now a {{role}}.\",\n});\n```\n\nWhen `recipientData` is provided, per-recipient data is also used as `dynamicData` for that recipient (overriding the shared `dynamicData` if both are set).\n\n### CC, BCC, and attachments\n\n```ts\nawait autosend.sendEmail(ctx, {\n  to: [\"user@example.com\"],\n  cc: [{ email: \"team@example.com\", name: \"Team\" }],\n  bcc: [{ email: \"archive@example.com\" }],\n  subject: \"Report\",\n  html: \"<p>See attached.</p>\",\n  attachments: [\n    { filename: \"report.pdf\", fileUrl: \"https://example.com/report.pdf\" },\n    {\n      filename: \"data.csv\",\n      content: \"base64-encoded-content\",\n      contentType: \"text/csv\",\n    },\n  ],\n  unsubscribeGroupId: \"marketing\",\n});\n```\n\n### Templates\n\n```ts\nawait autosend.sendEmail(ctx, {\n  to: [\"user@example.com\"],\n  templateId: \"welcome-template-id\",\n  dynamicData: { firstName: \"Jane\", plan: \"Pro\" },\n});\n```\n\n### Status, batch status, and events\n\n```ts\n// Single email status\nconst email = await autosend.status(ctx, { emailId });\n\n// Batch status for multiple emails\nconst statuses = await autosend.statusBatch(ctx, {\n  emailIds: [emailId1, emailId2, emailId3],\n});\n\n// List webhook events for an email\nconst events = await autosend.listEvents(ctx, { emailId, limit: 20 });\n\n// Cancel a queued or retrying email\nconst { canceled } = await autosend.cancelEmail(ctx, { emailId });\n```\n\n### Cleanup\n\n```ts\n// Dry-run preview (no deletions)\nconst preview = await autosend.cleanupOldEmails(ctx, { dryRun: true });\n\n// Delete old terminal emails (default: older than 7 days)\nawait autosend.cleanupOldEmails(ctx, { olderThanMs: 7 * 24 * 60 * 60 * 1000 });\n\n// Recover abandoned sending jobs (default: stale after 15 min)\nawait autosend.cleanupAbandonedEmails(ctx, { staleAfterMs: 15 * 60 * 1000 });\n\n// Prune old webhook delivery records (default: older than 7 days)\nawait autosend.cleanupOldDeliveries(ctx, {\n  olderThanMs: 7 * 24 * 60 * 60 * 1000,\n});\n```\n\n## API Reference\n\n### `AutoSend` class\n\n| Method                                     | Context  | Returns                                                                | Notes                                                                 |\n| ------------------------------------------ | -------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------- |\n| `sendEmail(ctx, args)`                     | mutation | `{ emailId, deduped }`                                                 | Enqueues and auto-processes one email                                 |\n| `sendBulk(ctx, args)`                      | mutation | `{ emailIds, acceptedCount }`                                          | Enqueues and auto-processes up to 100 recipients                      |\n| `status(ctx, { emailId })`                 | query    | `EmailDoc \\| null`                                                     | Reads current email state                                             |\n| `statusBatch(ctx, { emailIds })`           | query    | `(EmailDoc \\| null)[]`                                                 | Batch status for multiple emails                                      |\n| `listEvents(ctx, { emailId, limit? })`     | query    | `EmailEvent[]`                                                         | Webhook events for an email (newest first, default limit 50, max 200) |\n| `cancelEmail(ctx, { emailId })`            | mutation | `{ canceled }`                                                         | Allowed only from `queued` or `retrying`                              |\n| `setConfig(ctx, { config, replace? })`     | mutation | `{ created }`                                                          | Merge by default, full replace when `replace: true`                   |\n| `getConfig(ctx)`                           | query    | `SafeConfig`                                                           | All non-secret config plus `hasApiKey`/`hasWebhookSecret` booleans    |\n| `processQueue(ctx, { batchSize? })`        | action   | `{ processedCount, sentCount, retriedCount, failedCount, hasMoreDue }` | Sends due queued/retrying emails                                      |\n| `cleanupOldEmails(ctx, args)`              | action   | `{ deletedCount, emailIds, hasMore }`                                  | Removes old terminal emails. Supports `dryRun`                        |\n| `cleanupAbandonedEmails(ctx, args)`        | action   | `{ recoveredCount, failedCount, emailIds, hasMore }`                   | Recovers stale `sending` jobs. Supports `dryRun`                      |\n| `cleanupOldDeliveries(ctx, args)`          | action   | `{ deletedCount, hasMore }`                                            | Removes old webhook delivery dedup records                            |\n| `handleCallback(ctx, args)`                | action   | `{ ok, eventType, emailId?, duplicate?, error? }`                      | Verifies and applies webhook callback                                 |\n| `contacts.create(ctx, args)`               | action   | `{ contact }`                                                          | Create a new contact                                                  |\n| `contacts.get(ctx, args)`                  | action   | `{ contact }`                                                          | Get contact by ID                                                     |\n| `contacts.upsert(ctx, args)`               | action   | `{ contact }`                                                          | Create or update contact by email                                     |\n| `contacts.delete(ctx, args)`               | action   | `{ success, message? }`                                                | Delete contact by ID                                                  |\n| `contacts.deleteByUserId(ctx, args)`       | action   | `{ success, message? }`                                                | Delete contact by user ID                                             |\n| `contacts.removeByEmails(ctx, args)`       | action   | `{ success, message? }`                                                | Remove contacts by email addresses                                    |\n| `contacts.search(ctx, args)`               | action   | `{ contacts }`                                                         | Search contacts by email addresses                                    |\n| `contacts.getUnsubscribeGroups(ctx, args)` | action   | `{ groups }`                                                           | Get unsubscribe groups for a contact                                  |\n| `contacts.bulkUpdate(ctx, args)`           | action   | `{ successCount, failedCount, totalCount }`                            | Bulk update up to 500 contacts                                        |\n| `lists.list(ctx, args?)`                   | action   | `{ contactLists }`                                                     | List all contact lists                                                |\n| `lists.get(ctx, args)`                     | action   | `{ contactList }`                                                      | Get contact list by ID                                                |\n| `lists.create(ctx, args)`                  | action   | `{ contactList }`                                                      | Create a new contact list                                             |\n| `lists.delete(ctx, args)`                  | action   | `{ success, message }`                                                 | Delete a contact list                                                 |\n| `lists.getContacts(ctx, args)`             | action   | `{ contacts, pagination }`                                             | Get paginated contacts in a list                                      |\n| `lists.addContacts(ctx, args)`             | action   | `{ success, added, created, ... }`                                     | Add contacts to a list                                                |\n| `lists.removeContacts(ctx, args)`          | action   | `{ success, removed, ... }`                                            | Remove contacts from a list                                           |\n\n### `sendEmail` arguments\n\n| Field                | Type                 | Required    | Notes                                                         |\n| -------------------- | -------------------- | ----------- | ------------------------------------------------------------- |\n| `to`                 | `string[]`           | yes         | Must contain exactly one recipient                            |\n| `toName`             | `string`             | no          | Display name for the recipient                                |\n| `from`               | `string`             | no          | Sender address (falls back to `defaultFrom` in config)        |\n| `fromName`           | `string`             | no          | Display name for the sender                                   |\n| `replyTo`            | `string`             | no          | Reply-to address (falls back to `defaultReplyTo` in config)   |\n| `replyToName`        | `string`             | no          | Display name for reply-to                                     |\n| `cc`                 | `{ email, name? }[]` | no          | Carbon copy recipients                                        |\n| `bcc`                | `{ email, name? }[]` | no          | Blind carbon copy recipients                                  |\n| `subject`            | `string`             | conditional | Required unless `templateId` is provided                      |\n| `html`               | `string`             | conditional | HTML body; required unless `templateId` or `text` is provided |\n| `text`               | `string`             | conditional | Plain text body                                               |\n| `templateId`         | `string`             | no          | Provider template identifier                                  |\n| `dynamicData`        | `any`                | no          | Template variables/merge fields                               |\n| `attachments`        | `Attachment[]`       | no          | File attachments (see below)                                  |\n| `metadata`           | `any`                | no          | Arbitrary metadata stored with the email                      |\n| `idempotencyKey`     | `string`             | no          | Explicit dedup key (auto-generated from payload if omitted)   |\n| `unsubscribeGroupId` | `string`             | no          | Suppression group identifier                                  |\n\n### `sendBulk` arguments\n\nSame as `sendEmail` except:\n\n- `recipients: string[]` replaces `to` (up to 100 recipients)\n- `recipientData?: Record<string, Record<string, unknown>>` — per-recipient merge fields keyed by email address; interpolates `{{placeholders}}` in `subject`, `html`, and `text`\n- `idempotencyKeyPrefix: string` replaces `idempotencyKey`\n- No `toName` (one email per recipient)\n\n### `Attachment` format\n\n| Field         | Type     | Required    | Notes                                                             |\n| ------------- | -------- | ----------- | ----------------------------------------------------------------- |\n| `filename`    | `string` | yes         | Name of the attached file                                         |\n| `content`     | `string` | conditional | Base64-encoded content (provide `content` or `fileUrl`, not both) |\n| `fileUrl`     | `string` | conditional | URL to fetch the file from                                        |\n| `contentType` | `string` | no          | MIME type (e.g., `application/pdf`)                               |\n| `disposition` | `string` | no          | `attachment` or `inline`                                          |\n| `description` | `string` | no          | File description                                                  |\n\n### `registerRoutes(http, component, options?)`\n\nMounts webhook route handling:\n\n- Default path: `/webhooks/autosend`\n- Optional override: `options.path`\n- Optional secret override: `options.webhookSecret`\n\nRequired headers:\n\n- `x-webhook-signature`\n- `x-webhook-event`\n- `x-webhook-delivery-id`\n- `x-webhook-timestamp`\n\n## Config Reference\n\n| Field                       | Type                    | Default                    | Description                                                        |\n| --------------------------- | ----------------------- | -------------------------- | ------------------------------------------------------------------ |\n| `autosendApiKey`            | `string`                | unset                      | Bearer token for AutoSend API                                      |\n| `webhookSecret`             | `string`                | unset                      | HMAC secret for webhook verification                               |\n| `testMode`                  | `boolean`               | `true`                     | Rewrites recipients to `sandboxTo`                                 |\n| `defaultFrom`               | `string`                | unset                      | Fallback sender address                                            |\n| `defaultReplyTo`            | `string`                | unset                      | Fallback reply-to address                                          |\n| `sandboxTo`                 | `string[]`              | `[]`                       | Target recipients used in test mode                                |\n| `rateLimitRps`              | `number`                | `2`                        | Max sends per queue run                                            |\n| `retryDelaysMs`             | `number[]`              | `[5000,10000,20000]`       | Retry delay schedule (ms)                                          |\n| `maxAttempts`               | `number`                | `4`                        | Total attempts including first try                                 |\n| `sendBatchSize`             | `number`                | `25`                       | Max queue items selected per run                                   |\n| `cleanupBatchSize`          | `number`                | `100`                      | Max items per cleanup batch                                        |\n| `cleanupOldEmailsMs`        | `number`                | `604800000` (7 days)       | Age threshold for deleting terminal emails                         |\n| `cleanupAbandonedMs`        | `number`                | `900000` (15 min)          | Stale threshold for recovering abandoned `sending` jobs            |\n| `cleanupDeliveriesMs`       | `number`                | `604800000` (7 days)       | Age threshold for pruning webhook delivery records                 |\n| `providerCompatibilityMode` | `\"strict\" \\| \"lenient\"` | `\"strict\"`                 | Response parsing strictness for provider variance                  |\n| `autosendBaseUrl`           | `string`                | `https://api.autosend.com` | Base URL for provider API                                          |\n| `projectId`                 | `string`                | unset                      | Project ID for Account API Keys (required with `ASA_` prefix keys) |\n\n### Multi-Project Support\n\nAutoSend supports two API key types:\n\n- **Project API Key** (`AS_` prefix): Scoped to a single project. No additional configuration needed.\n- **Account API Key** (`ASA_` prefix): Cross-project scope. Requires `projectId` to be set.\n\nWhen `projectId` is configured, every API request includes an `x-project-id` header. If you use an Account API Key without setting `projectId`, the component throws a clear error at send time.\n\n```ts\n// Single-project setup (Project API Key) — no projectId needed\nawait autosend.setConfig(ctx, {\n  config: {\n    autosendApiKey: \"AS_your_project_key\",\n    defaultFrom: \"noreply@example.com\",\n  },\n});\n\n// Multi-project setup (Account API Key) — projectId required\nawait autosend.setConfig(ctx, {\n  config: {\n    autosendApiKey: \"ASA_your_account_key\",\n    projectId: \"proj_abc123\",\n    defaultFrom: \"noreply@example.com\",\n  },\n});\n```\n\nFor multiple projects from a single Convex deployment, mount the component once per project:\n\n```ts\n// convex/convex.config.ts\nconst app = defineApp();\napp.use(autosend, { name: \"marketing\" });\napp.use(autosend, { name: \"transactional\" });\nexport default app;\n```\n\nEach instance gets its own config with its own `projectId`.\n\n### Projects API\n\nManage projects programmatically using an Account API Key (`ASA_` prefix). These methods call the AutoSend Projects API and require admin-level access.\n\n```ts\n// List all projects in your organization\nconst { projects } = await autosend.listProjects(ctx);\n\n// Create a new project\nconst { project } = await autosend.createProject(ctx, {\n  name: \"Marketing Emails\",\n  domain: \"mail.example.com\", // optional\n  regionKey: \"us-east-1\", // optional: us-east-1, us-east-2, ap-south-1\n});\n\n// Use the new project's ID for email sending\nawait autosend.setConfig(ctx, {\n  config: {\n    projectId: project.id,\n  },\n});\n\n// Delete a project (irreversible — removes all associated resources)\nawait autosend.deleteProject(ctx, {\n  projectId: \"60d5ec49f1b2c72d9c8b1234\",\n});\n```\n\nAll three methods read the API key from config by default. You can also pass an `apiKey` override:\n\n```ts\nconst { projects } = await autosend.listProjects(ctx, {\n  apiKey: \"ASA_your_account_key\",\n});\n```\n\nProject-scoped API keys (`AS_` prefix) cannot call these endpoints — only Account API Keys (`ASA_` prefix) are accepted.\n\n### Contacts API\n\nManage contacts in your AutoSend project. All methods are available under `autosend.contacts`.\n\n```ts\n// Create a contact\nconst { contact } = await autosend.contacts.create(ctx, {\n  email: \"jane@example.com\",\n  firstName: \"Jane\",\n  lastName: \"Doe\",\n  listIds: [\"list_abc123\"], // optional: add to lists on creation\n  customFields: { plan: \"pro\" }, // optional\n});\n\n// Get a contact by ID\nconst { contact } = await autosend.contacts.get(ctx, {\n  contactId: \"ct_abc123\",\n});\n\n// Upsert — create or update by email\nconst { contact } = await autosend.contacts.upsert(ctx, {\n  email: \"jane@example.com\",\n  firstName: \"Jane\",\n  lastName: \"Doe\",\n});\n\n// Search contacts by email addresses\nconst { contacts } = await autosend.contacts.search(ctx, {\n  emails: [\"jane@example.com\", \"bob@example.com\"],\n});\n\n// Bulk update contacts (up to 500)\nconst result = await autosend.contacts.bulkUpdate(ctx, {\n  contacts: [\n    { email: \"jane@example.com\", firstName: \"Jane\" },\n    { email: \"bob@example.com\", firstName: \"Bob\" },\n  ],\n  runWorkflow: true, // optional: trigger automations\n});\n\n// Delete a contact\nawait autosend.contacts.delete(ctx, { contactId: \"ct_abc123\" });\n\n// Delete by user ID\nawait autosend.contacts.deleteByUserId(ctx, { userId: \"user_123\" });\n\n// Remove contacts by email addresses\nawait autosend.contacts.removeByEmails(ctx, {\n  emails: [\"jane@example.com\"],\n});\n\n// Get unsubscribe groups for a contact\nconst { groups } = await autosend.contacts.getUnsubscribeGroups(ctx, {\n  contactId: \"ct_abc123\",\n});\n```\n\nAll contacts methods accept optional `apiKey` and `projectId` overrides.\n\n### Contact Lists API\n\nManage contact lists and their membership. All methods are available under `autosend.lists`.\n\n```ts\n// Create a list\nconst { contactList } = await autosend.lists.create(ctx, {\n  name: \"Newsletter Subscribers\",\n  description: \"Monthly newsletter recipients\",\n});\n\n// List all lists (optionally filter by type)\nconst { contactLists } = await autosend.lists.list(ctx, { type: \"list\" });\n\n// Get a list by ID\nconst { contactList } = await autosend.lists.get(ctx, {\n  listId: \"cl_abc123\",\n});\n\n// View contacts in a list (paginated)\nconst { contacts, pagination } = await autosend.lists.getContacts(ctx, {\n  listId: \"cl_abc123\",\n  page: 1,\n  limit: 50,\n  email: \"jane@\", // optional: filter by email\n});\n\n// Add contacts to a list (by email or contact ID)\nconst result = await autosend.lists.addContacts(ctx, {\n  listId: \"cl_abc123\",\n  emails: [\"jane@example.com\", \"bob@example.com\"],\n});\n// Or by contact IDs:\nawait autosend.lists.addContacts(ctx, {\n  listId: \"cl_abc123\",\n  contactIds: [\"ct_abc123\", \"ct_def456\"],\n});\n\n// Remove contacts from a list\nawait autosend.lists.removeContacts(ctx, {\n  listId: \"cl_abc123\",\n  emails: [\"jane@example.com\"],\n});\n\n// Delete a list (contacts are not deleted)\nawait autosend.lists.delete(ctx, { listId: \"cl_abc123\" });\n```\n\nAll list methods accept optional `apiKey` and `projectId` overrides.\n\n### `getConfig` return value\n\n`getConfig` returns a `SafeConfig` object containing all non-secret configuration values plus two booleans indicating whether secrets are set:\n\n- All fields above except `autosendApiKey` and `webhookSecret`\n- `hasApiKey: boolean` — whether `autosendApiKey` is configured\n- `hasWebhookSecret: boolean` — whether `webhookSecret` is configured\n\n## Email Lifecycle\n\n### Statuses\n\n- `queued`: accepted and waiting to be claimed by processor.\n- `sending`: currently claimed by queue processor.\n- `retrying`: previous attempt failed and next retry is scheduled.\n- `sent`: successfully accepted by provider.\n- `failed`: terminal failure (retries exhausted or non-retryable).\n- `canceled`: canceled before send.\n\n### Retry policy\n\n- Retries on network failures, HTTP `429`, and HTTP `5xx`.\n- Default delays: `5000`, `10000`, `20000` ms.\n- Default `maxAttempts`: `4` total attempts.\n\n## Webhook Behavior\n\n- Signature: HMAC SHA-256 over raw body.\n- Timestamp skew limit: 2 minutes.\n- Dedupe key: `deliveryId`.\n- All callback payloads are recorded to `emailEvents`.\n\nEvent mapping:\n\n| Event type                                            | Effect                                      |\n| ----------------------------------------------------- | ------------------------------------------- |\n| `email.sent`, `email.delivered`                       | Mark/keep as sent, update provider status   |\n| `email.deferred`                                      | Provider status update only                 |\n| `email.bounced`, `email.spam_reported`                | Mark failed if not already terminal         |\n| `email.opened`, `email.clicked`, `email.unsubscribed` | Event recorded, provider status update only |\n\n## Direct Component Functions\n\nIf you do not use the `AutoSend` wrapper, the component exposes:\n\n- `config.setConfig`\n- `config.getConfig`\n- `emails.sendEmail`\n- `emails.sendBulk`\n- `emails.cancelEmail`\n- `queries.status`\n- `queries.statusBatch`\n- `queries.listEvents`\n- `queue.processQueue`\n- `cleanup.cleanupOldEmails`\n- `cleanup.cleanupAbandonedEmails`\n- `cleanup.cleanupOldDeliveries`\n- `webhooks.handleCallback`\n- `projects.listProjects`\n- `projects.createProject`\n- `projects.deleteProject`\n- `contacts.createContact`\n- `contacts.getContact`\n- `contacts.upsertContact`\n- `contacts.deleteContact`\n- `contacts.deleteContactByUserId`\n- `contacts.removeContactsByEmails`\n- `contacts.searchContactsByEmails`\n- `contacts.getUnsubscribeGroups`\n- `contacts.bulkUpdateContacts`\n- `contactLists.listContactLists`\n- `contactLists.getContactList`\n- `contactLists.createContactList`\n- `contactLists.deleteContactList`\n- `contactLists.getContactListContacts`\n- `contactLists.addContactsToList`\n- `contactLists.removeContactsFromList`\n\n## Exported Types and Validators\n\nThe package exports TypeScript types and Convex validators for use in your own functions:\n\n```ts\nimport type {\n  // Email\n  EmailStatus, // \"queued\" | \"retrying\" | \"sending\" | \"sent\" | \"failed\" | \"canceled\"\n  SendEmailArgs, // Arguments for sendEmail\n  SendBulkArgs, // Arguments for sendBulk\n  EmailRecipient, // { email: string; name?: string }\n  Attachment, // Attachment object shape\n  ConfigUpdate, // Fields accepted by setConfig\n  SafeConfig, // Return type of getConfig\n  DeliveryCleanupResult, // Return type of cleanupOldDeliveries\n  ProviderCompatibilityMode, // \"strict\" | \"lenient\"\n  // Projects\n  Project, // Project object shape\n  ProjectDomain, // Project domain with verification status\n  CreateProjectResult,\n  ListProjectsResult,\n  DeleteProjectResult,\n  // Contacts\n  Contact, // Contact object shape\n  CreateContactArgs,\n  CreateContactResult,\n  GetContactResult,\n  UpsertContactResult,\n  DeleteContactResult,\n  DeleteContactByUserIdResult,\n  RemoveContactsByEmailsResult,\n  SearchContactsResult,\n  GetUnsubscribeGroupsResult,\n  UnsubscribeGroup,\n  BulkUpdateContactsResult,\n  // Contact Lists\n  ContactList, // Contact list object shape\n  ContactListType, // \"list\" | \"segment\"\n  Pagination, // { page, limit, total, pages }\n  ListContactListsResult,\n  GetContactListResult,\n  CreateContactListResult,\n  DeleteContactListResult,\n  GetContactListContactsResult,\n  AddContactsToListResult,\n  RemoveContactsFromListResult,\n  BulkAddError,\n} from \"@autosend/convex\";\n\n// Convex validators (for use in your own function args/returns)\nimport {\n  // Email\n  emailStatusValidator,\n  sendEmailArgsValidator,\n  sendBulkArgsValidator,\n  sendResultValidator,\n  sendBulkResultValidator,\n  cancelResultValidator,\n  processQueueResultValidator,\n  cleanupResultValidator,\n  abandonedCleanupResultValidator,\n  deliveryCleanupResultValidator,\n  attachmentValidator,\n  emailRecipientValidator,\n  configUpdateValidator,\n  safeConfigValidator,\n  webhookHandleResultValidator,\n  providerCompatibilityModeValidator,\n  // Projects\n  projectValidator,\n  projectDomainValidator,\n  createProjectArgsValidator,\n  createProjectResultValidator,\n  listProjectsResultValidator,\n  deleteProjectResultValidator,\n  // Contacts\n  contactValidator,\n  createContactArgsValidator,\n  createContactResultValidator,\n  getContactArgsValidator,\n  getContactResultValidator,\n  upsertContactArgsValidator,\n  upsertContactResultValidator,\n  deleteContactArgsValidator,\n  deleteContactResultValidator,\n  deleteContactByUserIdArgsValidator,\n  deleteContactByUserIdResultValidator,\n  removeContactsByEmailsArgsValidator,\n  removeContactsByEmailsResultValidator,\n  searchContactsArgsValidator,\n  searchContactsResultValidator,\n  getUnsubscribeGroupsArgsValidator,\n  getUnsubscribeGroupsResultValidator,\n  unsubscribeGroupValidator,\n  bulkUpdateContactsArgsValidator,\n  bulkUpdateContactsResultValidator,\n  // Contact Lists\n  contactListValidator,\n  contactListTypeValidator,\n  paginationValidator,\n  listContactListsArgsValidator,\n  listContactListsResultValidator,\n  getContactListArgsValidator,\n  getContactListResultValidator,\n  createContactListArgsValidator,\n  createContactListResultValidator,\n  deleteContactListArgsValidator,\n  deleteContactListResultValidator,\n  getContactListContactsArgsValidator,\n  getContactListContactsResultValidator,\n  addContactsToListArgsValidator,\n  addContactsToListResultValidator,\n  removeContactsFromListArgsValidator,\n  removeContactsFromListResultValidator,\n  bulkAddErrorValidator,\n} from \"@autosend/convex\";\n```\n\n## Testing\n\nUse `@autosend/convex/test` with `convex-test`:\n\n```ts\nimport { convexTest } from \"convex-test\";\nimport { register } from \"@autosend/convex/test\";\nimport schema from \"./schema\";\n\nconst modules = import.meta.glob(\"./**/*.ts\");\n\nconst t = convexTest(schema, modules);\nregister(t, \"autosend\");\n```\n\n## License\n\nApache-2.0\n\n## Credits\n\nOriginally created by [mzedstudio](https://github.com/mzedstudio) as `@autosend/convex`. Adopted and maintained by [AutoSend](https://autosend.com) as the official Convex integration.\n","readmeFilename":"README.md","_rev":"1-b710c952ae0908b992c7baeefee94d2e"}