Skip to content

Find a component

Loading the directory…

↑ ↓ to select · Enter to open · Esc to closeBrowse all →
Contributor login

Lettermint

A Lettermint component for Convex.

Upstream source
git clone https://github.com/pierre-H/convex-lettermint.git
cd convex-lettermint

This upstream package has not been adapted or qualified for Bijection. Read the original documentation before adopting it.

Description

A Lettermint component for Convex.

Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.

Available to evaluate

Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.

Getting started

  1. Get the source at the revision above and read its package documentation.
  2. Check the required Bijection SDK version, dependencies, and external service credentials.
  3. Follow the package setup instructions and the guide to using components.
  4. Exercise the component in your deployment according to your project’s verification rules.

Open source

Maintained source: pierre-H/convex-lettermint. License: See repository. Consult the repository for license terms, notices, and contributor attribution.

From the upstream publisher

A Lettermint component for Convex.

Explore the demo ↗

Upstream overview

From the upstream package documentation.

convex-lettermint integrates the Lettermint transactional email service with Convex, providing a durable email queue backed by Convex workpools. Emails are batched to Lettermint's /send/batch endpoint with idempotency keys derived from internal Convex email IDs, and delivery events are tracked via verified HMAC-signed webhooks. The component handles rate limiting, retry backoff, and status tracking out of the box.

Use cases

From the upstream package documentation.

  • Sending transactional emails (welcome, password reset, notifications) from Convex mutations or actions with durable retry guarantees
  • Tracking email delivery lifecycle events (delivered, bounced, opened, clicked) by wiring Lettermint webhooks to a Convex HTTP endpoint
  • Queuing high volumes of outbound emails without overwhelming the Lettermint API, using built-in rate limiting and batching
  • Cancelling pending emails before they are dispatched, or checking per-email status including bounce and complaint flags
  • Sending emails with attachments or inline images by bypassing the queue with sendEmailManually and calling the Lettermint API directly inside a Convex action

How it works

From the upstream package documentation.

After installing the package and adding app.use(lettermint) in convex/convex.config.ts, you instantiate a Lettermint client with new Lettermint(components.lettermint, options). Calling lettermint.sendEmail(ctx, params) enqueues the message and returns a branded EmailId. The component batches queued messages to Lettermint's /send/batch endpoint, using the internal EmailId as the Idempotency-Key to prevent duplicate sends across retries.

Failed API calls are retried using Convex workpools with configurable initialBackoffMs and retryAttempts options. testMode defaults to true, restricting delivery to Lettermint test domain addresses until explicitly disabled in LettermintOptions.

Webhook events are handled by mounting an HTTP endpoint in convex/http.ts that calls lettermint.handleLettermintEventWebhook(ctx, req). The component verifies the X-Lettermint-Signature HMAC-SHA256 header and rejects stale or invalid requests before routing events to your registered onEmailEvent mutation callback, which receives a typed { id: EmailId; event: EmailEvent } argument.

Data retention is managed manually via cleanupOldEmails and cleanupAbandonedEmails functions exposed on the component, which can be scheduled from a Convex cron job. For emails requiring attachments or features outside the batch API, sendEmailManually accepts a callback that constructs and fires the raw Lettermint API request, while still recording the email in the component's tracking layer.

Upstream package FAQ

Does convex-lettermint support email templates?

convex-lettermint does not support Lettermint template IDs because the Lettermint Sending API schema used by the component does not include a template field. You should generate HTML before calling sendEmail, either with plain string interpolation or with React Email rendered inside a Convex Node action.

How do I track whether an email was opened or clicked?

convex-lettermint returns a branded EmailId from sendEmail. Call lettermint.status(ctx, emailId) to read the current delivery state including the bounced, failed, complained, opened, and clicked boolean fields. Status is updated when Lettermint webhook events arrive and are processed by the component.

How do I cancel a queued email before it is sent?

Call lettermint.cancelEmail(ctx, emailId) with the EmailId returned from sendEmail. If the email has already been submitted to the Lettermint API it cannot be cancelled. Cancellation only works while the email is still in the component's internal queue.

What environment variables does convex-lettermint require?

convex-lettermint reads LETTERMINT_PROJECT_TOKEN for the Lettermint Project API token and LETTERMINT_WEBHOOK_SECRET for webhook signature verification. Both values can alternatively be supplied directly via the projectToken and webhookSecret fields in the LettermintOptions constructor argument, which avoids reading from environment variables.

How do I clean up old email records stored by the component?

convex-lettermint exposes cleanupOldEmails and cleanupAbandonedEmails functions on the component. Schedule them from a Convex cron job or run them from the Convex dashboard, passing an olderThan millisecond threshold. For example, passing { olderThan: 604800000 } removes emails finalized more than one week ago.

Use with agents and CLI

Give your coding agent the component’s source identity and setup guidance.

agent-prompt.md
Help me integrate Lettermint with Bijection.

Read: https://bijection.com/components/convex-lettermint/SKILL.md

Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.

Upstream README.md

View repository ↗

Documentation from pierre-H/convex-lettermint, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.

Lettermint Convex Component

This component integrates the Lettermint transactional email service with Convex.

Note: This is an unofficial implementation. Lettermint and related trademarks are the property of Lettermint, and all trademark rights are reserved by Lettermint. This project is not affiliated with, endorsed by, or sponsored by Lettermint.

Features:

  • Queueing: Send many emails quickly; the component processes them durably.
  • Batching: Sends queued emails to Lettermint's /send/batch endpoint.
  • Durable execution: Uses Convex workpools to retry temporary failures.
  • Idempotency: Uses Convex email IDs as Lettermint Idempotency-Key values.
  • Rate limiting: Limits calls to the Lettermint Sending API.
  • Webhooks: Verifies Lettermint HMAC signatures and tracks message status.

See example for a demo of how to incorporate this component into your application.

Installation

npm install convex-lettermint

Get Started

Create a Lettermint project and create a Project API

Add the component to your Convex app via convex/convex.config.ts:

import { defineApp } from "convex/server";
import lettermint from "convex-lettermint/convex.config.js";

const app = defineApp();
app.use(lettermint);

export default app;

Use it from a Convex function:

import { components } from "./_generated/api";
import { Lettermint } from "convex-lettermint";
import { internalMutation } from "./_generated/server";

export const lettermint = new Lettermint(components.lettermint, {});

export const sendTestEmail = internalMutation({
  handler: async (ctx) => {
    await lettermint.sendEmail(ctx, {
      from: "Me <test@mydomain.com>",
      to: "ok@testing.lettermint.co",
      subject: "Hi there",
      html: "This is a test email",
    });
  },
});

testMode is enabled by default. In test mode, recipients must use the Lettermint test domain @testing.lettermint.co. Examples include:

  • ok@testing.lettermint.co
  • softbounce@testing.lettermint.co
  • hardbounce@testing.lettermint.co
  • spamcomplaint@testing.lettermint.co
  • dsn@testing.lettermint.co

Set testMode: false in LettermintOptions before sending to real recipients.

Webhooks

The component can receive Lettermint delivery, bounce, complaint, open, and click events.

Mount an HTTP endpoint in convex/http.ts:

import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { lettermint } from "./sendEmails";

const http = httpRouter();

http.route({
  path: "/lettermint-webhook",
  method: "POST",
  handler: httpAction(async (ctx, req) => {
    return await lettermint.handleLettermintEventWebhook(ctx, req);
  }),
});

export default http;

Create a webhook in the Lettermint dashboard for that URL and subscribe to the message events you need. Copy the webhook secret and set it to LETTERMINT_WEBHOOK_SECRET in your Convex deployment environment.

Lettermint signs webhooks with X-Lettermint-Signature. The component verifies the HMAC-SHA256 signature and rejects invalid or stale webhook requests.

Event Handler

Register a mutation to run when email events arrive:

import { components, internal } from "./_generated/api";
import { internalMutation } from "./_generated/server";
import { Lettermint, vOnEmailEventArgs } from "convex-lettermint";

export const lettermint = new Lettermint(components.lettermint, {
  onEmailEvent: internal.example.handleEmailEvent,
});

export const handleEmailEvent = internalMutation({
  args: vOnEmailEventArgs,
  handler: async (ctx, args) => {
    // args provides { id: EmailId; event: EmailEvent; }
    if (args.event.event === "message.delivered") {
      // Handle delivery.
    }
  },
});

Supported outbound Lettermint events include message.created, message.sent, message.delivered, message.hard_bounced, message.soft_bounced, message.spam_complaint, message.failed, message.suppressed, message.policy_rejected, message.unsubscribed, message.opened, and message.clicked. webhook.test is acknowledged and ignored.

Options

The LettermintOptions constructor argument customizes component behavior:

  • projectToken: Provide the Lettermint Project API token instead of reading LETTERMINT_PROJECT_TOKEN.
  • webhookSecret: Provide the webhook secret instead of reading LETTERMINT_WEBHOOK_SECRET.
  • testMode: Only allow delivery to Lettermint test addresses. Defaults to true.
  • initialBackoffMs: Initial retry backoff for failed API calls. Defaults to 30 seconds.
  • retryAttempts: Maximum retry attempts for failed API calls. Defaults to 5.
  • onEmailEvent: Mutation callback for Lettermint webhook events.

Sending Parameters

sendEmail supports the batch-safe Lettermint fields:

  • from
  • to
  • cc
  • bcc
  • subject
  • html
  • text
  • replyTo
  • headers
  • route
  • tag
  • metadata
  • settings.trackOpens
  • settings.trackClicks

Example:

await lettermint.sendEmail(ctx, {
  from: "Notifications <notifications@mydomain.com>",
  to: "ok@testing.lettermint.co",
  subject: "Welcome!",
  html: "<p>Welcome to our app.</p>",
  route: "transactional",
  tag: "welcome-email",
  metadata: {
    user_id: "user_123",
  },
  settings: {
    trackOpens: false,
    trackClicks: true,
  },
});

Templates are not supported by the Lettermint Sending API schema used by this component. Generate dynamic HTML yourself or use React Email before calling sendEmail.

Tracking And Status

sendEmail returns a branded EmailId.

Use it to check status or cancel queued emails:

const emailId = await lettermint.sendEmail(ctx, {
  from: "Me <test@mydomain.com>",
  to: "ok@testing.lettermint.co",
  subject: "Hi",
  text: "Hello",
});

const emailStatus = await lettermint.status(ctx, emailId);
if (emailStatus) {
  console.log(emailStatus.status);
  console.log(emailStatus.bounced);
  console.log(emailStatus.failed);
  console.log(emailStatus.complained);
  console.log(emailStatus.opened);
  console.log(emailStatus.clicked);
  console.log(emailStatus.errorMessage);
}

await lettermint.cancelEmail(ctx, emailId);

If an email has already been sent to Lettermint, it cannot be cancelled.

Data Retention

This component retains finalized emails and delivery events. You can clear old data with cleanupOldEmails and cleanupAbandonedEmails, either from the Convex dashboard or a cron job:

import { cronJobs } from "convex/server";
import { components, internal } from "./_generated/api.js";
import { internalMutation } from "./_generated/server.js";

const crons = cronJobs();
crons.interval(
  "Remove old emails from the Lettermint component",
  { hours: 1 },
  internal.crons.cleanupLettermint,
);

const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
export const cleanupLettermint = internalMutation({
  args: {},
  handler: async (ctx) => {
    await ctx.scheduler.runAfter(0, components.lettermint.lib.cleanupOldEmails, {
      olderThan: ONE_WEEK_MS,
    });
    await ctx.scheduler.runAfter(
      0,
      components.lettermint.lib.cleanupAbandonedEmails,
      { olderThan: 4 * ONE_WEEK_MS },
    );
  },
});

export default crons;

React Email

Use React Email to generate HTML in a Convex Node action, then pass the generated HTML to sendEmail.

"use node";
import { action } from "./_generated/server";
import { render, pretty } from "@react-email/render";
import { Button, Html } from "@react-email/components";
import { components } from "./_generated/api";
import { Lettermint } from "convex-lettermint";

export const lettermint = new Lettermint(components.lettermint, {
  testMode: false,
});

export const sendEmail = action({
  args: {},
  handler: async (ctx) => {
    const html = await pretty(
      await render(
        <Html>
          <Button href="https://example.com">Click me</Button>
        </Html>,
      ),
    );

    await lettermint.sendEmail(ctx, {
      from: "Me <test@mydomain.com>",
      to: "recipient@example.com",
      subject: "Hi there",
      html,
    });
  },
});

Sending Manually And Attachments

Attachments are intentionally not part of the durable queue API. Use sendEmailManually when you need Lettermint features that should be sent immediately, including attachments or inline images.

import { components } from "./_generated/api";
import { internalAction } from "./_generated/server";
import { Lettermint } from "convex-lettermint";

export const lettermint = new Lettermint(components.lettermint, {});

export const sendManualEmail = internalAction({
  args: {},
  handler: async (ctx) => {
    const from = "Acme <onboarding@example.com>";
    const to = ["ok@testing.lettermint.co"];
    const subject = "hello world";
    const html = "<p>it works!</p>";

    await lettermint.sendEmailManually(
      ctx,
      { from, to, subject },
      async (emailId) => {
        const response = await fetch("https://api.lettermint.co/v1/send", {
          method: "POST",
          headers: {
            "x-lettermint-token": process.env.LETTERMINT_PROJECT_TOKEN!,
            "Content-Type": "application/json",
            "Idempotency-Key": emailId,
          },
          body: JSON.stringify({
            from,
            to,
            subject,
            html,
            attachments: [
              {
                filename: "invoice.pdf",
                content: "base64-encoded-content",
              },
            ],
          }),
        });
        if (!response.ok) {
          throw new Error(`[Email] Failed to send: ${await response.text()}`);
        }
        const data = (await response.json()) as { message_id: string };
        return data.message_id;
      },
    );
  },
});