Official npm package: reloop-email.
You need two things:
- API key — create one in your Reloop account
- Verified domain — add and verify a sending domain; use it in the
fromaddress
For setup details and the full API reference, see reloop.sh/docs.
npm install reloop-emailConstruction takes a required apiKey and an optional baseUrl (defaults to https://reloop.sh).
import { Reloop } from "reloop-email";
const reloop = new Reloop({ apiKey: "rl_your_api_key_here" });
// Optional custom host:
// const reloop = new Reloop({ apiKey: "rl_...", baseUrl: "https://reloop.sh" });HTTP helpers use named result fields — they do not throw for API or network failures. Mail send returns { response, emailError }; API keys use { apiKey, apiKeyError }, and so on.
Invalid arguments (e.g. empty key id or name) throw ReloopValidationError before any network request.
const { response, emailError } = await reloop.mail.send({
from: "Reloop <hello@your-verified-domain.com>",
to: "user@example.com",
subject: "Welcome to Reloop",
html: "<p>Thanks for signing up.</p>",
text: "Thanks for signing up.",
});
if (emailError) throw emailError;
console.log(response.messageId, response.id);Pass a React element as react and it is rendered to HTML on your machine before the request is made (the react field itself is never sent). Any plain JSX/TSX component works; you do not need @react-email/components.
Rendering needs react plus one renderer, tried in this order:
@react-email/renderif installed (email-tuned output)react-dom/serveras a fallback (renderToStaticMarkupwith a doctype prepended)
npm install react react-dom
# optional, for email-specific rendering:
npm install @react-email/renderimport { Reloop } from "reloop-email";
import { WelcomeEmail } from "./emails/welcome";
const reloop = new Reloop({ apiKey: "rl_your_api_key_here" });
const { response, emailError } = await reloop.mail.send({
from: "Reloop <hello@your-verified-domain.com>",
to: "user@example.com",
subject: "Welcome to Reloop",
react: WelcomeEmail({ name: "Ada" }),
text: "Welcome to Reloop",
});When both react and html are given, react wins. If neither renderer is installed, send throws ReloopValidationError before any network call.
Manage keys with reloop.apiKey (one method per API route):
| Method | Purpose |
|---|---|
create({ name }) |
Create a key (secret returned once) |
list(params?) |
List / search keys |
get(id) |
Get one key |
update(id, { name }) |
Rename |
delete(id) |
Delete |
rotate(id) |
Rotate secret (new secret once) |
enable(id) / disable(id) |
Toggle enabled state |
const { apiKey, apiKeyError } = await reloop.apiKey.create({
name: "Production Key",
});
if (apiKeyError) throw apiKeyError;
console.log(apiKey.id, apiKey.key);Manage contacts with reloop.contacts:
| Method | Purpose |
|---|---|
create(params) |
Create a contact |
list(params?) |
List / search contacts |
get(id) |
Retrieve one contact |
update(id, params) |
Update a contact |
delete(id) |
Delete a contact |
const { contact, contactError } = await reloop.contacts.create({
email: "john.doe@example.com",
firstName: "John",
lastName: "Doe",
status: "subscribed",
});
if (contactError) throw contactError;
console.log(contact.id, contact.email);Manage custom properties with reloop.contacts.properties:
| Method | Purpose |
|---|---|
create({ name, type, fallbackValue? }) |
Create a property |
list(params?) |
List / filter properties |
update(id, { fallbackValue }) |
Update fallback value |
delete(id) |
Delete a property |
const { property, propertyError } = await reloop.contacts.properties.create({
name: "company_name",
type: "string",
fallbackValue: "Unknown",
});
if (propertyError) throw propertyError;
console.log(property.id, property.propertyName);Manage groups with reloop.contacts.groups:
| Method | Purpose |
|---|---|
create({ name }) |
Create a group |
list(params?) |
List / search groups |
get(id) |
Get one group |
update(id, { name }) |
Rename |
delete(id) |
Delete a group |
listContacts(id, params?) |
List contacts in a group |
addContact(id, { contact_id? | email? }) |
Add a contact |
removeContact(id, { contact_id? | email? }) |
Remove a contact |
const { group, groupError } = await reloop.contacts.groups.create({
name: "Beta Testers",
});
if (groupError) throw groupError;
console.log(group.id, group.name);Manage channels with reloop.contacts.channels:
| Method | Purpose |
|---|---|
create({ name, description?, defaultSubscription?, visibility? }) |
Create a channel |
list(params?) |
List channels |
get(id) |
Get one channel |
update(id, params) |
Update name / description / visibility |
delete(id) |
Delete a channel |
addContact(id, { contact_id? | email?, subscription? }) |
Enroll a contact |
updateSubscription(id, { contact_id? | email?, subscription }) |
Update enrollment |
const { channel, channelError } = await reloop.contacts.channels.create({
name: "Product Updates",
description: "Get the latest news about our products",
defaultSubscription: "opt_in",
visibility: "public",
});
if (channelError) throw channelError;
console.log(channel.id, channel.name);Manage sending domains with reloop.domain:
| Method | Purpose |
|---|---|
create({ domain, ... }) |
Add a domain |
list(params?) |
List / search domains |
get(id) |
Get one domain |
update(id, params) |
Update tracking / TLS / send settings |
delete(id) |
Delete a domain |
verify(id) |
Start DNS verification |
const { domain, domainError } = await reloop.domain.create({
domain: "send.example.com",
click_tracking: true,
open_tracking: true,
tls: "opportunistic",
sending_email: true,
receiving_email: false,
});
if (domainError) throw domainError;
console.log(domain.id, domain.domain);More examples: reloop.sh/docs
Manage outbound webhooks with reloop.webhook and verify incoming events locally (no HTTP call):
| Method | Purpose |
|---|---|
create({ description, url, events }) |
Register a webhook |
list(params?) |
List webhooks |
get(id) |
Get one webhook |
update(id, params) |
Update settings / status |
delete(id) |
Delete a webhook |
pause(id) / enable(id) / disable(id) |
Status shortcuts |
trigger({ event, payload }) |
Fire an event manually |
listDeliveries(id, params?) |
List delivery attempts |
retryDelivery(deliveryId) |
Retry a failed delivery |
import { Reloop, WebhookService } from "reloop-email";
const reloop = new Reloop({ apiKey: "rl_your_api_key_here" });
const { webhook, webhookError } = await reloop.webhook.create({
description: "Production webhook",
url: "https://example.com/webhooks/reloop",
events: ["domain.created"],
});
if (webhookError) throw webhookError;
// In your HTTP handler (Express, etc.):
const event = WebhookService.constructEvent(
rawBody,
req.headers["x-webhook-signature"],
webhookSecret,
);Manage agent inboxes with reloop.inbox.mailboxes, reloop.inbox.messages, and reloop.inbox.threads:
| Method | Purpose |
|---|---|
list() |
List mailboxes |
get(id) |
Get mailbox details |
create(params) |
Create a mailbox |
update(id, params) |
Update mailbox settings |
delete(id) |
Delete a mailbox |
| Method | Purpose |
|---|---|
list(params?) |
List inbox messages |
listSent(params?) |
List sent messages |
get(id) |
Get one message |
send(params) |
Send a new message |
reply(id, params) / replyAll(id, params) / forward(id, params) |
Compose actions |
setRead(id, { isRead }) / setStar(id, { isStarred }) |
Status shortcuts |
delete(id) |
Delete a message |
| Method | Purpose |
|---|---|
list(params?) |
List conversation threads |
get(id) |
Get thread with messages |
batch({ ids, action }) |
Bulk archive/trash/star/etc. |
archive(id) / trash(id) / restore(id) |
Folder actions |
setRead(id, { isRead }) / setStar(id, { isStarred }) |
Status shortcuts |
const { mailboxes, mailboxError } = await reloop.inbox.mailboxes.list();
if (mailboxError) throw mailboxError;
const { message, messageError } = await reloop.inbox.messages.send({
mailboxId: mailboxes[0].id,
to: "user@example.com",
subject: "Hello from Reloop",
html: "<p>Hi there</p>",
});
if (messageError) throw messageError;
const { threads, threadError } = await reloop.inbox.threads.list({ limit: 50 });
if (threadError) throw threadError;More examples: reloop.sh/docs
An automation runs a series of steps (send an email, wait, branch on a condition) for a contact when your app starts it.
Start every active automation listening for event. email creates the contact if it doesn't exist yet.
const { automation, automationError } = await reloop.automations.start({
event: "user.signed_up",
email: "john.doe@example.com",
properties: { plan: "pro" },
});
if (automationError) throw automationError;
console.log(`Started ${automation.enrollments} automations`);A trigger is the event start sends, plus the properties it carries. Manage them with reloop.automations.triggers:
const { trigger, triggerError } = await reloop.automations.triggers.update(id, {
description: "Sent right after sign-up",
});| Method | Purpose |
|---|---|
create(params) |
Define a trigger and its properties |
list(params?) |
List triggers |
get(idOrKey) |
Get a trigger by id or key |
update(id, params) |
Rename, or replace its properties |
delete(id) |
Delete a trigger |
Trigger keys always start with trigger.; you can leave the prefix off (user.signed_up and trigger.user.signed_up are the same trigger).
const { automation, automationError } = await reloop.automations.create({
name: "Welcome",
status: "active",
steps: [
{ key: "start", type: "trigger", config: { eventName: "user.signed_up" } },
{ key: "is_pro", type: "condition", config: { type: "rule", field: "trigger.plan", operator: "eq", value: "pro" } },
{ key: "welcome_pro", type: "send_email", config: { template: { id: "tmpl_pro" } } },
{ key: "wait", type: "delay", config: { amount: 1, unit: "days" } },
{ key: "welcome_free", type: "send_email", config: { template: { id: "tmpl_free" } } },
],
connections: [
{ from: "start", to: "is_pro" },
{ from: "is_pro", to: "welcome_pro", type: "condition_met" },
{ from: "is_pro", to: "wait", type: "condition_not_met" },
{ from: "wait", to: "welcome_free" },
],
});
if (automationError) throw automationError;| Method | Purpose |
|---|---|
start(params) |
Start every active automation for event |
create(params) |
Create an automation from steps and connections |
list(params?) |
List automations |
get(id) |
Get one automation |
update(id, params) |
Rename, or replace steps and connections together |
activate(id) / pause(id) |
Start or stop it |
enroll(id, params) |
Start this one automation for a contact, without a trigger |
delete(id) |
Delete an automation |
enrollments.list(id, params?) |
Contacts that went through it; filter by status |
enrollments.get(id, enrollmentId) |
One enrollment with each step's result |
triggers.* |
See Triggers |
When creating or updating fails, automationError.body.why lists every problem with the steps at once.
Licensed under the Apache License 2.0 with additional use restrictions from Reloop Labs (same as the Reloop project).