Skip to content

Find a component

Loading the directory…

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

Usesend

The Bijection useSend Component - send emails durably with batching, rate limiting, and webhook support.

Bijection source
git clone https://github.com/bijectionhq/usesend.git
cd usesend
git checkout fe4f6c88d49622ef258bd33298b0bb36645e25f7

Bijection repository access is required. Request access.

Description

The Bijection useSend Component - send emails durably with batching, rate limiting, and webhook support.

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: bijectionhq/usesend. Based on pulgueta/usesend-convex. License: Apache-2.0. Consult the repository for license terms, notices, and contributor attribution.

From the upstream publisher

The Convex useSend Component - send emails durably with batching, rate limiting, and webhook support.

Explore the demo ↗

Upstream overview

From the upstream package documentation.

useSend Convex Component integrates the useSend transactional email service (an open-source Resend alternative) into Convex applications with durable, exactly-once email delivery. It uses Convex workpools for retry logic, automatic batching to the useSend batch endpoint, and idempotency key management to prevent duplicate sends. Webhook support enables real-time delivery status callbacks, and a typed REST client exposes the full useSend API for contacts, domains, campaigns, and analytics.

Use cases

From the upstream package documentation.

  • Sending transactional emails (signup confirmation, password reset) with guaranteed delivery even if the useSend API is temporarily unavailable
  • Queuing large volumes of marketing or notification emails without hitting rate limits, relying on automatic batching and backoff
  • Tracking per-email delivery status (delivered, bounced, spam complaint) by wiring up a webhook handler and an onEmailEvent callback mutation
  • Using React Email components to author templates server-side, rendering to HTML and plain-text fallback before enqueueing through the durable send pipeline
  • Managing contacts, contact books, campaigns, and domain verification through a typed REST client from within Convex actions

How it works

From the upstream package documentation.

After installing the package and adding app.use(usesend) in convex.config.ts, you instantiate UseSend with components.usesend and optionally pass a UseSendOptions object for API key, base URL, retry settings, and an onEmailEvent callback. Calling usesend.sendEmail(ctx, { from, to, subject, html }) from any mutation enqueues the email into the component's internal queue, which batches and forwards sends to the useSend /emails/batch endpoint via Convex workpools, handling retries with configurable backoff.

The sendEmail method returns an EmailId you can use to check status via usesend.status(ctx, emailId) or cancel a waiting email via usesend.cancelEmail(ctx, emailId). Status fields include delivered, bounced, failed, complained, opened, and clicked.

Webhook integration is set up by mounting an HTTP route in convex/http.ts that delegates to usesend.handleUseSendEventWebhook(ctx, req). Setting the USESEND_WEBHOOK_SECRET environment variable enables signature verification. When an event arrives, the component calls the registered onEmailEvent internal mutation with the email ID and event type.

The full useSend REST API is available through usesend.api, covering emails, contacts, contact books, domains, campaigns, and analytics. These methods make HTTP calls and must run inside Convex actions. Data retention is not automatic; you are expected to schedule cleanupOldEmails and cleanupAbandonedEmails via a cron job to remove finalized email records.

Upstream package FAQ

How does @pulgueta/usesend-convex ensure emails are not sent more than once during retries?

The useSend Convex component manages useSend idempotency keys internally for every email it sends. Combined with Convex workpools for durable execution, this guarantees exactly-once delivery even when mutations are retried due to transient failures or network outages.

Can I use @pulgueta/usesend-convex with a self-hosted useSend instance?

Yes. Pass the baseUrl option to the UseSend constructor pointing at your self-hosted instance: new UseSend(components.usesend, { baseUrl: 'https://your-usesend-instance.com' }). The component defaults to https://app.usesend.com but will use any compatible endpoint.

What Convex function types can call usesend.sendEmail?

usesend.sendEmail works inside Convex mutations and actions. The full REST API client at usesend.api, which covers contacts, domains, campaigns, and analytics, performs HTTP calls and must run inside a Convex action rather than a mutation.

How do I clean up stored email records from the useSend component?

The useSend Convex component retains finalized emails (delivered, cancelled, bounced) indefinitely by default. You are responsible for cleanup. Schedule cleanupOldEmails and cleanupAbandonedEmails via a Convex cron job using ctx.scheduler.runAfter to remove records older than a threshold you define, such as one week.

Can I schedule an email to be sent at a future time using this component?

Yes. Pass a scheduledAt field with an ISO 8601 timestamp to usesend.sendEmail to have useSend deliver the email at a later time. Scheduled emails can be rescheduled or cancelled through the REST API client at usesend.api.emails.updateSchedule and usesend.api.emails.cancel using the email's usesendId.

Use with agents and CLI

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

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

Read: https://bijection.com/components/usesend/SKILL.md

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

Upstream README.md

View repository ↗

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

useSend Convex Component

This component is the official way to integrate the useSend email service with your Convex project. useSend is an open-source alternative to Resend, Sendgrid, Mailgun, and Postmark.

Features:

  • Queueing: Send as many emails as you want, as fast as you want - they'll all be delivered (eventually).
  • Batching: Automatically batches large groups of emails and sends them to useSend's /emails/batch endpoint efficiently.
  • Durable execution: Uses Convex workpools to ensure emails are eventually delivered, even in the face of temporary failures or network outages.
  • Idempotency: Manages useSend idempotency keys to guarantee emails are delivered exactly once, preventing accidental spamming from retries.
  • Rate limiting: Honors API rate limits established by useSend.
  • Webhook support: Receive real-time email delivery status updates.
  • Full API coverage: Contacts, contact books, domains, campaigns, and analytics are available through a typed REST client (usesend.api).
  • Self-hosted support: Works with both useSend's hosted service and self-hosted instances.

See example/convex/example.ts for a demo of how to incorporate this component into your application. Its public demo functions require Convex authentication and only allow the identity whose token identifier matches USESEND_EXAMPLE_ADMIN_TOKEN_IDENTIFIER.

Installation

npm install @pulgueta/usesend-convex

Get Started

Create a useSend account and grab an API key. Set it to USESEND_API_KEY in your deployment environment.

Next, add the component to your Convex app via convex/convex.config.ts:

import { defineApp } from "convex/server";
import usesend from "@pulgueta/usesend-convex/convex.config.js";

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

export default app;

Then you can use it in your Convex functions:

// convex/emails.ts
import { components } from "./_generated/api";
import { UseSend } from "@pulgueta/usesend-convex";
import { internalMutation } from "./_generated/server";

export const usesend = new UseSend(components.usesend);

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

Then, calling sendTestEmail from anywhere in your app will send this test email.

Advanced Usage

Setting up a useSend webhook

While the setup we have so far will reliably send emails, you don't have any feedback on anything delivering, bouncing, or triggering spam complaints. For that, we need to set up a webhook!

On the Convex side, we need to mount an HTTP endpoint to our project to route it to the useSend component in convex/http.ts:

import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { usesend } from "./emails";

const http = httpRouter();

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

export default http;

If your Convex project is happy-leopard-123, you now have a useSend webhook for your project running at https://happy-leopard-123.convex.site/usesend/webhook.

Navigate to the useSend dashboard and create a new webhook at that URL. Make sure to enable all the email.* events.

Finally, copy the webhook secret out of the useSend dashboard and set it to the USESEND_WEBHOOK_SECRET environment variable in your Convex deployment.

Registering an email status event handler

If you have your webhook established, you can also register an event handler to get notifications when email statuses change.

import { components, internal } from "./_generated/api";
import { internalMutation } from "./_generated/server";
import { vOnEmailEventArgs, UseSend } from "@pulgueta/usesend-convex";

export const usesend = new UseSend(components.usesend, {
  onEmailEvent: internal.emails.handleEmailEvent,
});

export const handleEmailEvent = internalMutation({
  args: vOnEmailEventArgs,
  handler: async (ctx, args) => {
    console.log(`Email ${args.id} received event:`, args.event.type);

    switch (args.event.type) {
      case "email.delivered":
        console.log("Email delivered!");
        break;
      case "email.bounced":
        console.log("Email bounced");
        break;
      case "email.complained":
        console.log("Email marked as spam");
        break;
    }
  },
});

UseSend component options

There is a UseSendOptions argument to the component constructor to help customize its behavior:

  • apiKey: Provide the useSend API key instead of having it read from the environment variable.
  • baseUrl: The base URL for the useSend API (defaults to https://app.usesend.com). Set this if you're using a self-hosted useSend instance.
  • webhookSecret: Same thing, but for the webhook secret.
  • initialBackoffMs: Initial backoff for retries (default: 30 seconds).
  • retryAttempts: Number of retry attempts (default: 5).
  • requestTimeoutMs: Maximum time to wait for a useSend API response (default: 30 seconds).
  • onEmailEvent: Your email event callback.

Using useSend Templates

You can use useSend templates to send emails with pre-designed templates from your useSend dashboard:

await usesend.sendEmail(ctx, {
  from: "Me <test@mydomain.com>",
  to: "recipient@example.com",
  template: {
    id: "my-template-id",
    variables: {
      name: "John Doe",
      verificationLink: "https://example.com/verify?token=abc123",
    },
  },
});

Note: You cannot use both template and html/text in the same email.

Scheduling and threading emails

Pass scheduledAt (ISO 8601) to have useSend deliver the email at a later time, or inReplyToId to thread it under a previously sent email:

await usesend.sendEmail(ctx, {
  from: "Me <test@mydomain.com>",
  to: "recipient@example.com",
  subject: "See you tomorrow",
  html: "<p>Reminder!</p>",
  scheduledAt: "2026-08-01T09:00:00Z",
});

Emails already handed off to useSend with a future scheduledAt can be rescheduled or cancelled through the REST API (see below) using the email's usesendId:

await usesend.api.emails.updateSchedule(usesendId, "2026-08-02T09:00:00Z");
await usesend.api.emails.cancel(usesendId);

Tracking, getting status, and cancelling emails

The sendEmail method returns a branded type, EmailId. You can use this for:

  • Reassociating the original email during status changes in your email event handler.
  • Checking on the status any time using usesend.status(ctx, emailId).
  • Cancelling a waiting email using usesend.cancelEmail(ctx, emailId). Once batching starts, useSend may already be processing it and local cancellation is no longer safe.
// Check email status
const emailStatus = await usesend.status(ctx, emailId);
if (emailStatus) {
  console.log(emailStatus.status); // e.g., "delivered", "bounced", "sent"
  console.log(emailStatus.bounced); // boolean
  console.log(emailStatus.failed); // boolean
  console.log(emailStatus.complained); // spam complaint (boolean)
  console.log(emailStatus.deliveryDelayed); // boolean
  console.log(emailStatus.opened); // if open tracking enabled (boolean)
  console.log(emailStatus.clicked); // if click tracking enabled (boolean)
  console.log(emailStatus.errorMessage); // error details (string | null)
}

Contacts, domains, campaigns, and analytics

Everything the useSend REST API offers beyond durable email sending is available through usesend.api, a typed client covering contacts, contact books, domains, campaigns, analytics, and direct email operations. These methods perform HTTP calls, so they must run inside an action:

import { action } from "./_generated/server";
import { v } from "convex/values";
import { usesend } from "./emails";

export const subscribe = action({
  args: { contactBookId: v.string(), email: v.string() },
  returns: v.string(),
  handler: async (_ctx, args) => {
    const { contactId } = await usesend.api.contacts.create(
      args.contactBookId,
      { email: args.email, subscribed: true },
    );
    return contactId;
  },
});

The full surface:

  • usesend.api.emails — send, batch, get, list, updateSchedule, cancel. Direct sends support attachments and scheduling, and accept an idempotency key: usesend.api.emails.send(email, { idempotencyKey }).
  • usesend.api.contacts — create, get, list, update, upsert, delete, bulkCreate, bulkDelete (all scoped to a contact book).
  • usesend.api.contactBooks — create, get, list, update, delete.
  • usesend.api.domains — create, get, list, verify, delete.
  • usesend.api.campaigns — create, get, list, delete, schedule, pause, resume.
  • usesend.api.analytics — emailTimeSeries, reputationMetrics.

Failed requests throw a UseSendApiError carrying the HTTP status and response body. Note that emails sent through usesend.api.emails.send are not tracked by the component — use sendEmail (durable batching) or sendEmailManually (tracked manual send, below) for that.

Self-hosted useSend

If you're running a self-hosted useSend instance, configure the baseUrl:

export const usesend = new UseSend(components.usesend, {
  baseUrl: "https://your-usesend-instance.com",
});

Data retention

This component retains "finalized" (delivered, cancelled, bounced) emails. It's your responsibility to clear out those emails on your own schedule. You can run cleanupOldEmails and cleanupAbandonedEmails from the dashboard or set up a cron job:

// in convex/crons.ts
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 usesend component",
  { hours: 1 },
  internal.crons.cleanupUseSend,
);

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

export default crons;

Using React Email

The component ships with a React Email integration at @pulgueta/usesend-convex/react-email. Author your emails as React components; sendReactEmail renders them to email-client-safe HTML plus a plain-text fallback (better accessibility and deliverability) and enqueues them through the durable send pipeline.

Install React Email in your app to author templates:

npm install react-email react-dom -E

Define a template (see react.email/docs for the client-compatibility rules — no flexbox/grid, pixel-based sizing, etc.):

// convex/emails/welcome.tsx
import { Body, Button, Container, Head, Html, Preview, Text } from "react-email";

export default function WelcomeEmail({ name }: { name: string }) {
  return (
    <Html lang="en">
      <Head />
      <Body style={{ fontFamily: "sans-serif" }}>
        <Preview>Welcome aboard!</Preview>
        <Container>
          <Text>{`Welcome, ${name}!`}</Text>
          <Button
            href="https://example.com"
            style={{ background: "#000", color: "#fff", padding: "12px 20px" }}
          >
            Get started
          </Button>
        </Container>
      </Body>
    </Html>
  );
}

Then render and send it from an action (use a Node action for maximum compatibility with react-dom/server):

// convex/reactEmail.tsx
"use node";
import { internalAction } from "./_generated/server";
import { sendReactEmail } from "@pulgueta/usesend-convex/react-email";
import { v } from "convex/values";
import { usesend } from "./emails";
import WelcomeEmail from "./emails/welcome";

export const sendWelcomeEmail = internalAction({
  args: { to: v.string(), name: v.string() },
  returns: v.string(),
  handler: async (ctx, args) => {
    return await sendReactEmail(usesend, ctx, {
      from: "Onboarding <onboarding@mydomain.com>",
      to: args.to,
      subject: `Welcome, ${args.name}!`,
      react: <WelcomeEmail name={args.name} />,
    });
  },
});

If you only want the rendered output (e.g. to pass to usesend.api.emails.send with attachments), use renderEmail:

import { renderEmail } from "@pulgueta/usesend-convex/react-email";

const { html, text } = await renderEmail(<WelcomeEmail name="Ada" />);

See example/convex/emails/welcome.tsx for a fuller template using Tailwind with pixelBasedPreset.

Sending emails manually

If you want to bypass the component's batching (e.g. to attach files) while still tracking the email's delivery status through webhooks, use sendEmailManually. It records the email in the component, you perform the actual send in the callback (here via usesend.api), and the returned useSend ID links webhook events back to the record:

export const sendManualEmail = internalAction({
  args: {},
  returns: v.string(),
  handler: async (ctx) => {
    const from = "Acme <onboarding@example.com>";
    const to = ["recipient@example.com"];
    const subject = "hello world";

    const emailId = await usesend.sendEmailManually(
      ctx,
      { from, to, subject },
      async () => {
        const result = await usesend.api.emails.send({
          from,
          to,
          subject,
          html: "<p>it works!</p>",
          attachments: [{ filename: "invoice.pdf", content: base64Pdf }],
        });
        return result.emailId;
      },
    );
    return emailId;
  },
});

Development

To develop this component:

pnpm install
pnpm dev

This will start a file watcher to rebuild the component, as well as the example project frontend and backend.

License

Apache-2.0

Keywords