Usesend
The Bijection useSend Component - send emails durably with batching, rate limiting, and webhook support.
git clone https://github.com/bijectionhq/usesend.git
cd usesend
git checkout fe4f6c88d49622ef258bd33298b0bb36645e25f7Bijection 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.
Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.
Getting started
- Get the source at the revision above and read its package documentation.
- Check the required Bijection SDK version, dependencies, and external service credentials.
- Follow the package setup instructions and the guide to using components.
- 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
onEmailEventcallback 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.
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/batchendpoint 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 tohttps://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
templateandhtml/textin 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
waitingemail usingusesend.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