SDK overview
The TypeScript SDK is a typed wrapper around the notification API. Use it when you want local payload validation, notification handles, idempotency helpers, file uploads, and webhook verification.
Install and create a client
npm install @roundtrip/sdkimport { RoundtripClient } from "@roundtrip/sdk";
export const roundtrip = new RoundtripClient({
apiKey: process.env.ROUNDTRIP_API_KEY!,
baseUrl: "https://api.roundtrip.sh",
});The constructor checks for a missing or malformed ak_... key and strips a
trailing slash from baseUrl.
Send a notification
Use notify for the common single-channel path. It returns a
NotificationHandle you can use for later updates.
const notification = await roundtrip.notify({
channel: "on-call",
push: {
title: "Checkout latency is high",
body: "p95 crossed 2.4s",
priority: "high",
},
content: {
title: "Checkout latency",
description: "Investigating elevated p95 latency.",
status: "warning",
},
metadata: { incidentId: "inc_42" },
});
console.log(notification.id);Use notifications.create when you target multiple channels and need every
returned id:
const result = await roundtrip.notifications.create({
channels: ["deploys", "alerts"],
push: { title: "Nightly job finished" },
});
for (const entry of result.notifications) {
console.log(entry.channel, entry.id, entry.deviceCount);
}Notification handles
A handle is the easiest way to keep a workflow moving after the first send.
notification.idstringoptionalThe notification id returned by the API.
notification.get()Promise<unknown>optionalReads the current snapshot, named slots, append history, and responses.
notification.cancel(options?)PromiseoptionalCancels the notification.
notification.push(push, options?)PromiseoptionalSends a follow-up push for the existing notification.
notification.content.replace(content, options?)PromiseoptionalReplaces the main durable content snapshot.
notification.content.replace(slot, update, options?)PromiseoptionalReplaces a named slot, such as decision or progress.
notification.content.append(entry, options?)PromiseoptionalAdds chronological history.
You can recreate a handle later from a stored id:
const notification = roundtrip.notifications.fromId("ntf_1");
await notification.content.append({
content: {
title: "Database checked",
description: "No saturation found.",
status: "success",
},
});Replace, append, and push
replace updates current UI. append adds history. Neither sends a push unless
you explicitly call push.
await notification.content.replace(
{
title: "Checkout latency",
description: "Cache miss rate is elevated.",
status: "warning",
},
{ idempotency: "cache-miss" }
);
await notification.content.append(
{
content: {
title: "Cache checked",
description: "Miss rate is elevated.",
status: "warning",
},
},
{ idempotency: "cache-checked" }
);
await notification.push(
{
title: "Decision needed",
body: "Rollback is ready.",
priority: "high",
},
{ idempotency: "decision-needed" }
);Named slots
Slots are durable named sections under the main notification. Use them for current decisions, progress, or any part of the UI you want to replace without losing history.
const incidentWorkflow = roundtrip.webhooks.workflow({
name: "incident",
actions: {
decision: ["rollback", "wait"] as const,
},
});
await notification.content.replace(
"decision",
{
content: {
title: "Approve rollback?",
description: "Rollback candidate is ready.",
status: "warning",
actions: incidentWorkflow.actions("decision", {
rollback: { label: "Rollback", style: "destructive" },
wait: { label: "Wait", style: "secondary" },
}),
},
response: {
mode: "required",
behavior: "resolve",
webhook: "https://api.example.com/roundtrip/webhooks",
},
metadata: { incidentId: "inc_42" },
},
{ idempotency: "rollback" }
);Webhook responses include targetSlot and targetRevision when a user acts on
a slot.
Channels and files
List channels available to the API key:
const { channels } = await roundtrip.channels.list();Upload files before referencing them in a notification:
import { readFile } from "node:fs/promises";
const file = await roundtrip.files.upload({
data: await readFile("quote.pdf"),
filename: "quote.pdf",
contentType: "application/pdf",
});
await roundtrip.notify({
channel: "approvals",
push: { title: "Review quote" },
content: { title: "Review quote", description: "PDF attached." },
attachments: [{ id: file.id }],
});Webhooks and workflows
Roundtrip sends signed event envelopes. For multi-step flows, use a workflow
object so action ids are generated from a typed definition and webhook handlers
receive typed step and action context.
const incidentWorkflow = roundtrip.webhooks
.workflow({
name: "incident",
actions: {
decision: ["rollback", "wait"] as const,
},
metadata: (value: unknown) => parseIncidentMetadata(value),
})
.onAction("rollback", async ({ metadata, notification }) => {
await startRollback(metadata.incidentId);
await notification.content.replace("decision", {
content: {
title: "Rollback approved",
status: "success",
},
});
})
.onAction("wait", async ({ metadata }) => {
await keepInvestigating(metadata.incidentId);
});
const refundWorkflow = roundtrip.webhooks
.workflow({
name: "refund-review",
actions: {
review: ["approve", "deny"] as const,
},
metadata: (value: unknown) => parseRefundMetadata(value),
})
.onAction("approve", async ({ metadata }) => {
await approveRefund(metadata.refundId);
})
.onAction("deny", async ({ metadata }) => {
await denyRefund(metadata.refundId);
});
export const POST = roundtrip.webhooks
.router()
.use(incidentWorkflow, refundWorkflow)
.handler({ secret: process.env.ROUNDTRIP_WEBHOOK_SECRET! });Mount multiple workflows on the same route with .use(a, b, c). SDK-generated
action ids include the workflow name, so the router can dispatch directly to the
right workflow. Use router-level .on(workflow.action(...), handler) only when
you want to compose behavior outside the workflow object.
The SDK also exports webhooks.unwrap, verifySignature, and parseWebhook for
custom handlers. Workflow handlers can infer raw curl action ids when the
workflow's action map has one clear match. When the router comes from a
RoundtripClient, handlers also receive notification, a handle for the
notification that produced the webhook.
Errors
Non-2xx API responses throw RoundtripApiError.
import { RoundtripApiError } from "@roundtrip/sdk";
try {
await roundtrip.notify({
channel: "alerts",
push: { title: "Disk full" },
});
} catch (err) {
if (err instanceof RoundtripApiError) {
console.error(err.status, err.code, err.message);
}
throw err;
}Method map
roundtrip.notify(input)primaryoptionalSend one notification and get a handle back.
roundtrip.notifications.create(input)multi-channeloptionalSend to one or more channels and receive every notification id.
roundtrip.notifications.fromId(id)handleoptionalRecreate a handle from a stored id.
roundtrip.notifications.get(id)readoptionalFetch snapshot, slots, history, and responses.
roundtrip.channels.list()readoptionalList channels available to the API key.
roundtrip.files.upload(input)writeoptionalUpload bytes and receive an attachment id.
idempotencywrite optionoptionalPass stable semantic values on SDK writes. Notification handles add the notification id, operation, and slot automatically.
notification.idempotency.*helperoptionalBuild explicit scoped keys when you need to inspect or store the exact raw key.
roundtrip.idempotency.from(parts)helperoptionalBuild deterministic raw keys for initial sends or low-level integrations.
roundtrip.webhooks.unwrap(input)verifyoptionalVerify a signed webhook event and parse the envelope.
roundtrip.webhooks.workflow(options)workflowoptionalDefine typed workflow steps/actions and dispatch signed webhook events.
roundtrip.webhooks.router()dispatchoptionalChain typed workflow action handlers and lower-level event handlers.
Next steps
Notification workflows
Build longer flows with append history, named slots, and worker patterns.
Webhook verification
Wire webhook verification into Workers, Hono, Express, and Fastify.
Idempotency
Make retries safe without hand-building long operation keys.
Examples
Copy ready-to-run payloads for common API calls.