SDKs
Official client libraries for the Roam API. They are generated from the same OpenAPI specification that produces the API Reference, so the methods, types, and error codes here always match the documented endpoints.
| Language | Package | Status |
|---|---|---|
| TypeScript / JavaScript | @roamhq/sdk | Available |
| Go | ro.am/roamhq | Available |
| Python | roamhq | Available |
You never have to use an SDK — the API is plain HTTPS with JSON, and every
reference page shows a curl example. The SDKs exist to save you from
re-implementing cursor pagination, retry-after handling, and webhook signature
verification in every project.
These SDKs target the v1 API (https://api.ro.am/v1). If you are still on
v0, see Migrating from v0 to v1 first.
Install
npm install @roamhq/sdk
Requires Node.js 18 or later (the SDK uses the built-in fetch). If you compile
from TypeScript source, you need TypeScript 5.7 or later. The package has
no runtime dependencies.
go get ro.am/roamhq
Requires Go 1.22+. Source: WonderInventions/roam-sdk-go.
pip install roamhq
Requires Python 3.10+. Source: WonderInventions/roam-sdk-python.
Authenticate
Create a client with a token from Roam Administration → Developer. The same
constructor accepts an organization API key (rmk-…), a personal access token,
or an OAuth access token — see Access models for
which one you want.
import { RoamClient } from "@roamhq/sdk";
const client = new RoamClient({
token: process.env.ROAM_TOKEN!,
});
Never hardcode a token. Read it from the environment or a secret manager.
Your first request
const result = await client.chat.post({
groupId: "88bebce7-6cbb-4666-96f9-5c02d73e6661",
text: "Build completed successfully!",
});
console.log(result.chatId, result.timestamp);
Methods are grouped by resource and mirror the endpoint names: /chat.post
becomes client.chat.post(…), /meeting.transcript becomes
client.meeting.transcript(…), /group.list becomes client.group.list(…).
Pagination
Most list endpoints return a page object that is also an async iterable. Iterating it walks every page for you, fetching the next cursor as needed:
const messages = await client.chat.history({ chatId });
for await (const message of messages) {
console.log(message.text);
}
If you want to control paging yourself — to stop early, or to persist a cursor between runs — use the page directly:
let page = await client.group.list({ limit: 50 });
while (true) {
for (const group of page.data) {
console.log(group.id, group.name);
}
if (!page.hasNextPage()) break;
page = await page.getNextPage();
}
page.data is the current page's items; page.response is the raw response
body, including nextCursor.
A few lists are not paginated and return the response body directly — they
have no .data array and no hasNextPage():
| Method | Body field |
|---|---|
client.lobby.list() | lobbies |
client.calendar.list() | events |
client.userAuditLog.list() | auditLogs |
const { lobbies } = await client.lobby.list();
for (const lobby of lobbies ?? []) {
console.log(lobby.handle, lobby.url);
}
Errors
Failed requests throw. Every error extends RoamError, which carries the status
code, the parsed body, and the request ID you should quote in a support request:
import { RoamClient, RoamError, Roam } from "@roamhq/sdk";
try {
await client.chat.post({ groupId, text: "hello" });
} catch (err) {
if (err instanceof Roam.UnauthorizedError) {
// 401 — token is invalid, expired, or revoked
} else if (err instanceof Roam.TooManyRequestsError) {
// 429 — see "Retries" below; the SDK already retried this
} else if (err instanceof RoamError) {
console.error(err.statusCode, err.body, err.requestId);
}
throw err;
}
Typed subclasses exist for each documented status: BadRequestError,
UnauthorizedError, ForbiddenError, NotFoundError, MethodNotAllowedError,
ConflictError, ContentTooLargeError, UnsupportedMediaTypeError,
TooManyRequestsError, and InternalServerError.
For branching on why a call failed, prefer the machine-readable error field
in the body over the status code:
if (err instanceof RoamError) {
const body = err.body as Roam.Error_;
if (body.error === "missing_scope") {
console.error("Token needs one of:", body.needed);
}
}
The full catalog is in Responses and errors and Error codes.
Retries
The client retries failed requests twice by default, with exponential
backoff. It retries 408, 429, and any 5xx.
On a 429 it honors the Retry-After header rather than guessing — so the
common case of tripping the rate limit is handled without any code from you. See
Rate Limiting for the limits and response headers.
Tune it globally or per call:
const client = new RoamClient({ token, maxRetries: 5 });
await client.chat.post({ groupId, text: "hello" }, { maxRetries: 0 });
Pinning an API version
Roam uses dated API versions. By default a request uses the version stamped on your credential when it was created; the SDK does not override that.
To pin explicitly — recommended, so a later revision cannot change your
integration's behavior underneath you — set roamVersion:
const client = new RoamClient({ token, roamVersion: "2026-08-25" });
Or override a single call to try a different revision:
await client.chat.history({ chatId }, { roamVersion: "2026-08-25" });
See API versioning.
Other request options
Every method takes an optional second argument:
await client.meeting.transcript(
{ id: meetingId },
{
timeoutInSeconds: 30,
abortSignal: controller.signal,
maxRetries: 1,
},
);
To inspect response headers, use .withRawResponse():
const { data, rawResponse } = await client.token.info().withRawResponse();
console.log(rawResponse.headers.get("Roam-Version"));
Managing webhook subscriptions
Subscribe, list, and unsubscribe with typed methods on client.webhook. Event
names are dotted (chat.message, lobby.booked, magicast.created). Colon
names (chat:message:dm) are v0-only and return Unrecognized event on v1.
Optional filter limits deliveries; { chatType: "dm" } is DMs only:
const sub = await client.webhook.subscribe({
url: process.env.WEBHOOK_URL!,
event: "chat.message",
filter: { chatType: "dm" },
});
const { webhooks } = await client.webhook.list();
await client.webhook.unsubscribe({ id: sub.id });
client.fetch is the escape hatch for endpoints the SDK does not yet wrap.
Relative paths resolve against https://api.ro.am/v1 — you do not need to pass
environment or baseUrl:
const res = await client.fetch("/token.info");
v1 unsubscribe is JSON {"id"}. v0 expects application/x-www-form-urlencoded
and rejects a JSON body with id parameter required.
Verifying webhooks
Roam signs webhook deliveries with the Standard Webhooks scheme. The SDK verifies them for you, from a subpath export:
import { verifyWebhook } from "@roamhq/sdk/webhooks";
const event = verifyWebhook(rawRequestBody, headers, process.env.ROAM_WEBHOOK_SECRET!);
verifyWebhook throws if the delivery cannot be trusted, and returns the parsed
event body if it can. Treat any throw as a 401 — never fall through to
processing the payload. If your payload is not JSON, or you want to run your own
parser, use verifyWebhookSignature, which checks the signature and returns
nothing.
The return type is unknown by default, so you narrow it before use. Pass a
type argument when you know the shape:
type RoamEvent = { type: string; data?: Record<string, unknown> };
const event = verifyWebhook<RoamEvent>(rawRequestBody, headers, secret);
A complete Express receiver:
import express from "express";
import { verifyWebhook } from "@roamhq/sdk/webhooks";
type RoamEvent = { type: string; data?: Record<string, unknown> };
const seen = new Set<string>(); // process-local; use Redis etc. in production
app.post(
"/webhooks/roam",
express.raw({ type: "application/json" }),
(req, res) => {
let event: RoamEvent;
try {
event = verifyWebhook<RoamEvent>(
req.body.toString("utf8"),
req.headers,
process.env.ROAM_WEBHOOK_SECRET!,
);
} catch {
return res.sendStatus(401);
}
// A single event can currently arrive twice (legacy tagged-id body +
// v1 body) with the same webhook-id. See the Webhooks overview.
const webhookId = req.get("webhook-id");
if (webhookId) {
if (seen.has(webhookId)) return res.sendStatus(200);
seen.add(webhookId);
}
// Envelope (`2026-07-07`+): type is "chat.message", fields under data.
// Baseline / dual body: type is "message", fields at the top level.
const type = event.type;
const data =
type === "chat.message" && event.data != null ? event.data : event;
// handle the event using type + data
res.sendStatus(200);
},
);
Two things account for most verification failures on deliveries that are genuinely valid:
- Verify the raw request body, before any JSON parsing. The signature covers
the exact bytes Roam sent, and re-serializing a parsed object will not
reproduce them — key order, whitespace, and unicode escaping all shift. That
is what
express.raw()is doing above. - Pass the signing secret exactly as issued,
whsec_prefix included. The SDK strips the prefix and base64-decodes the rest to recover the key.
The verifier accepts multiple space-separated signatures, so a secret rotation does not drop deliveries. It also enforces a 300-second replay window by default. Roam signs an event once and reuses that signature across delivery retries, and the retry ladder runs to roughly six minutes — so if you would rather accept a late retry than drop it, widen the window:
verifyWebhook(rawBody, headers, secret, { toleranceInSeconds: 600 });
This part of the SDK runs on Node only; it uses node:crypto for constant-time
comparison. That is a builtin, so the package still has no dependencies.
See Webhooks for the event catalog, payload
shapes, and dual bodies for one event,
and Unfurling links for the
chat.link.shared flow specifically. The source of this SDK is not a public
GitHub repository — report problems via support.
Configuring the base URL
The client defaults to https://api.ro.am/v1. Override it only when you have
been told to — for example, to route through a proxy you control:
const client = new RoamClient({ token, baseUrl: "https://proxy.internal/v1" });
Go
The Go client is generated from the same spec as the TypeScript SDK. Install
with go get ro.am/roamhq (see Install).
Fern splits the client across sibling packages, so a call site imports two or three of them:
import (
roamhq "ro.am/roamhq" // request/response types
"ro.am/roamhq/client" // the client
"ro.am/roamhq/option" // constructor options
)
c := client.NewClient(option.WithToken(os.Getenv("ROAM_TOKEN")))
result, err := c.Chat.Post(ctx, &roamhq.PostChatRequest{
GroupID: roamhq.String("88bebce7-6cbb-4666-96f9-5c02d73e6661"),
Text: roamhq.String("Build completed successfully!"),
})
Methods are grouped the same way as TypeScript: /chat.post is
c.Chat.Post, /meeting.transcript is c.Meeting.Transcript. Optional
fields are pointers; roamhq.String / roamhq.Int are the generated
helpers. Go names follow initialisms: groupId is GroupID, url is URL.
Pagination
Paginated methods return a Page. Iterate items, or walk pages yourself:
page, err := c.Chat.History(ctx, &roamhq.HistoryChatRequest{ChatID: roamhq.String(chatID)})
if err != nil { return err }
iter := page.Iterator()
for iter.Next(ctx) {
msg := iter.Current()
fmt.Println(msg.Text)
}
if err := iter.Err(); err != nil { return err }
page, err := c.Group.List(ctx, &roamhq.ListGroupRequest{Limit: roamhq.Int(50)})
for page != nil {
for _, group := range page.Results {
fmt.Println(group.ID, group.Name)
}
page, err = page.GetNextPage(ctx)
if errors.Is(err, core.ErrNoPages) {
break
}
if err != nil { return err }
}
page.Results is the current page's items; page.Response is the raw
response body. core.ErrNoPages is the sentinel that means you are done —
import ro.am/roamhq/core.
Unpaginated lists (Lobby.List, Calendar.List, UserAuditLog.List) return
the response body directly, same as TypeScript.
Errors, retries, versions
Failed requests return an error compatible with errors.As. Typed subclasses
exist for each documented status (*roamhq.UnauthorizedError,
*roamhq.TooManyRequestsError, …); the common envelope is *core.APIError.
_, err := c.Chat.Post(ctx, req)
var unauthorized *roamhq.UnauthorizedError
var apiErr *core.APIError
if errors.As(err, &unauthorized) {
// 401
} else if errors.As(err, &apiErr) {
fmt.Println(apiErr.StatusCode)
}
Retries default to two attempts on 408, 429, and 5xx, honoring
Retry-After. Tune with option.WithMaxAttempts; disable with
option.WithoutRetries() (WithMaxAttempts(0) falls through to the default
of 2). Pin a dated API version with option.WithRoamVersion(roamhq.String("2026-08-20"))
(it takes a *string), on the client or a single call. Timeouts are the
request context.
c := client.NewClient(
option.WithToken(token),
option.WithMaxAttempts(5),
option.WithRoamVersion(roamhq.String("2026-08-20")),
option.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
)
Webhook management is on this client (c.Webhook.Subscribe, List,
Unsubscribe). Signature verification is a hand-written package, because
the generated helper rejects genuine whsec_ deliveries — same reason as
the TypeScript verifyWebhook:
import "ro.am/roamhq/webhooks"
event, err := webhooks.Verify(body, r.Header, os.Getenv("ROAM_WEBHOOK_SECRET"), nil)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
body must be the raw request bytes. See Webhooks.
Python
The Python client is generated from the same spec as the TypeScript SDK.
Install with pip install roamhq (see Install). Source:
WonderInventions/roam-sdk-python.
from roamhq import RoamClient
client = RoamClient(token=os.environ["ROAM_TOKEN"])
result = client.chat.post(
group_id="88bebce7-6cbb-4666-96f9-5c02d73e6661",
text="Build completed successfully!",
)
An async client is also generated (AsyncRoamClient). Methods take
snake_case keyword arguments.
List methods return the response body. Paginate by passing cursor= from
next_cursor:
page = client.group.list(limit=50)
for group in page.groups:
print(group.id, group.name)
while page.next_cursor:
page = client.group.list(limit=50, cursor=page.next_cursor)
Retries default to two retries on 408, 429, and 5xx, honoring
Retry-After. Tune with RoamClient(token=..., max_retries=5) or
request_options={"max_retries": 0}. Pin a dated API version with
RoamClient(token=..., roam_version="2026-08-20"). Per-request override
is request_options={"additional_headers": {"Roam-Version": "2026-08-20"}}.
Webhook management is client.webhook.subscribe / list /
unsubscribe. Signature verification is
from roamhq.webhooks import verify_webhook — hand-written, same reason
as TypeScript and Go.
Reporting problems
The SDKs are generated, so a wrong type or a missing field is almost always a
bug in the OpenAPI spec rather than in
hand-written code — which means fixing it fixes this site and every SDK at once.
Include the requestId from the error when you
contact support. The Go module source is
WonderInventions/roam-sdk-go;
the Python package source is
WonderInventions/roam-sdk-python;
the TypeScript package source is not a public GitHub repository.