Official npm package: reloop-email. This SDK is the reference implementation for the Reloop API — implement new endpoints here first, then port to other languages.
License: Apache License 2.0 with additional use restrictions from Reloop Labs.
API reference: reloop.sh/docs
git clone git@github.com:reloop-labs/reloop-node.git
cd reloop-node
bun install
bun test
bun run buildRequires Bun (tests) and Node.js 20+ for the published package target (CI may use Node 22 for build tooling).
src/
index.ts # Public entry: Reloop + selected types
client.ts # HTTP transport (internal)
core/ # Result types, client options
services/
api-key/
create/
create.ts # validate + HTTP
create.test.ts # wire + validation
list/
list.ts
list.test.ts
...
api-key.ts # thin facade
api-key.test.ts
test-helpers.ts
mail/
send/
send.ts
send.test.ts
render/
render.ts # react -> html (optional peer dep)
render.test.ts
mail.ts
mail.test.ts
paths.ts
result.ts
errors.ts
fields.ts
test-helpers.ts
types.ts
contacts/
create/ … delete/
contacts.ts
contacts.test.ts
paths.ts
result.ts
errors.ts
fields.ts
test-helpers.ts
types.ts
property/
group/
channel/
domain/
create/ … verify/
domain.ts
domain.test.ts
paths.ts
result.ts
errors.ts
fields.ts
test-helpers.ts
types.ts
webhook/
create/ … retry-delivery/
verify/
webhook.ts
webhook.test.ts
paths.ts
result.ts
errors.ts
fields.ts
test-helpers.ts
types.ts
inbox/
inbox.ts
mailbox/ message/ thread/
tests/ # cross-cutting package tests
init.test.mjs
exports.test.mjs
dist/
| Topic | Rule |
|---|---|
| Init | new Reloop({ apiKey, baseUrl? }) — apiKey required; no key / url aliases |
| HTTP errors (transport) | Internal ReloopResult is { response, error } |
| Mail results | Named fields: { response, emailError } |
| Contact results | Named fields: { contact, contactError } (list: { contacts, contactError }) |
| Webhook results | Named fields: { webhook, webhookError } (list: { webhooks, webhookError }, deliveries: { deliveries, webhookError }) |
| Inbox results | Mailboxes: { mailbox, mailboxError } / { mailboxes, mailboxError }. Messages: { message, messageError } / { messages, messageError }. Threads: { thread, threadError } / { threads, threadError } |
| API key results | Named fields: { apiKey, apiKeyError } (list: { apiKeys, apiKeyError }) |
| Input validation | Throw ReloopValidationError before fetch (no network). Rules match backend (name 1–255, page ≥ 1, limit 1–100, non-empty ids) |
| Api-key layout | One folder per op: create/create.ts + create/create.test.ts. Validation in the op file; tests cover wire + validation. Shared fields.ts / paths.ts; thin api-key.ts facade |
| Mail & domain requests | snake_case JSON (reply_to, click_tracking) |
Mail react |
Rendered locally in mail/render/render.ts: optional peer @react-email/render first, else react-dom/server fallback; react is stripped from the wire body and replaces html |
| Contacts, API keys & inbox | camelCase in JSON bodies as the API expects |
| API key methods | 1:1 with backend: create, list, get, update, delete, rotate, enable, disable (no pause) |
| Types | Request/response interfaces in each service’s types.ts |
| Public exports | Reloop (+ default), options, Result/error types, api-key types, other resource modules as today. Do not export constructable ReloopClient or ApiKeyService |
| Tests | Prefer foo.ts + foo.test.ts next to source. Mock fetch; assert path, method, headers, body, success + error Result |
| Imports | Absolute only: @/... (never ../). Maps to src/* via tsconfig + Bun + tsup |
| Test runner | Bun — bun test (see package.json scripts) |
| README | Prerequisites, strict init, send example, api-key table, link to docs |
// src/services/api-key/create/create.test.ts (next to create.ts)
import assert from "node:assert/strict";
import { afterEach, mock, test } from "node:test";
import {
apiKeyWithKeyFixture,
assertAuthAndJson,
createClient,
getCall,
jsonResponse,
mockFetch,
parseBody,
} from "@/services/api-key/test-helpers";
afterEach(() => mock.restoreAll());
test("create: POST /api/api-key/v1/ with name body", async () => {
const payload = apiKeyWithKeyFixture();
const fetchMock = mockFetch(jsonResponse(payload, 201));
const { apiKey, apiKeyError } = await createClient().apiKey.create({ name: "Production Key" });
assert.equal(apiKeyError, null);
assert.deepEqual(apiKey, payload);
const call = getCall(fetchMock);
assert.equal(call.url, "https://reloop.sh/api/api-key/v1/");
assert.equal(call.method, "POST");
assertAuthAndJson(call.headers);
assert.deepEqual(parseBody(call.body), { name: "Production Key" });
});Run tests: bun test
- API:
reloop.webhook— create, list, get, update, delete, pause/enable/disable, trigger, listDeliveries, retryDelivery - Results:
{ webhook, webhookError }(and list/delivery variants) - Verify (local):
WebhookService.constructEvent(rawBody, signatureHeader, secret)orreloop.webhook.verify({ payload, headers, secret })— HMAC-SHA256 viaX-Webhook-Signature(t=...,v1=...) - Tests: co-located under
src/services/webhook/*/.test.ts andverify/verify.test.ts - Retry route:
POST /api/webhook/deliveries/:id/retry(not under/v1)
-
npm testpasses -
npm run buildsucceeds - New routes have colocated
*.test.ts(or tests undertests/) - Types match the OpenAPI spec at reloop.sh/docs
- No API keys or secrets committed
- Public export surface unchanged unless intentional (see Conventions)
- Bump
package.jsonversion only when cutting a release (maintainers)
Version lives in package.json and package-lock.json.
# Edit package.json → "version": "2.0.1"
npm install # refreshes package-lock.json
git add package.json package-lock.json
git commit -m "chore: release v2.0.1"
git push origin mainTag must match the manifest: v + version.
git tag v2.0.1
git push origin v2.0.1Pushing v* runs .github/workflows/release.yml, which:
- Verifies the tag matches
package.json - Runs
npm testandnpm run build - Runs
npm pack - Creates a GitHub Release with:
reloop-node-{version}-source.zipreloop-email-{version}.tgz
You can also run the workflow manually: Actions → Release → Run workflow.
Registry publish uses .github/workflows/sdk-node-publish.yml or:
npm publish --provenanceRequires NPM_TOKEN (or OIDC) in repository secrets.
| Issue | Fix |
|---|---|
Tag ≠ package.json version |
Align both, then recreate the tag |
npm ci fails in CI |
Commit an updated package-lock.json |
| Release already exists | Bump patch version (e.g. 2.0.1) and tag again |