# Usesend The Bijection useSend Component - send emails durably with batching, rate limiting, and webhook support. - Package: @bijectionhq/usesend - Category: Messaging - Status: Preview - License: Apache-2.0 - Bijection source (repository access required): https://github.com/bijectionhq/usesend/tree/fe4f6c88d49622ef258bd33298b0bb36645e25f7 - Upstream: https://github.com/pulgueta/usesend-convex/tree/07d1c9a81ab64c3e828f58ac875b700ddba71a01 - Documentation: https://docs.bijection.com/components/using - Directory: https://bijection.com/components/usesend ## Get the source Bijection repository access is required. Request access at https://bijection.com/contact. ```sh git clone https://github.com/bijectionhq/usesend.git cd usesend git checkout fe4f6c88d49622ef258bd33298b0bb36645e25f7 ``` ## Before using this component Read the package README at the recorded revision for setup, dependencies, and API examples. Upstream documentation describes the original package and may differ from the Bijection adaptation. This is a source candidate for evaluation. Catalog inclusion does not establish npm availability or runtime qualification. Check compatibility with your SDK and deployment before adoption. External providers run their own compute. ## Upstream package documentation Recorded 2026-10-23. This describes @pulgueta/usesend-convex; check the Bijection adaptation separately. 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 - 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 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 README # useSend Convex Component This component is the official way to integrate the [useSend](https://usesend.com) 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](./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 ```bash npm install @pulgueta/usesend-convex ``` ## Get Started Create a [useSend](https://usesend.com) 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`: ```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: ```ts // 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 ", 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`: ```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. ```ts 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](https://docs.usesend.com) to send emails with pre-designed templates from your useSend dashboard: ```ts await usesend.sendEmail(ctx, { from: "Me ", 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: ```ts await usesend.sendEmail(ctx, { from: "Me ", to: "recipient@example.com", subject: "See you tomorrow", html: "

Reminder!

", 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`: ```ts 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. ```ts // 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**: ```ts 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`: ```ts 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: ```ts // 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](https://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: ```sh npm install react-email react-dom -E ``` Define a template (see [react.email/docs](https://react.email/docs) for the client-compatibility rules — no flexbox/grid, pixel-based sizing, etc.): ```tsx // convex/emails/welcome.tsx import { Body, Button, Container, Head, Html, Preview, Text } from "react-email"; export default function WelcomeEmail({ name }: { name: string }) { return ( Welcome aboard! {`Welcome, ${name}!`} ); } ``` Then render and send it from an action (use a Node action for maximum compatibility with `react-dom/server`): ```tsx // 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 ", to: args.to, subject: `Welcome, ${args.name}!`, react: , }); }, }); ``` If you only want the rendered output (e.g. to pass to `usesend.api.emails.send` with attachments), use `renderEmail`: ```tsx import { renderEmail } from "@pulgueta/usesend-convex/react-email"; const { html, text } = await renderEmail(); ``` See [example/convex/emails/welcome.tsx](./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: ```ts export const sendManualEmail = internalAction({ args: {}, returns: v.string(), handler: async (ctx) => { const from = "Acme "; 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: "

it works!

", attachments: [{ filename: "invoice.pdf", content: base64Pdf }], }); return result.emailId; }, ); return emailId; }, }); ``` ## Development To develop this component: ```sh 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