Use @postrune/sdk in server-side Node.js code. Its exported client is Postrune. Supply the deployment origin and a scoped key, and persist one idempotency key per business event. The package names are compatibility identifiers, not a different product.
Before you start #
- For
- JavaScript and TypeScript developers using Node.js 22+ or a compatible Next.js App Router application.
- Bring
- A Stampwing source checkout for building the SDK, a separate consumer app, and the Test credential from the quickstart.
- Scope
- Registry publication is not assumed. If you have no checkout or package archive, use the dependency-free HTTP sender instead. Next.js snippets show backend integration, not a ready-made public send endpoint.
1. Build a local installable package #
From the Stampwing repository root, install dependencies and create the SDK archive. npm pack runs the SDK’s prepack build and prints the archive filename. Copy its absolute path; do not install the private application root as your SDK.
npm ci
npm pack ./packages/sdk2. Install the archive in your own app #
Switch to your application’s directory. If you are creating a new Node project, run npm init -y first. Use a .mjs file for the tutorial’s ES module imports; an existing CommonJS project does not need its package-wide module type changed. Replace the path below with your actual archive path; keep quotes around paths containing spaces.
The dependency listing should contain @postrune/sdk. Keep the archive available to builds or store it in your own controlled artifact registry; a developer’s absolute path is not a portable CI dependency. Test a clean install using the same artifact before deployment.
npm install "/absolute/path/to/postrune-sdk-0.2.0.tgz"
npm ls @postrune/sdk3. Send and inspect a Test message #
Copy your private .env.stampwing from the quickstart into the consumer application folder and add .env.stampwing to that application’s .gitignore before copying the secret. The tutorial folder’s ignore rule does not protect a different folder. Run commands from the consumer folder and save this script there as send-test.mjs. The doctor call checks the key without sending and the guard refuses Live credentials or a different project. Keep POSTRUNE_EXPECTED_PROJECT_ID from the quickstart. Use a key with email:send and email:read.
import { Postrune } from '@postrune/sdk';
const operationKey = process.env.POSTRUNE_OPERATION_KEY;
const apiKey = process.env.POSTRUNE_API_KEY;
const baseUrl = process.env.POSTRUNE_BASE_URL;
const expectedProjectId = process.env.POSTRUNE_EXPECTED_PROJECT_ID?.trim();
if (!operationKey || !apiKey || !baseUrl || !expectedProjectId) {
throw new Error('Set the four POSTRUNE_ values from the quickstart.');
}
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(expectedProjectId)) {
throw new Error('Set the expected project UUID from Settings.');
}
const client = new Postrune({ apiKey, baseUrl });
const check = await client.doctor();
if (!check.credential.valid || check.sent !== false || check.credential.environment !== 'test'
|| check.credential.projectId.toLowerCase() !== expectedProjectId.toLowerCase()) {
throw new Error('This tutorial requires a verified Test credential for the expected project.');
}
for (const permission of ['email:send', 'email:read']) {
if (!check.credential.permissions.includes(permission)) {
throw new Error('Missing permission: ' + permission);
}
}
const sent = await client.send({
from: 'hello@postrune.test',
to: 'reader@example.com',
subject: 'Your Stampwing integration is ready',
text: 'This is a simulated Test message. No email is delivered.',
}, { idempotencyKey: operationKey });
console.log(sent); // Preserve this UUID even if the following read fails.
try {
console.log(await client.getEmail(sent.id));
} catch {
console.error('Send accepted as ' + sent.id + '. Status lookup failed; do not submit another message.');
process.exitCode = 2;
}4. Run it and understand retries #
This example deliberately uses the exact quickstart payload. With the same project, Test key, and saved operation key, it reads back the same message UUID instead of creating another email. If you used an older or edited quickstart payload, recover that exact payload first. Do not change the key just to bypass a conflict.
Expect the same { id, status, mode: "demo" } acceptance shape as the HTTP quickstart, followed by current delivery metadata. The SDK’s ordinary send method makes one attempt and has a 15-second deadline. A timeout is unconfirmed, not evidence of failure.
For a genuinely new authorized business event, save its new key and frozen content before the first send. If you choose sendWithRetry, supply that saved key and a bounded retry budget. Never generate a fresh key in each job attempt. PostruneError carries status, code, requestId, issues, and retryAfterMs when available; network failures can occur before a structured response exists.
node --env-file=.env.stampwing send-test.mjs5. Keep the Next.js integration on the server #
In Next.js, put POSTRUNE_API_KEY, POSTRUNE_BASE_URL, and POSTRUNE_EXPECTED_PROJECT_ID in your private .env.local or deployment secret settings and restart the app after changing them. The standalone Node script uses --env-file; Next.js loads .env.local itself. Install server-only in the consumer app with npm install server-only if needed.
Save the following helper in src/lib/stampwing.ts (or lib/stampwing.ts if your app has no src directory). Call it from an authenticated server action, route handler, or durable worker only after your app authorizes the business event. The caller must supply a recipient from trusted application data and an operation key saved with that event.
import 'server-only';
import { Postrune } from '@postrune/sdk';
export async function sendWelcomeTest(event: {
recipient: string; savedOperationKey: string;
}) {
const apiKey = process.env.POSTRUNE_API_KEY;
const baseUrl = process.env.POSTRUNE_BASE_URL;
const expectedProjectId = process.env.POSTRUNE_EXPECTED_PROJECT_ID?.trim();
if (!apiKey || !baseUrl || !expectedProjectId) throw new Error('Stampwing is not configured.');
const client = new Postrune({ apiKey, baseUrl });
const check = await client.doctor();
if (!check.credential.valid || check.sent !== false || check.credential.environment !== 'test'
|| check.credential.projectId.toLowerCase() !== expectedProjectId.toLowerCase()
|| !check.credential.permissions.includes('email:send')) {
throw new Error('Use a Test sending credential for the expected project.');
}
return client.send({
from: 'hello@postrune.test', to: event.recipient,
subject: 'Welcome to your app',
text: 'Your Test integration is connected. Delivery is simulated.',
}, { idempotencyKey: event.savedOperationKey });
}6. Use a published template #
- Create a template in the same project and Test environment, declare its variables, and preview it using synthetic values.
- Publish a version and copy its version UUID. Draft IDs and version IDs are different; a send pins the published version.
- Give the sending key email:send and templates:use. Keep email:read if you also read progress.
- Replace raw subject/text/html with templateVersionId and variables. A request cannot mix the two forms.
const templateVersionId = process.env.POSTRUNE_TEMPLATE_VERSION;
if (!templateVersionId) throw new Error('Set the published POSTRUNE_TEMPLATE_VERSION UUID.');
const result = await client.send({
from: 'hello@postrune.test', to: 'reader@example.com',
templateVersionId,
variables: { name: 'Ada' },
}, { idempotencyKey: 'template-tutorial:welcome:001' });7. Verify webhook bytes before applying changes #
Create an HTTPS endpoint in Webhooks when your installation supports it. Store its signing secret on your server separately from the API key. Verify the original request text before parsing or handling the event. A JSON parser that runs first can change the bytes and break signature verification.
Return a non-2xx response if verification or durable storage fails. Expect duplicate and out-of-order events. A webhook replay repeats a notification; it does not resend the email. Your local loopback listener is not reachable from a hosted service. For an account-free exercise, use the signed local receiver tutorial.
import { verifyWebhook } from '@postrune/sdk';
const secret = process.env.POSTRUNE_WEBHOOK_SECRET;
if (!secret) throw new Error('Webhook verification is not configured.');
const rawBody = await request.text();
const event = await verifyWebhook(rawBody, request.headers, secret);
// Persist the verified event identity and intended work atomically.
// Only then acknowledge the request with a 2xx response.
// Apply business effects asynchronously and deduplicate by event identity.8. Use HTTP or another language #
HTTP works from any backend language: the request body, Bearer header, idempotency rules, and response contract are the same. The repository also contains Python, Go, PHP, Ruby, Java, .NET, Rust, and Elixir source clients. Read docs/integrations/native-sdks.md and the relevant packages/*-sdk/README.md for that language’s build commands. Those source clients are not proof of registry publication.
Sources and further reading
Written for Stampwing with AI assistance and checked against the linked documentation. Examples are educational; simulated results are labelled. How these resources are maintained.