@apex-inc/transactional
v0.2.0
Published
Apex transactional email client for Node 18+ and modern runtimes (Edge, Deno, Bun, browsers).
Maintainers
Readme
@apex-inc/transactional
Apex transactional email client for Node 18+, modern runtimes (Edge, Deno, Bun), and browsers.
Works against POST /api/v1/transactional/send using a workspace apex_tx_* secret. Built-in retries with safe Idempotency-Key replay, sandbox helpers, and typed errors.
Install
npm install @apex-inc/transactionalQuickstart
import { ApexTransactionalClient } from "@apex-inc/transactional";
const client = new ApexTransactionalClient({
baseUrl: "https://app.apex.inc",
apiKey: process.env.APEX_TX_KEY!, // apex_tx_…
});
const result = await client.send({
to: "[email protected]",
subject: "You've been invited",
html: "<p>Welcome aboard!</p>",
text: "Welcome aboard!",
});
console.log(result.id, result.messageId);Sandbox
import { ApexTransactionalClient, sandboxRecipient } from "@apex-inc/transactional";
const sandbox = new ApexTransactionalClient({
baseUrl: "https://app.apex.inc",
apiKey: process.env.APEX_TX_KEY!,
sandbox: true, // forces X-Apex-Mode: sandbox on every call
});
await sandbox.send({
to: sandboxRecipient("bounce"),
subject: "Test bounce",
text: "ignored",
}); // -> throws ApexSendFailedError code="bounce"Retries + idempotency
The client always sends an Idempotency-Key header. If you don't provide one, it generates a per-call key so transient retries (network errors, 5xx, 429) can't cause duplicates. The same key is reused across retry attempts.
import {
ApexAuthError,
ApexRateLimitError,
ApexSendFailedError,
} from "@apex-inc/transactional";
try {
await client.send({ to, subject, text });
} catch (err) {
if (err instanceof ApexAuthError) /* rotate keys */;
if (err instanceof ApexRateLimitError) /* back off */;
if (err instanceof ApexSendFailedError) /* err.code: bounce | suppressed | send_failed */;
}Configuration
| Option | Default | Notes |
|---|---|---|
| baseUrl | — | required |
| apiKey | — | required, must start with apex_tx_ |
| sandbox | false | per-call override available |
| timeoutMs | 30000 | per request |
| maxRetries | 3 | total attempts (initial + retries) |
| retryBaseDelayMs | 250 | exponential backoff with full jitter |
| retryMaxDelayMs | 5000 | cap |
| fetchFn | global fetch | inject for tests / custom transports |
Error types
ApexAuthError(401)ApexValidationError(400)ApexIdempotencyMismatchError(409 — same key, different payload)ApexIdempotencyInFlightError(409 — concurrent retry)ApexRateLimitError(429)ApexSendFailedError(422 — bounce / suppressed / send_failed)ApexServerError(5xx)ApexNetworkError/ApexTimeoutErrorApexError(base)
All errors expose status, code, requestId, and the raw decoded body.
License
MIT
