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

install
npm install @roundtrip/sdk
roundtrip.ts
import { 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.

send.ts
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:

multi-channel.ts
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.idstringoptional

The notification id returned by the API.

notification.get()Promise<unknown>optional

Reads the current snapshot, named slots, append history, and responses.

notification.cancel(options?)Promiseoptional

Cancels the notification.

notification.push(push, options?)Promiseoptional

Sends a follow-up push for the existing notification.

notification.content.replace(content, options?)Promiseoptional

Replaces the main durable content snapshot.

notification.content.replace(slot, update, options?)Promiseoptional

Replaces a named slot, such as decision or progress.

notification.content.append(entry, options?)Promiseoptional

Adds chronological history.

You can recreate a handle later from a stored id:

from-id.ts
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.

updates.ts
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.

decision-slot.ts
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:

channels.ts
const { channels } = await roundtrip.channels.list();

Upload files before referencing them in a notification:

files.ts
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.

webhook.ts
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.

errors.ts
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)primaryoptional

Send one notification and get a handle back.

roundtrip.notifications.create(input)multi-channeloptional

Send to one or more channels and receive every notification id.

roundtrip.notifications.fromId(id)handleoptional

Recreate a handle from a stored id.

roundtrip.notifications.get(id)readoptional

Fetch snapshot, slots, history, and responses.

roundtrip.channels.list()readoptional

List channels available to the API key.

roundtrip.files.upload(input)writeoptional

Upload bytes and receive an attachment id.

idempotencywrite optionoptional

Pass stable semantic values on SDK writes. Notification handles add the notification id, operation, and slot automatically.

notification.idempotency.*helperoptional

Build explicit scoped keys when you need to inspect or store the exact raw key.

roundtrip.idempotency.from(parts)helperoptional

Build deterministic raw keys for initial sends or low-level integrations.

roundtrip.webhooks.unwrap(input)verifyoptional

Verify a signed webhook event and parse the envelope.

roundtrip.webhooks.workflow(options)workflowoptional

Define typed workflow steps/actions and dispatch signed webhook events.

roundtrip.webhooks.router()dispatchoptional

Chain typed workflow action handlers and lower-level event handlers.

Next steps