Lodge API
1Quickstart
Ask an administrator to add your app in Admin → Integrations. Copy the key; it is shown once. Then check it:
GET /me returns your app's name and scopes.
Base URL: https://<your Lodge address>/api/v1. Each workspace has its own address.
https://<your Lodge address>/api/v11.1GET/me
Needs scopeevents:write
Returns the app's slug, name and scopes. Use it to verify a key.
| Status | Meaning |
|---|---|
| 200 | The key works |
| 401 | Unknown or malformed keyMissing or malformed Authorization header. Expected: Bearer hub_live_… |
curl "https://<your Lodge address>/api/v1/me" \
-H "Authorization: Bearer hub_live_…"import { createRoundhouseClient } from "./roundhouse-client";
const hub = createRoundhouseClient({
baseUrl: process.env.ROUNDHOUSE_URL!,
apiKey: process.env.ROUNDHOUSE_API_KEY!,
});
const res = await hub.me();{
"ok": true,
"app": {
"slug": "billing",
"name": "Billing",
"scopes": [
"events:write",
"tasks:write",
"crm:write"
],
"webhook_url": "https://billing.example.com/hooks/lodge",
"webhook_events": [
"task.completed"
]
}
}2Authentication
Send the key as a bearer token: Authorization: Bearer hub_live_…. Lodge keeps only a SHA-256 hash of it, so a lost key is replaced, not recovered. Scopes are set per app:
| events:write | GET /me · POST /events |
|---|---|
| tasks:write | POST /tasks |
| tasks:read | GET /tasks |
| projects:read | GET /projects |
| people:read | GET /people |
| kpis:read | GET /kpis |
| kpis:write | POST /kpi-entries |
| crm:write | POST /contacts · POST /companies |
GET /api/v1/me HTTP/1.1
Host: <your Lodge address>
Authorization: Bearer hub_live_…3Events
Record something that happened in another tool. It appears in What's happening, can notify people, and can be forwarded to other apps by webhook. Send an idempotency_key and a retry returns deduplicated: true instead of a second item.
3.1POST/events
Needs scopeevents:write
Record something that happened.
| Field | Type and rules |
|---|---|
| type | stringrequiredlower-case, dotted · max 80 chars |
| title | stringrequired1–200 chars |
| body | stringoptionaldefault ""max 2000 chars |
| url | stringoptionalhttp(s) URL · max 1000 chars |
| occurred_at | stringoptionalISO 8601 date-time with offset |
| idempotency_key | stringoptional1–200 chars |
| actor | objectoptional |
| actor.name | stringoptionalmax 120 chars |
| actor.email | stringoptionalemail |
| entity | objectoptional |
| entity.type | stringrequiredmax 60 chars |
| entity.id | stringrequiredmax 200 chars |
| project_key | stringoptional2–10 capitals or digits |
| notify | objectoptional |
| notify.emails | string[]optionalmax 50 items · each: email |
| notify.roles | enum[]optionaleach: admin | member |
| notify.title | stringoptionalmax 200 chars |
| payload | objectoptionaldefault {}any keys · values: any JSON |
- type: lower-case, dotted. Shown in the feed and used for webhook subscriptions.
- idempotency_key: repeat sends return 200 {deduplicated: true} instead of a second item.
- actor.email: if it matches a member, the feed shows their avatar.
- notify: creates in-app notifications (and emails, per each person's preference) for the people named or for every member with a role.
| Status | Meaning |
|---|---|
| 202 | Recorded |
| 200 | Already recorded under this idempotency_key |
| 422 | Invalid event |
JSON · 414 bytes
curl -X POST "https://<your Lodge address>/api/v1/events" \
-H "Authorization: Bearer hub_live_…" \
-H "Content-Type: application/json" \
-d '{"type":"order.placed","title":"Acme placed an order for 40 units","body":"Standard plan · ships next week","url":"https://billing.example.com/orders/abc","idempotency_key":"order:abc","actor":{"name":"Jane Customer","email":"jane@customer.example"},"entity":{"type":"order","id":"abc"},"project_key":"LAUNCH","notify":{"roles":["admin"],"emails":["owner@example.com"],"title":"New order"},"payload":{"units":40}}'import { createRoundhouseClient } from "./roundhouse-client";
const hub = createRoundhouseClient({
baseUrl: process.env.ROUNDHOUSE_URL!,
apiKey: process.env.ROUNDHOUSE_API_KEY!,
});
const res = await hub.postEvent({
type: "order.placed",
title: "Acme placed an order for 40 units",
body: "Standard plan · ships next week",
url: "https://billing.example.com/orders/abc",
idempotency_key: "order:abc",
actor: {
name: "Jane Customer",
email: "jane@customer.example"
},
entity: {
type: "order",
id: "abc"
},
project_key: "LAUNCH",
notify: {
roles: [
"admin"
],
emails: [
"owner@example.com"
],
title: "New order"
},
payload: {
units: 40
}
});Accepted
{
"ok": true,
"id": "1c7e9a52-3b8d-4f60-a2e1-9d4c5b6a7f80",
"feed_id": "8a2d4c6e-1f3b-4a5c-9e7d-0b1c2d3e4f5a",
"deduplicated": false,
"notified": 1,
"feed_url": "https://<your Lodge address>/feed?app=billing"
}A repeat idempotency_key
{
"ok": true,
"id": "1c7e9a52-3b8d-4f60-a2e1-9d4c5b6a7f80",
"feed_id": "8a2d4c6e-1f3b-4a5c-9e7d-0b1c2d3e4f5a",
"deduplicated": true,
"feed_url": "https://<your Lodge address>/feed?app=billing"
}Invalid
{
"error": {
"status": 422,
"message": "Invalid event.",
"issues": [
{
"path": "type",
"message": "type must look like `deal.registered`"
}
]
}
}4Tasks
Create a task, or update one you created before by sending the same external_ref. Updates are partial: send only what changed, so an app can close a task without touching its owner, date or description.
4.1POST/tasks
Needs scopetasks:write
Create or upsert a task.
| Field | Type and rules |
|---|---|
| title | stringrequired1–200 chars |
| description | stringoptionalmax 8000 chars |
| project_key | stringoptional2–10 capitals or digits |
| priority | enumoptionalA | B | C |
| status | enumoptionalnot_started | in_process | strong | completed | cancelled |
| percent | integeroptional0–100 |
| due_date | stringoptionalYYYY-MM-DD |
| owner_email | stringoptionalemail |
| collaborator_emails | string[]optionalmax 6 items · each: email |
| external_ref | stringoptional1–200 chars |
| external_url | stringoptionalhttp(s) URL · max 1000 chars |
- With external_ref, a second call updates the same task (title, description, due, owner, status/percent) instead of creating another one. The owner is notified; the task shows the app as its source with a link back.
- Updates are partial: { title, external_ref, status: "completed" } closes a task without touching its owner, date or description.
- owner_email, collaborator_emails, actor.email and notify.emails on one of your staff domains also match the same mailbox name on the others.
| Status | Meaning |
|---|---|
| 201 | Created, or updated by external_ref |
| 200 | The task was deleted in Lodge; the send is not applied (deleted: true) |
| 400 | An unknown project_keyUnknown project key LAUNCH. |
| 422 | Invalid task |
JSON · 302 bytes
curl -X POST "https://<your Lodge address>/api/v1/tasks" \
-H "Authorization: Bearer hub_live_…" \
-H "Content-Type: application/json" \
-d '{"title":"Confirm delivery date with Acme","description":"Markdown allowed","project_key":"LAUNCH","priority":"A","due_date":"2026-09-25","owner_email":"owner@example.com","collaborator_emails":["teammate@example.com"],"external_ref":"order:abc","external_url":"https://billing.example.com/orders/abc"}'import { createRoundhouseClient } from "./roundhouse-client";
const hub = createRoundhouseClient({
baseUrl: process.env.ROUNDHOUSE_URL!,
apiKey: process.env.ROUNDHOUSE_API_KEY!,
});
const res = await hub.createTask({
title: "Confirm delivery date with Acme",
description: "Markdown allowed",
project_key: "LAUNCH",
priority: "A",
due_date: "2026-09-25",
owner_email: "owner@example.com",
collaborator_emails: [
"teammate@example.com"
],
external_ref: "order:abc",
external_url: "https://billing.example.com/orders/abc"
});Created
{
"ok": true,
"task": {
"id": "9d3e2a71-0c4b-4f86-b5d2-7a1c6e8f0b34",
"title": "Confirm delivery date with Acme",
"status": "not_started",
"steps": {
"done": 0,
"total": 0
},
"percent": 0,
"due_date": "2026-09-25",
"url": "https://<your Lodge address>/tasks/9d3e2a71-0c4b-4f86-b5d2-7a1c6e8f0b34"
}
}Invalid
{
"error": {
"status": 422,
"message": "Invalid task.",
"issues": [
{
"path": "priority",
"message": "Invalid option: expected one of \"A\"|\"B\"|\"C\""
}
]
}
}4.2GET/tasks
Needs scopetasks:read
Read tasks. Each has its status and steps {done, total}. Deleted tasks are left out.
| project | KEYOnly this project's tasks. |
|---|---|
| status | open · all · not_started · in_process · strong · completed · cancelledDefault open. |
| owner | emailOnly tasks this person owns. |
| source | meOnly tasks this app created. |
| Status | Meaning |
|---|---|
| 200 | Up to 200 tasks, earliest due first |
| 404 | Unknown projectUnknown project LAUNCH. |
| 422 | An unknown status |
curl "https://<your Lodge address>/api/v1/tasks?project=LAUNCH&status=open" \
-H "Authorization: Bearer hub_live_…"import { createRoundhouseClient } from "./roundhouse-client";
const hub = createRoundhouseClient({
baseUrl: process.env.ROUNDHOUSE_URL!,
apiKey: process.env.ROUNDHOUSE_API_KEY!,
});
const res = await hub.listTasks({ project: "LAUNCH", status: "open" });OK
{
"ok": true,
"tasks": [
{
"id": "9d3e2a71-0c4b-4f86-b5d2-7a1c6e8f0b34",
"title": "Confirm delivery date with Acme",
"description": "Markdown allowed",
"status": "in_process",
"percent": 50,
"priority": "A",
"due_date": "2026-09-25",
"project_id": "e2b7c9d4-5a13-4c08-9f6e-1d2b3c4a5e6f",
"owner_id": "6b5a4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
"source_app": "billing",
"external_ref": "order:abc",
"external_url": "https://billing.example.com/orders/abc",
"updated_at": "2026-09-23T16:40:12.000Z",
"steps": {
"done": 1,
"total": 3
},
"url": "https://<your Lodge address>/tasks/9d3e2a71-0c4b-4f86-b5d2-7a1c6e8f0b34"
}
]
}Unknown project
{
"error": {
"status": 404,
"message": "Unknown project LAUNCH."
}
}5Projects and people
One source of truth for the team and the work: names, emails, titles, departments, roles.
5.1GET/projects
Needs scopeprojects:read
Keys and names, for pickers in other apps. Archived projects are left out.
| Status | Meaning |
|---|---|
| 200 | Every project that isn't archived |
curl "https://<your Lodge address>/api/v1/projects" \
-H "Authorization: Bearer hub_live_…"import { createRoundhouseClient } from "./roundhouse-client";
const hub = createRoundhouseClient({
baseUrl: process.env.ROUNDHOUSE_URL!,
apiKey: process.env.ROUNDHOUSE_API_KEY!,
});
const res = await hub.listProjects();{
"ok": true,
"projects": [
{
"id": "e2b7c9d4-5a13-4c08-9f6e-1d2b3c4a5e6f",
"key": "LAUNCH",
"name": "Spring launch",
"status": "active",
"target_date": "2026-11-30",
"url": "https://<your Lodge address>/projects/LAUNCH"
}
]
}5.2GET/people
Needs scopepeople:read
One source of truth for "the team": name, email, title, department, role.
| Status | Meaning |
|---|---|
| 200 | The active roster, by name |
curl "https://<your Lodge address>/api/v1/people" \
-H "Authorization: Bearer hub_live_…"import { createRoundhouseClient } from "./roundhouse-client";
const hub = createRoundhouseClient({
baseUrl: process.env.ROUNDHOUSE_URL!,
apiKey: process.env.ROUNDHOUSE_API_KEY!,
});
const res = await hub.listPeople();{
"ok": true,
"people": [
{
"id": "0d9c8b7a-6f5e-4d3c-2b1a-0f9e8d7c6b5a",
"full_name": "Jordan Avery",
"email": "jordan@example.com",
"title": "Project lead",
"department": "Operations",
"role": "member"
}
]
}6Scorecard
Log one number per KPI per day; a second value for the same day replaces the first. KPIs Lodge computes itself refuse logged values with 409.
6.1POST/kpi-entries
Needs scopekpis:write
Log a number on the scorecard.
| Field | Type and rules |
|---|---|
| kpi | stringrequired1–60 chars |
| value | numberrequiredfinite |
| recorded_on | stringoptionalYYYY-MM-DD |
| note | stringoptionalmax 300 chars |
- kpi is the KPI's handle (shown on its page). One value per KPI per day; a second call for the same day replaces it.
- Without recorded_on, the day is today in the workspace's time zone.
- An app that owns a number (orders placed, open tickets, revenue booked) logs it once a day.
| Status | Meaning |
|---|---|
| 201 | Logged (or replaced for that day) |
| 404 | An unknown or archived KPIUnknown KPI "orders-placed". |
| 409 | Lodge computes this KPI itself |
| 422 | Invalid KPI entry |
JSON · 94 bytes
curl -X POST "https://<your Lodge address>/api/v1/kpi-entries" \
-H "Authorization: Bearer hub_live_…" \
-H "Content-Type: application/json" \
-d '{"kpi":"orders-placed","value":3,"recorded_on":"2026-09-22","note":"Acme, Northwind, Contoso"}'import { createRoundhouseClient } from "./roundhouse-client";
const hub = createRoundhouseClient({
baseUrl: process.env.ROUNDHOUSE_URL!,
apiKey: process.env.ROUNDHOUSE_API_KEY!,
});
const res = await hub.logKpi({
kpi: "orders-placed",
value: 3,
recorded_on: "2026-09-22",
note: "Acme, Northwind, Contoso"
});Logged
{
"ok": true,
"kpi": "orders-placed",
"recorded_on": "2026-09-22",
"value": 3,
"url": "/kpis/4f6a8c0e-2b4d-4e6f-8a0c-2e4f6a8c0e2b"
}Computed by Lodge
{
"error": {
"status": 409,
"message": "KPI \"orders-placed\" is computed automatically and cannot be logged."
}
}6.2GET/kpis
Needs scopekpis:read
Every active KPI with its targets and latest logged value.
| Status | Meaning |
|---|---|
| 200 | The scorecard, in its order |
curl "https://<your Lodge address>/api/v1/kpis" \
-H "Authorization: Bearer hub_live_…"import { createRoundhouseClient } from "./roundhouse-client";
const hub = createRoundhouseClient({
baseUrl: process.env.ROUNDHOUSE_URL!,
apiKey: process.env.ROUNDHOUSE_API_KEY!,
});
const res = await hub.listKpis();{
"ok": true,
"kpis": [
{
"id": "4f6a8c0e-2b4d-4e6f-8a0c-2e4f6a8c0e2b",
"slug": "orders-placed",
"title": "Orders placed",
"description": "",
"unit": "orders",
"prefix": "",
"direction": "up",
"aggregation": "sum",
"source": "app:billing",
"target_day": null,
"target_week": 15,
"target_month": 60,
"target_quarter": null,
"target_year": null,
"featured": true,
"latest": {
"recorded_on": "2026-09-22",
"value": 3
},
"url": "/kpis/4f6a8c0e-2b4d-4e6f-8a0c-2e4f6a8c0e2b"
}
]
}7Contacts and companies
Send the people and organisations your other tools know, one at a time or up to 200 per call. Lodge merges them with what other apps sent. Your own staff are skipped. Nothing is ever sent to the people themselves, and no one in Lodge is notified.
7.1POST/contacts
Needs scopecrm:write
Send people to Contacts. One record, or { "items": [ … ] } with up to 200. Each item succeeds or fails on its own.
| Field | Type and rules |
|---|---|
| external_id | stringrequired1–200 chars |
| name | stringrequired1–200 chars |
| kind | enumrequiredpartner | pilot | lead | customer | other |
| label | stringoptionalmax 80 chars |
| status | stringoptionalmax 80 chars |
| url | stringoptionalhttp(s) URL · max 1000 chars |
| facts | objectoptionalkeys max 60 chars · values: string | number | boolean | null · text values max 500 chars · at most 30 facts |
| owner_email | stringoptionalemail |
| seen_at | stringoptionalISO 8601 date-time with offset |
| stringoptionalemail · max 320 chars | |
| phone | stringoptionalmax 60 chars |
| title | stringoptionalmax 120 chars |
| company | object | nulloptional |
| company.external_id | stringoptional1–200 chars |
| company.name | stringrequired1–200 chars |
| company.domain | stringoptionalmax 120 chars |
- People merge on your external_id, then the email address. The same person from two apps is one contact, with one card per app.
- Your staff and anyone on the roster are skipped; they are the team, not contacts.
- Fields you send overwrite; fields you leave out are kept.
| Status | Meaning |
|---|---|
| 200 | A result per item, in order (some may have failed) |
| 422 | Every item failed, or the batch was empty or over 200Send between 1 and 200 items. |
JSON · 408 bytes
curl -X POST "https://<your Lodge address>/api/v1/contacts" \
-H "Authorization: Bearer hub_live_…" \
-H "Content-Type: application/json" \
-d '{"items":[{"external_id":"user:ck7a","name":"Jane Doe","email":"jane@customer.example","phone":"+1 555 0100","title":"Operations lead","kind":"customer","company":{"external_id":"account:ck9x","name":"Example Customer Co"},"label":"Account admin","status":"Active","seen_at":"2026-09-24T14:02:00Z","url":"https://billing.example.com/users/ck7a","facts":{"Role":"Account admin","Last sign-in":"2026-09-24"}}]}'import { createRoundhouseClient } from "./roundhouse-client";
const hub = createRoundhouseClient({
baseUrl: process.env.ROUNDHOUSE_URL!,
apiKey: process.env.ROUNDHOUSE_API_KEY!,
});
const res = await hub.upsertContacts([
{
external_id: "user:ck7a",
name: "Jane Doe",
email: "jane@customer.example",
phone: "+1 555 0100",
title: "Operations lead",
kind: "customer",
company: {
external_id: "account:ck9x",
name: "Example Customer Co"
},
label: "Account admin",
status: "Active",
seen_at: "2026-09-24T14:02:00Z",
url: "https://billing.example.com/users/ck7a",
facts: {
Role: "Account admin",
"Last sign-in": "2026-09-24"
}
}
]);Merged
{
"ok": true,
"saved": 1,
"skipped": 0,
"failed": 0,
"results": [
{
"external_id": "user:ck7a",
"id": "3e1a5c7b-9d2f-4b6a-8c0e-1f3a5b7c9d2e"
}
]
}None saved
{
"ok": false,
"saved": 0,
"skipped": 0,
"failed": 1,
"results": [
{
"index": 0,
"external_id": "user:ck7a",
"error": "Invalid item.",
"code": "invalid",
"issues": [
{
"path": "kind",
"message": "Invalid option: expected one of \"partner\"|\"pilot\"|\"lead\"|\"customer\"|\"other\""
}
]
}
],
"error": {
"status": 422,
"message": "None of the items were saved."
}
}7.2POST/companies
Needs scopecrm:write
Send organizations to Contacts. One record, or { "items": [ … ] } with up to 200.
| Field | Type and rules |
|---|---|
| external_id | stringrequired1–200 chars |
| name | stringrequired1–200 chars |
| kind | enumrequiredpartner | pilot | customer | prospect |
| label | stringoptionalmax 80 chars |
| status | stringoptionalmax 80 chars |
| url | stringoptionalhttp(s) URL · max 1000 chars |
| facts | objectoptionalkeys max 60 chars · values: string | number | boolean | null · text values max 500 chars · at most 30 facts |
| owner_email | stringoptionalemail |
| seen_at | stringoptionalISO 8601 date-time with offset |
| domains | string[]optionalmax 20 items · each: max 120 chars |
| website | stringoptionalmax 300 chars |
| region | stringoptionalmax 80 chars |
| industry | stringoptionalmax 120 chars |
- Companies merge on your external_id, then the name (ignoring case), then any shared email domain.
- facts are shown on the profile as sent, in order.
- Nothing is ever sent to the people themselves and no one in Lodge is notified.
| Status | Meaning |
|---|---|
| 200 | A result per item, in order |
| 422 | Every item failed, or the batch was empty or over 200 |
JSON · 359 bytes
curl -X POST "https://<your Lodge address>/api/v1/companies" \
-H "Authorization: Bearer hub_live_…" \
-H "Content-Type: application/json" \
-d '{"items":[{"external_id":"account:ck9x","name":"Example Customer Co","kind":"customer","domains":["customer.example"],"region":"US East","industry":"Logistics","website":"customer.example","label":"Annual plan","status":"Active","url":"https://billing.example.com/accounts/ck9x","owner_email":"owner@example.com","facts":{"Plan":"Annual","Open invoices":1}}]}'import { createRoundhouseClient } from "./roundhouse-client";
const hub = createRoundhouseClient({
baseUrl: process.env.ROUNDHOUSE_URL!,
apiKey: process.env.ROUNDHOUSE_API_KEY!,
});
const res = await hub.upsertCompanies([
{
external_id: "account:ck9x",
name: "Example Customer Co",
kind: "customer",
domains: [
"customer.example"
],
region: "US East",
industry: "Logistics",
website: "customer.example",
label: "Annual plan",
status: "Active",
url: "https://billing.example.com/accounts/ck9x",
owner_email: "owner@example.com",
facts: {
Plan: "Annual",
"Open invoices": 1
}
}
]);{
"ok": true,
"saved": 1,
"skipped": 0,
"failed": 0,
"results": [
{
"external_id": "account:ck9x",
"id": "7a9c1e3b-5d7f-4a2c-8e0b-3d5f7a9c1e3b"
}
]
}8Webhooks
Set a webhook URL and the events you want on your app's page. Each delivery carries four headers and a signature over the timestamp and the raw body: sha256=HMAC(secret, "<timestamp>.<raw body>"). Failed deliveries retry six times with quadratic back-off and show on the app's admin page. An app never receives its own events back.
| X-Roundhouse-Event | the event kind |
|---|---|
| X-Roundhouse-Delivery | one id per delivery |
| X-Roundhouse-Timestamp | Unix seconds |
| X-Roundhouse-Signature | sha256=<hex> |
Kinds Lodge sends
task.createdtask.completedproject.createdproject.statusevent.createdannouncement.publisheddocument.uploadedpage.createdmember.joinedchannel.createdkudos.givengoal.createdgoal.statuskpi.created
- DEMO SECRET
- whsec_demo_…
- X-Roundhouse-Event
- task.completed
- X-Roundhouse-Delivery
- 5b0f6c2a-81e4-4d9b-a3c7-2f1e9d8c7b6a
- X-Roundhouse-Timestamp
- …
- X-Roundhouse-Signature
- …
- Signs
- …
…
import { verifyRoundhouseWebhook } from "./roundhouse-client";
export async function POST(request: Request) {
const rawBody = await request.text();
const ok = await verifyRoundhouseWebhook({
secret: process.env.ROUNDHOUSE_WEBHOOK_SECRET!,
timestamp: request.headers.get("x-roundhouse-timestamp"),
signature: request.headers.get("x-roundhouse-signature"),
rawBody,
});
if (!ok) return Response.json({ error: "bad signature" }, { status: 401 });
// …handle JSON.parse(rawBody)
return Response.json({ received: true });
}9Errors
Every error has the same shape. 401 unknown key · 403 app disabled or missing scope · 422 validation, with the field that failed · 404 unknown project.
| Status | Meaning |
|---|---|
| 401 | No key, or a key Lodge doesn't knowUnknown API key. |
| 403 | The app is disabled, or lacks the scopeApp "billing" lacks the tasks:write scope. |
| 422 | The body failed validation; issues name the field |
| 404 | An unknown project (GET /tasks) |
| 429 | Too many calls for this app, or too many bad keys from one network; see Retry-AfterRate limit exceeded for this app. |
| 500 | Lodge's own failure; the details stay in its logSomething went wrong on our side. It has been logged. |
{
"error": {
"status": 422,
"message": "Invalid event.",
"issues": [
{
"path": "type",
"message": "type must look like `deal.registered`"
}
]
}
}10The client
integrations/roundhouse-client.ts is one TypeScript file with no dependencies. Copy it into any app; it covers every endpoint and verifies webhooks on the receiving side.
| me | GET /me |
|---|---|
| postEvent | POST /events |
| createTask | POST /tasks |
| logKpi | POST /kpi-entries |
| listKpis | GET /kpis |
| listTasks | GET /tasks |
| listProjects | GET /projects |
| upsertContacts | POST /contacts |
| upsertCompanies | POST /companies |
| listPeople | GET /people |
| verifyRoundhouseWebhook | receiving side |
const hub = createRoundhouseClient({ baseUrl: process.env.ROUNDHOUSE_URL!, apiKey: process.env.ROUNDHOUSE_API_KEY! });
await hub.postEvent({ type: "ticket.opened", title: "Login loop on Android", idempotency_key: "ticket:123" });
await hub.createTask({ title: "Fix Android login loop", priority: "A", owner_email: "dev@example.com", external_ref: "ticket:123" });11Names that don't change
Some names in this contract say "Roundhouse": the X-Roundhouse-* webhook headers, the client file and its environment variables. They come from Lodge's earlier name and are fixed on purpose, so your integrations never break.
| X-Roundhouse-Event | webhook header · the event kind |
|---|---|
| X-Roundhouse-Delivery | webhook header · one id per delivery |
| X-Roundhouse-Timestamp | webhook header · Unix seconds |
| X-Roundhouse-Signature | webhook header · sha256=<hex> |
| ROUNDHOUSE_URL | env · your Lodge address |
| ROUNDHOUSE_API_KEY | env · the app's key |
| ROUNDHOUSE_WEBHOOK_SECRET | env · the webhook secret |
| integrations/roundhouse-client.ts | the client file |
| createRoundhouseClient | client · calls the API |
| verifyRoundhouseWebhook | client · checks a delivery |
ROUNDHOUSE_URL=https://<your Lodge address>
ROUNDHOUSE_API_KEY=hub_live_…
ROUNDHOUSE_WEBHOOK_SECRET=whsec_…