npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

convex-sendblue

v0.1.0

Published

Convex component for Sendblue iMessage/SMS API

Readme

Convex Sendblue Component

npm version

Send and receive iMessage, SMS, and RCS messages in your Convex app using Sendblue.

import { Sendblue } from "@convex-dev/sendblue";
import { components } from "./_generated/api";

export const sendblue = new Sendblue(components.sendblue);

export const sendSms = internalAction({
  handler: async (ctx) => {
    return await sendblue.sendMessage(ctx, {
      number: "+14151234567",
      content: "Hello from Sendblue!",
      from_number: process.env.SENDBLUE_PHONE_NUMBER!,
    });
  },
});

Features

  • Send messages via iMessage, SMS, or RCS with media support
  • Receive messages via webhooks with configurable callbacks
  • Track delivery status with automatic status updates
  • Manage contacts with full CRUD, bulk operations, and opt-out
  • Reactions & presence — send tapback reactions, read receipts, and typing indicators
  • Evaluate service — check if a number supports iMessage before sending
  • Reactive queries — all messages and contacts are stored in Convex tables with indexes

Prerequisites

Sendblue Account

Create a Sendblue account and obtain your API credentials:

  • sb-api-key-id — your API key identifier
  • sb-api-secret-key — your API secret key

You can find these in the Sendblue dashboard.

Note the phone number assigned to your account — you'll need it as the from_number when sending messages. You can retrieve your assigned numbers via the GET /api/lines endpoint.

Convex App

You'll need a Convex app to use the component. Follow any of the Convex quickstarts to set one up.

Installation

Install the component package:

npm install @convex-dev/sendblue

Create a convex.config.ts file in your app's convex/ folder and install the component by calling use:

// convex/convex.config.ts
import { defineApp } from "convex/server";
import sendblue from "@convex-dev/sendblue/convex.config.js";

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

export default app;

Set your API credentials:

npx convex env set SB_API_KEY_ID your-api-key-id
npx convex env set SB_API_SECRET_KEY your-api-secret-key

Instantiate the Sendblue client in a file in your app's convex/ folder:

// convex/sendblue.ts
import { Sendblue } from "@convex-dev/sendblue";
import { components } from "./_generated/api";

export const sendblue = new Sendblue(components.sendblue);

You can also pass credentials explicitly instead of using environment variables:

export const sendblue = new Sendblue(components.sendblue, {
  SB_API_KEY_ID: process.env.MY_SENDBLUE_KEY!,
  SB_API_SECRET_KEY: process.env.MY_SENDBLUE_SECRET!,
});

Register webhook handlers by creating an http.ts file in your convex/ folder:

// convex/http.ts
import { sendblue } from "./sendblue";
import { httpRouter } from "convex/server";

const http = httpRouter();
sendblue.registerRoutes(http);
export default http;

Sending Messages

Send a message using the sendMessage method from within a Convex action:

// convex/messages.ts
import { v } from "convex/values";
import { internalAction } from "./_generated/server";
import { sendblue } from "./sendblue";

export const send = internalAction({
  args: { to: v.string(), body: v.string() },
  handler: async (ctx, args) => {
    return await sendblue.sendMessage(ctx, {
      number: args.to,
      content: args.body,
      from_number: process.env.SENDBLUE_PHONE_NUMBER!,
    });
  },
});

The message is sent via the Sendblue API and automatically stored in the component's messages table. You can query it later (see Querying Messages).

Send with Media

Attach an image or file by including media_url:

await sendblue.sendMessage(ctx, {
  number: "+14151234567",
  content: "Check this out!",
  media_url: "https://example.com/photo.jpg",
  from_number: process.env.SENDBLUE_PHONE_NUMBER!,
});

Send with iMessage Effects

Use send_style to send with an expressive iMessage effect:

await sendblue.sendMessage(ctx, {
  number: "+14151234567",
  content: "Happy birthday!",
  send_style: "celebration",
  from_number: process.env.SENDBLUE_PHONE_NUMBER!,
});

Evaluate Service

Check whether a phone number supports iMessage before sending:

const result = await sendblue.evaluateService(ctx, {
  number: "+14151234567",
});
// result.service is "iMessage", "SMS", or "RCS"

Receiving Messages

sendblue.registerRoutes registers two webhook HTTP handlers in your Convex deployment:

  • YOUR_CONVEX_SITE_URL/sendblue/incoming-message — captures incoming messages sent to your Sendblue number
  • YOUR_CONVEX_SITE_URL/sendblue/message-status — captures delivery status updates for messages you send

Custom HTTP Prefix

You can route Sendblue endpoints to a custom path:

export const sendblue = new Sendblue(components.sendblue, {
  httpPrefix: "/custom-sendblue",
});

This routes to YOUR_CONVEX_SITE_URL/custom-sendblue/incoming-message and YOUR_CONVEX_SITE_URL/custom-sendblue/message-status.

Configure Sendblue Webhooks

Set your webhook URL in the Sendblue dashboard or via the API to point at your Convex deployment's incoming-message endpoint.

Incoming Message Callback

Execute your own logic when a message arrives by setting a callback:

// convex/sendblue.ts
import { Sendblue } from "@convex-dev/sendblue";
import { components, internal } from "./_generated/api";

export const sendblue = new Sendblue(components.sendblue);
sendblue.incomingMessageCallback = internal.sendblue.handleIncoming;
// convex/sendblue.ts (continued)
import { v } from "convex/values";
import { internalMutation } from "./_generated/server";

export const handleIncoming = internalMutation({
  args: { message: v.any() },
  handler: async (ctx, args) => {
    // This runs in the same transaction as the message insertion.
    // Use ctx to update the database or schedule other actions.
    console.log("Incoming message from:", args.message.from_number);
    console.log("Content:", args.message.content);
  },
});

If the callback throws an error, the message will not be saved and the webhook will return an error.

Outgoing Message Callback

You can also set a callback for outgoing messages:

sendblue.defaultOutgoingMessageCallback = internal.sendblue.handleOutgoing;

Querying Messages

All messages (sent and received) are stored in the component's database and can be queried reactively.

List All Messages

export const listAll = query({
  handler: async (ctx) => {
    return await sendblue.list(ctx, { limit: 50 });
  },
});

List Incoming / Outgoing

export const incoming = query({
  handler: async (ctx) => {
    return await sendblue.listIncoming(ctx);
  },
});

export const outgoing = query({
  handler: async (ctx) => {
    return await sendblue.listOutgoing(ctx);
  },
});

Get Message by Handle

export const getMessage = query({
  args: { message_handle: v.string() },
  handler: async (ctx, args) => {
    return await sendblue.getMessageByHandle(ctx, args);
  },
});

Get Messages by Phone Number

// Messages sent to a number
const sentTo = await sendblue.getMessagesTo(ctx, { number: "+14151234567" });

// Messages received from a number
const receivedFrom = await sendblue.getMessagesFrom(ctx, {
  number: "+14151234567",
});

// All messages to/from a number (conversation view)
const conversation = await sendblue.getMessagesByCounterparty(ctx, {
  number: "+14151234567",
});

Delete a Message

Deletes from both Sendblue and the local database:

await sendblue.deleteMessage(ctx, {
  message_handle: "abc-123-def",
});

Reactions & Presence

Send a Tapback Reaction

await sendblue.sendReaction(ctx, {
  message_id: "abc-123-def",
  reaction_type: "love", // love, like, dislike, laugh, emphasize, question
});

Send Read Receipt

await sendblue.markRead(ctx, { number: "+14151234567" });

Send Typing Indicator

await sendblue.sendTypingIndicator(ctx, { number: "+14151234567" });

Contacts

The component provides full contact management with local caching.

Create a Contact

await sendblue.createContact(ctx, {
  number: "+14151234567",
  first_name: "Jane",
  last_name: "Doe",
  tags: ["vip", "beta"],
});

Query Contacts

// List all contacts from local DB
const contacts = await sendblue.listContacts(ctx, { limit: 50 });

// Get a specific contact
const contact = await sendblue.getContact(ctx, { number: "+14151234567" });

Update a Contact

await sendblue.updateContact(ctx, {
  number: "+14151234567",
  first_name: "Janet",
  tags: ["vip", "premium"], // tags are replaced entirely
});

Delete a Contact

await sendblue.deleteContact(ctx, { number: "+14151234567" });

Bulk Operations

// Bulk create
await sendblue.bulkCreateContacts(ctx, {
  contacts: [
    { phone: "+14151234567", firstName: "Jane" },
    { phone: "+14159876543", firstName: "John" },
  ],
});

// Bulk delete
await sendblue.bulkDeleteContacts(ctx, {
  contact_ids: ["id1", "id2"],
});

Opt Out

Block outbound messages to a number:

await sendblue.optOutContact(ctx, { number: "+14151234567" });

// Opt back in
await sendblue.optOutContact(ctx, { number: "+14151234567", opted_out: false });

Media

Upload Media from URL

const result = await sendblue.uploadMediaObject(ctx, {
  url: "https://example.com/photo.jpg",
});

Direct API Access

For advanced use cases, you can query the Sendblue API directly through the component without local DB caching:

// List messages from Sendblue API
const apiMessages = await sendblue.listMessagesFromApi(ctx, {
  limit: 20,
  number: "+14151234567",
});

// Get a specific message from the API
const apiMessage = await sendblue.getMessageFromApi(ctx, {
  message_id: "abc-123",
});

// List contacts from Sendblue API
const apiContacts = await sendblue.listContactsFromApi(ctx, {
  limit: 50,
  order_by: "created_at",
  order_direction: "desc",
});

// Get contact count
const count = await sendblue.countContactsFromApi(ctx);

API Reference

Sendblue Constructor

new Sendblue(component, options?)

| Option | Type | Default | Description | | ------------------------------- | ---------------- | ----------------------------- | ----------------------------------------------------- | | SB_API_KEY_ID | string | process.env.SB_API_KEY_ID | Sendblue API key ID | | SB_API_SECRET_KEY | string | process.env.SB_API_SECRET_KEY | Sendblue API secret key | | httpPrefix | string | "/sendblue" | URL prefix for webhook routes | | incomingMessageCallback | FunctionHandle | — | Mutation to call on incoming messages | | defaultOutgoingMessageCallback| FunctionHandle | — | Mutation to call on outgoing messages |

Message Statuses

Messages go through the following statuses:

| Status | Description | | ------------ | ------------------------------------- | | QUEUED | Message accepted and queued | | PENDING | Message is being processed | | SENT | Message sent to carrier | | DELIVERED | Message delivered to recipient | | ERROR | Message failed to send | | DECLINED | Message was declined | | RECEIVED | Incoming message received | | ACCEPTED | Message accepted by carrier | | REGISTERED | Recipient registered for RCS |

Service Types

| Service | Description | | ---------- | ---------------------------- | | iMessage | Apple iMessage | | SMS | Standard text messaging | | RCS | Rich Communication Services |