convex-sendblue
v0.1.0
Published
Convex component for Sendblue iMessage/SMS API
Maintainers
Readme
Convex Sendblue Component
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 identifiersb-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/sendblueCreate 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-keyInstantiate 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 numberYOUR_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 |
