Skip to content

Quickstart

By the end of this guide you will have fired a trigger and watched palumb turn it into a real email — using managed palumb. You write the workflow behind a signed webhook (the Bridge); palumb runs the control plane, the durable engine, and the delivery. It exercises every part of the architecture: a channel, a subscriber, a workflow, and a real send.

  • A palumb account — sign up (email + password, then verify your email) and log in to the dashboard. That’s where you create your API key (shown once, at creation) and where you’ll later manage channels, subscribers, and billing. Your base URL is the managed endpoint (e.g. https://api.palumb.com).
  • Node.js 22+ — to run your Bridge.

Set your credentials once; every step below uses them:

Terminal window
export PALUMB_BASE_URL="https://api.palumb.com"
export PALUMB_API_KEY="pk_live_…"

A channel is a concrete provider. Enable email-smtp with your transactional email provider’s SMTP credentials (Scaleway TEM, Mailgun, Postmark, …). palumb stores them encrypted and never returns them — they never live in your app code:

Terminal window
curl -sS "$PALUMB_BASE_URL/v1/channels" \
-H "Authorization: Bearer $PALUMB_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"channelKey": "email-smtp",
"config": {
"host": "smtp.your-provider.com",
"port": 587,
"secure": false,
"username": "your-smtp-user",
"password": "your-smtp-secret",
"fromEmail": "noreply@yourdomain.com",
"fromName": "Acme"
}
}'

A subscriber is a recipient, addressed by your own external_id. Give cust_1 an email contact:

Terminal window
curl -sS -X PUT "$PALUMB_BASE_URL/v1/subscribers/cust_1" \
-H "Authorization: Bearer $PALUMB_API_KEY" \
-H 'Content-Type: application/json' \
-d '{ "contacts": { "email": ["cust1@example.com"] } }'

Your workflows are plain TypeScript built with @palumb/sdk, exposed as one signed HTTP endpoint. This workflow emails an order confirmation:

import express, { type Request, type Response } from 'express';
import { workflow, BridgeClient, serveNest } from '@palumb/sdk/bridge';
const orderCreated = workflow('orderCreated', async ({ payload, subscriber, step }) => {
const orderId = String(payload.orderId ?? 'unknown');
await step.send('confirm', 'email', () => ({
subject: `Order ${orderId} received`,
body: `<p>Thanks, ${subscriber?.subscriberId}! We got order <b>${orderId}</b>.</p>`,
}));
});
const bridge = new BridgeClient().register([orderCreated]);
const handle = serveNest(bridge, { signingKey: process.env.PALUMB_SIGNING_KEY! });
const app = express();
// Capture the raw body so the HMAC verifies against the exact bytes palumb signed.
app.use(express.json({
verify: (req, _res, buf) => { (req as Request & { rawBody?: string }).rawBody = buf.toString('utf8'); },
}));
app.all('/api/palumb', (req: Request, res: Response) => handle(req as any, res as any));
app.listen(9080, () => console.log('bridge on http://localhost:9080/api/palumb'));

Deploy this anywhere palumb can reach over HTTPS. See Writing a workflow for the line-by-line walkthrough.

Tell palumb where your Bridge lives. palumb generates a signing secret, returns it once, and uses it to sign every call to your webhook:

Terminal window
curl -sS -X PUT "$PALUMB_BASE_URL/v1/bridge" \
-H "Authorization: Bearer $PALUMB_API_KEY" \
-H 'Content-Type: application/json' \
-d '{ "webhookUrl": "https://your-app.com/api/palumb" }'
# → { "webhookUrl": "...", "signingSecret": "whsec_…" } (shown ONCE)

Set that secret as PALUMB_SIGNING_KEY in your Bridge’s environment and restart it, so serveNest can verify palumb’s signature.

Now play your app — fire orderCreated for cust_1:

Terminal window
curl -sS "$PALUMB_BASE_URL/v1/events" \
-H "Authorization: Bearer $PALUMB_API_KEY" \
-H 'Content-Type: application/json' \
-d '{ "workflowId": "orderCreated", "to": { "subscriberId": "cust_1" }, "payload": { "orderId": "O-1" } }'

The call returns 202 with the notification id.

Check the inbox of cust1@example.com — subject Order O-1 received.

  1. POST /v1/events → palumb started a run on its engine for cust_1.
  2. The engine called your Bridge (HMAC-signed) for the next step — your orderCreated workflow returned a send.
  3. palumb resolved your email-smtp channel, decrypted it, and made the real send through your provider — recording the attempt idempotently.

That is the entire loop, durably — and you operated none of it. If the engine had restarted mid-run, the run’s state would have survived. See Durability & Restate.