Labs · recipe builder

A starting point.
With the edges included.

Choose a task. Get deterministic, complete files you can inspect and run in your own environment.

Default example is ready. Nothing is executed by this page.

Send your first transactional email

No account or network in mock mode. Optional sandbox needs send scope and consumes quota/rate. There is no live-delivery mode in this starter.

Prerequisites

  • Node.js 22.23.2; no third-party packages are required.
  • For the optional authenticated sandbox: a verified sending domain and a backend API key with send scope.

Know the limits

  • Only local fixtures were executed; an authenticated API response has not been observed.
  • Every executable send path forces X-Oveyon-Sandbox: 1. Sandbox consumes quota/rate limits and does not deliver.
  • The API idempotency window is 24 hours. This minimal recipe does not persist its event key across application restarts; use the order recipe for durable application state.

Expected result: Mock mode prints mock-message-0001 and status sandbox. A real API 202 acknowledges acceptance, sandbox freezing or a hold; it does not establish delivery.

README.md
README.md
# Send your first transactional email

Maintained by Oveyon. MIT licensed. Version 1.0.0.
Canonical: https://1loc.dev/recipes/transactional-email/

Build one server-side request, keep its idempotency key across retries, and start with a local mock.

## Run locally

Node.js 22.23.2. No install is needed; there are no external dependencies.

```sh
npm run mock
npm test
```

These commands do not send email or make API requests. SQLite, when used, is experimental in Node 22.

## Prerequisites

- Node.js 22.23.2; no third-party packages are required.
- For the optional authenticated sandbox: a verified sending domain and a backend API key with send scope.

## Procedure

1. Run npm run mock to inspect a synthetic acknowledgement without network access.
2. Run npm test to exercise request construction, recoverable failures and permanent refusals.
3. For one optional authenticated sandbox call, copy .env.example to .env locally and configure your backend key and verified sender. Then run npm run sandbox.

## Expected result

Mock mode prints mock-message-0001 and status sandbox. A real API 202 acknowledges acceptance, sandbox freezing or a hold; it does not establish delivery.

## Permissions and effects

No account or network in mock mode. Optional sandbox needs send scope and consumes quota/rate. There is no live-delivery mode in this starter.

Optional backend command(s): npm run sandbox.
Keep credentials in .env on your own backend or in a secrets manager. Never commit .env. The public site does not accept credentials.

## Failures

- 401 or 403: correct backend credentials/scopes; do not retry unchanged.
- 422: inspect the stable error code and correct sender, recipient or policy.
- 429, 5xx, network uncertainty or 409 idempotency_in_progress: retry at most three times with the same key and body. Honor Retry-After; waits over 30 seconds return to the caller for durable scheduling.

## Limits and verification

- Only local fixtures were executed; an authenticated API response has not been observed.
- Every executable send path forces X-Oveyon-Sandbox: 1. Sandbox consumes quota/rate limits and does not deliver.
- The API idempotency window is 24 hours. This minimal recipe does not persist its event key across application restarts; use the order recipe for durable application state.

Local fixture tests executed on Node.js 22.23.2 on 2026-10-28. Contract snapshot reviewed; authenticated API/sandbox and real delivery were not executed.

## Sources

- https://api.oveyon.com/
- https://api.oveyon.com/v1/openapi.json
package.json
package.json
{
  "name": "1loc-transactional-email",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "license": "MIT",
  "engines": {
    "node": ">=22.23.2 <23"
  },
  "scripts": {
    "mock": "OVEYON_MODE=mock node --env-file-if-exists=.env --experimental-strip-types transactional-email/main.ts",
    "test": "node --experimental-strip-types --test transactional-email/recipe.test.ts",
    "sandbox": "OVEYON_MODE=sandbox node --env-file-if-exists=.env --experimental-strip-types transactional-email/main.ts"
  }
}
package-lock.json
package-lock.json
{
  "name": "1loc-transactional-email",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "1loc-transactional-email",
      "version": "1.0.0",
      "license": "MIT",
      "engines": {
        "node": ">=22.23.2 <23"
      }
    }
  }
}
.env.example
.env.example
# Backend only. npm run mock never uses these values.
OVEYON_API_KEY=replace_with_a_server_side_key
OVEYON_FROM=Sender <[email protected]>
[email protected]
.gitignore
.gitignore
.env
.1loc-data/
*.sqlite
*.sqlite-shm
*.sqlite-wal
node_modules/
LICENSE
LICENSE
MIT License

Copyright (c) 2026 Oveyon

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
transactional-email/main.ts
transactional-email/main.ts
import { pathToFileURL } from 'node:url';
import { cliOptions, messageAddresses, sendMessage } from '../shared/client.ts';
import { mockApi } from '../shared/mock.ts';
export async function run() {
  const options = cliOptions(mockApi().transport);
  const result = await sendMessage({ ...messageAddresses(), subject: 'Your workspace is ready', text: 'This is a synthetic welcome example.' }, { ...options, idempotencyKey: '1loc:welcome:demo-001' });
  console.log(JSON.stringify({ mode: options.mode, result, meaning: 'API acknowledgement, not proof of delivery' }, null, 2));
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) await run();
transactional-email/recipe.test.ts
transactional-email/recipe.test.ts
import test from 'node:test';
import assert from 'node:assert/strict';
import { requestForSend, sendMessage, ApiError } from '../shared/client.ts';
import { mockApi } from '../shared/mock.ts';
const payload = { from: '[email protected]', to: '[email protected]', subject: 'Synthetic test', text: 'No email is sent.' };
test('send builds a sandbox-only request and keeps data out of its URL', () => {
  const request = requestForSend(payload, 'fixture-not-a-key', 'event:demo-1');
  assert.equal(request.url, 'https://api.oveyon.com/v1/send');
  assert.equal(new Headers(request.init.headers).get('X-Oveyon-Sandbox'), '1');
  assert.equal(new Headers(request.init.headers).get('Idempotency-Key'), 'event:demo-1');
  assert.deepEqual(JSON.parse(request.init.body), payload);
  assert.throws(() => requestForSend(payload, 'fixture', 'space in key'));
});
test('retry handles 429/503 using one unchanged idempotency key and body', async () => {
  const api = mockApi([429, 503]); const waits: number[] = [];
  const result = await sendMessage(payload, { apiKey: 'fixture', idempotencyKey: 'event:demo-1', transport: api.transport, wait: async ms => { waits.push(ms); } });
  assert.equal(result.status, 'sandbox'); assert.deepEqual(waits, [0, 0]);
  assert.equal(new Set(api.requests.map(r => r.init.body)).size, 1);
  assert.equal(new Set(api.requests.map(r => new Headers(r.init.headers).get('Idempotency-Key'))).size, 1);
});
test('permanent refusals and unexpected responses stop immediately', async () => {
  for (const status of [400, 401, 403, 422]) {
    let calls = 0;
    await assert.rejects(sendMessage(payload, { apiKey: 'fixture', idempotencyKey: 'event:demo-1', transport: async () => { calls++; return Response.json({ error: 'refused' }, { status }); } }), (error: unknown) => error instanceof ApiError && !error.retryable);
    assert.equal(calls, 1);
  }
});
test('long Retry-After does not cause an early retry; network failures stay unknown', async () => {
  let calls = 0;
  await assert.rejects(sendMessage(payload, { apiKey: 'fixture', idempotencyKey: 'event:demo-1', transport: async () => { calls++; return Response.json({ error: 'rate_limited' }, { status: 429, headers: { 'Retry-After': '3600' } }); } }));
  assert.equal(calls, 1);
  await assert.rejects(sendMessage(payload, { apiKey: 'fixture', idempotencyKey: 'event:demo-1', transport: async () => { throw new Error('offline'); }, wait: async () => {} }), (error: unknown) => error instanceof ApiError && error.code === 'network_result_unknown');
});
shared/client.ts
shared/client.ts
// MIT License — Copyright (c) 2026 Oveyon.
export type Transport = (url: string, init: RequestInit) => Promise<Response>;
export type SendPayload = { from: string; to: string; subject?: string; text?: string; templateId?: string; version?: number; data?: Record<string, unknown> };
export type Options = { apiKey: string; idempotencyKey: string; transport?: Transport; wait?: (ms: number) => Promise<void>; attempts?: number };

export class ApiError extends Error {
  status: number;
  code: string;
  retryable: boolean;
  constructor(status: number, code: string, retryable: boolean) {
    super(`Oveyon request failed: ${status} ${code}`);
    this.status = status; this.code = code; this.retryable = retryable;
  }
}

export function requestForSend(payload: SendPayload, apiKey: string, key: string) {
  if (!apiKey || /[\r\n]/.test(apiKey)) throw new TypeError('A server-side API key is required.');
  if (!/^[\x21-\x7e]{1,190}$/.test(key)) throw new TypeError('Idempotency key must be 1–190 printable ASCII characters without spaces.');
  if (!payload.from || !payload.to) throw new TypeError('from and to are required.');
  if (payload.templateId && (payload.subject !== undefined || payload.text !== undefined)) throw new TypeError('A template cannot be combined with subject/text.');
  if (!payload.templateId && (!payload.subject || !payload.text)) throw new TypeError('subject and text are required by this example.');
  return {
    url: 'https://api.oveyon.com/v1/send',
    init: { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Idempotency-Key': key, 'X-Oveyon-Sandbox': '1' }, body: JSON.stringify(payload), redirect: 'error' as const },
  };
}

export async function sendMessage(payload: SendPayload, options: Options) {
  const request = requestForSend(payload, options.apiKey, options.idempotencyKey);
  const transport = options.transport ?? fetch;
  const wait = options.wait ?? ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)));
  const attempts = options.attempts ?? 3;
  if (!Number.isInteger(attempts) || attempts < 1 || attempts > 3) throw new TypeError('Use 1–3 attempts.');
  for (let attempt = 0; attempt < attempts; attempt++) {
    let response: Response;
    try {
      response = await transport(request.url, { ...request.init, signal: AbortSignal.timeout(10_000) });
    } catch {
      if (attempt + 1 === attempts) throw new ApiError(0, 'network_result_unknown', true);
      await wait(1000 * 2 ** attempt);
      continue;
    }
    let result: any;
    try { result = await response.json(); }
    catch { throw new ApiError(response.status, 'invalid_response', false); }
    if (response.status === 200 && result.status === 'duplicate' && typeof result.id === 'string') return result;
    if (response.status === 202 && ['accepted', 'sandbox', 'held'].includes(result.status) && typeof result.id === 'string') return result;
    if (response.ok) throw new ApiError(response.status, 'unexpected_response', false);
    const code = typeof result.error === 'string' ? result.error : 'unknown_error';
    const retryable = response.status === 429 || response.status >= 500 || (response.status === 409 && code === 'idempotency_in_progress');
    const error = new ApiError(response.status, code, retryable);
    if (!retryable || attempt + 1 === attempts) throw error;
    const retryAfter = response.headers.get('Retry-After');
    const advertised = retryAfter === null ? NaN : /^\d+$/.test(retryAfter) ? Number(retryAfter) * 1000 : Date.parse(retryAfter) - Date.now();
    const delay = Number.isFinite(advertised) ? Math.max(0, advertised) : 1000 * 2 ** attempt;
    // Long waits belong in a durable job queue. Do not retry earlier than requested.
    if (delay > 30_000) throw error;
    await wait(delay);
  }
  throw new Error('Unreachable attempt state.');
}

export function cliOptions(transport: Transport) {
  const mode = process.env.OVEYON_MODE ?? 'mock';
  if (!['mock', 'sandbox'].includes(mode)) throw new TypeError('OVEYON_MODE supports mock or sandbox only.');
  if (mode === 'sandbox' && !process.env.OVEYON_API_KEY) throw new TypeError('Set OVEYON_API_KEY on the backend before an explicit sandbox run.');
  return { mode, apiKey: mode === 'mock' ? 'mock-key-not-a-credential' : process.env.OVEYON_API_KEY!, transport: mode === 'mock' ? transport : fetch };
}

export function messageAddresses() {
  return { from: process.env.OVEYON_FROM ?? 'Demo <[email protected]>', to: process.env.OVEYON_TO ?? '[email protected]' };
}
shared/mock.ts
shared/mock.ts
// Synthetic, in-memory API fixture. No HTTP request or email is sent.
import type { Transport } from './client.ts';
export function mockApi(failures: number[] = []) {
  const requests: { url: string; init: RequestInit }[] = [];
  const messages = new Map<string, { body: string; id: string }>();
  const templates = new Map<string, { version: number; published: boolean }>();
  const transport: Transport = async (url, init) => {
    requests.push({ url, init });
    const forced = failures.shift();
    if (forced) return Response.json({ error: forced === 429 ? 'rate_limited' : 'injection_failed' }, { status: forced, headers: { 'Retry-After': '0' } });
    const route = new URL(url).pathname;
    const data = JSON.parse(String(init.body ?? '{}'));
    if (route.endsWith('/templates')) {
      if (templates.has(data.name)) return Response.json({ error: 'template_exists' }, { status: 409 });
      templates.set(data.name, { version: 1, published: false });
      return Response.json({ id: 1, name: data.name, version: 1, status: 'draft', vars: [{ name: 'name', kind: 'var', required: true }] }, { status: 201 });
    }
    if (route.endsWith('/publish')) {
      const name = decodeURIComponent(route.split('/').at(-2)!);
      const template = templates.get(name);
      if (!template) return Response.json({ error: 'template_not_found' }, { status: 404 });
      template.published = true;
      return Response.json({ id: 1, name, version: 1, status: 'published' });
    }
    if (data.templateId && !templates.get(data.templateId)?.published) return Response.json({ error: 'template_not_found' }, { status: 422 });
    const key = new Headers(init.headers).get('Idempotency-Key')!.toLowerCase();
    const known = messages.get(key);
    if (known) return known.body === init.body ? Response.json({ id: known.id, status: 'duplicate' }) : Response.json({ error: 'idempotency_conflict' }, { status: 409 });
    const id = `mock-message-${String(messages.size + 1).padStart(4, '0')}`;
    messages.set(key, { body: String(init.body), id });
    return Response.json({ id, status: 'sandbox' }, { status: 202 });
  };
  return { requests, transport };
}

Only combinations that are implemented

All five tasks have complete Node.js/TypeScript starters. cURL is available for the first sandbox send and the public disposable-domain lookup. Persistent outboxes, signed webhook receivers and template publication use the complete starter.

Options use local state and a URL fragment; no credentials or personal addresses are requested. Generated .env.example files contain placeholders only. A cURL request is an API example, not a substitute for an application’s persistence or retry policy.

Type two or more characters. Try “duplicate”, “webhook” or “retry”.