Apps

Apps

Reference article: Part of the API reference. To post as an App, see Incoming webhooks and MCP. For rich, interactive messages, see Frames.

An App is an identity your organization creates for a tool or service, such as a deploy bot, an on-call pager, or an internal assistant, so it can post into a room as itself. An App has a name, an icon, a description, a homepage, and optionally an interaction URL where Nimbus sends button presses. Its messages show its name and icon, with the author type app. An App isn't a person, and it isn't the Nimbus agent.

An App on its own can't do anything. You install it on a room, and each install gets its own credentials that let the App post into that one room.

Installs and credentials

An install puts one App on one surface:

  • a workstream, whose room it posts into,
  • a workspace's Conflux room, or
  • a room: one of a workspace's named rooms, public or private.

An App installed in a private room can read and post there like a member, until it's uninstalled or the room is archived. Only the room's members can press the controls on its Frames.

Every install gets two credentials of its own:

Credential Looks like Used for
Signing secret whsec_… Signing the bodies you send to the install's incoming webhook, and verifying the interaction callbacks Nimbus sends your App.
MCP token nmb_it_… Authenticating to the MCP server as this App, in this room.

Both are shown once, when the App is installed. Store them in your secret manager straight away.

  • Rotating an install issues a new signing secret and token. The old ones stop working immediately.
  • Uninstalling revokes both immediately. Later calls with them fail with 401 install_revoked.

Because credentials belong to one install, a leaked secret or token reaches only that one room, as that one App. Rotate or uninstall it and the leak is closed; the App's other installs keep working.

Managing Apps in Nimbus

Creating an App

Organization Admins and Global Admins create Apps on the Developers page.

  1. In the sidebar, open Developers.
  2. In the Apps section, click Create App.
  3. Enter a name, and optionally a description, icon URL, homepage URL, and interaction URL.
  4. Click Create App.

To change an App later, open its ⋯ menu and choose Edit…. To remove it, choose Delete App…. Deleting an App uninstalls it everywhere, and all of its credentials stop working.

Adding an App to a workstream

People who administer a workstream can add Apps to it: organization Admins and Global Admins who can open the workstream, and the workstream's managers.

  1. Open the workstream's settings and go to Apps.
  2. Click Choose an App, pick one, and click Add App.
  3. Copy the signing secret and MCP token from the dialog that appears. They aren't shown again.

For a workspace's Conflux room, organization admins use the Apps button in the Conflux room's header. For a room, its owners (and organization admins who can see it) use the Apps button in the room's header; this is the only way to install an App in a private room.

Seeing and removing Apps

Everyone in a room can see which Apps are installed there, and anyone in it can remove one: open the App's ⋯ menu and choose Remove from this room…. Admins of the room also see Rotate credentials…, which shows the new signing secret and token once.

The App object

{
  "id": "app_5Hc2Lq9Rv4Tx7Nb1Mz8Kw3Pd",
  "object": "app",
  "name": "Deploy Bot",
  "description": "Posts production deploys and lets the team roll back.",
  "icon_url": "https://example.com/deploy-bot.png",
  "homepage_url": "https://example.com/deploy-bot",
  "callback_url": "https://deploy-bot.example.com/nimbus/interactions",
  "created": 1758792000
}
Field Type Description
id string The App's ID (app_…).
object string Always app.
name string Shown as the author of its messages.
description string or null What the App does.
icon_url string or null Shown as its avatar.
homepage_url string or null Where people can learn more about it.
callback_url string or null The interaction URL, where Nimbus sends Frame interactions, signed with the install's secret. null if the App doesn't take interactions.
created integer When it was created, in Unix seconds.

The AppInstall object

{
  "id": "ins_8Wd3Kp6Rm1Vt4Lx9Qb2Nh7Cz",
  "object": "app_install",
  "app": "app_5Hc2Lq9Rv4Tx7Nb1Mz8Kw3Pd",
  "surface": {
    "kind": "workstream",
    "id": "3f6c2a1e-8b4d-4e2f-9a7c-5d1b0e6f2a94",
    "workspace": "default"
  },
  "hook_url": "https://api.gonimbus.ai/v1/apps/hooks/ins_8Wd3Kp6Rm1Vt4Lx9Qb2Nh7Cz",
  "mcp_url": "https://api.gonimbus.ai/v1/apps/mcp",
  "created": 1758792100,
  "rotated": null
}
Field Type Description
id string The install's ID (ins_…).
object string Always app_install.
app string The ID of the App.
surface.kind string workstream, conflux, or room.
surface.id string The workstream's ID, the Conflux room's ID, or the room's ID (__room__:room_…).
surface.workspace string The workspace the surface is in.
hook_url string This install's incoming webhook URL.
mcp_url string The MCP endpoint. The MCP token says which install you are.
created integer When it was installed, in Unix seconds.
rotated integer or null When its credentials were last rotated, in Unix seconds.
signing_secret string The signing secret (whsec_…). Only in the responses to Install an App and Rotate an install's credentials.
token string The MCP token (nmb_it_…). Same: install and rotate responses only.

Create an App

POST /v1/apps

Scope: apps:write · Accepts Idempotency-Key

Body field Type Description
name string The App's name, 1–80 characters.
description string, optional Up to 300 characters.
icon_url string, optional An https URL, up to 2,048 characters.
homepage_url string, optional An https URL, up to 2,048 characters.
callback_url string, optional The interaction URL: a public https URL, up to 2,048 characters. Leave it out if the App won't use interactive Frames.

::: code-group

curl https://api.gonimbus.ai/v1/apps \
  -H "Authorization: Bearer $NIMBUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Deploy Bot",
    "description": "Posts production deploys and lets the team roll back.",
    "callback_url": "https://deploy-bot.example.com/nimbus/interactions"
  }'
const response = await fetch('https://api.gonimbus.ai/v1/apps', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NIMBUS_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Deploy Bot',
    description: 'Posts production deploys and lets the team roll back.',
    callback_url: 'https://deploy-bot.example.com/nimbus/interactions',
  }),
})
const app = await response.json()
response = requests.post(
    "https://api.gonimbus.ai/v1/apps",
    headers={"Authorization": f"Bearer {os.environ['NIMBUS_API_KEY']}"},
    json={
        "name": "Deploy Bot",
        "description": "Posts production deploys and lets the team roll back.",
        "callback_url": "https://deploy-bot.example.com/nimbus/interactions",
    },
)
app = response.json()

:::

Returns 201 with the App object. A callback_url fails with 400 callback_url_invalid if it doesn't use https, has a username or password in it, or points at localhost, a .internal name, or a private IP address. As with webhook endpoints, Nimbus also refuses to connect to it if its host resolves to a private address when an interaction is sent.

List Apps

Returns the organization's Apps, newest first.

GET /v1/apps

Scope: apps:read

Parameter In Type Description
limit query integer, optional How many objects to return, 1–100. Default 20.
cursor query string, optional The next_cursor from the previous page. See Pagination.

::: code-group

curl https://api.gonimbus.ai/v1/apps \
  -H "Authorization: Bearer $NIMBUS_API_KEY"
const response = await fetch('https://api.gonimbus.ai/v1/apps', {
  headers: { Authorization: `Bearer ${process.env.NIMBUS_API_KEY}` },
})
const { data: apps } = await response.json()
response = requests.get(
    "https://api.gonimbus.ai/v1/apps",
    headers={"Authorization": f"Bearer {os.environ['NIMBUS_API_KEY']}"},
)
apps = response.json()["data"]

:::

Returns a list object of App objects.

Retrieve an App

GET /v1/apps/{app}

Scope: apps:read

Parameter In Type Description
app path string The App's ID (app_…).

::: code-group

curl https://api.gonimbus.ai/v1/apps/app_5Hc2Lq9Rv4Tx7Nb1Mz8Kw3Pd \
  -H "Authorization: Bearer $NIMBUS_API_KEY"
const response = await fetch('https://api.gonimbus.ai/v1/apps/app_5Hc2Lq9Rv4Tx7Nb1Mz8Kw3Pd', {
  headers: { Authorization: `Bearer ${process.env.NIMBUS_API_KEY}` },
})
const app = await response.json()
response = requests.get(
    "https://api.gonimbus.ai/v1/apps/app_5Hc2Lq9Rv4Tx7Nb1Mz8Kw3Pd",
    headers={"Authorization": f"Bearer {os.environ['NIMBUS_API_KEY']}"},
)
app = response.json()

:::

Returns the App object.

Update an App

Changes an App's details. Send only the fields you want to change; null clears an optional field.

PATCH /v1/apps/{app}

Scope: apps:write

Parameter In Type Description
app path string The App's ID (app_…).

The request body takes these fields:

Body field Type Description
name string, optional 1–80 characters.
description string or null, optional Up to 300 characters.
icon_url string or null, optional An https URL.
homepage_url string or null, optional An https URL.
callback_url string or null, optional A public https URL, or null to stop taking interactions.

::: code-group

curl -X PATCH https://api.gonimbus.ai/v1/apps/app_5Hc2Lq9Rv4Tx7Nb1Mz8Kw3Pd \
  -H "Authorization: Bearer $NIMBUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"icon_url": "https://example.com/deploy-bot-v2.png"}'
const response = await fetch('https://api.gonimbus.ai/v1/apps/app_5Hc2Lq9Rv4Tx7Nb1Mz8Kw3Pd', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.NIMBUS_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ icon_url: 'https://example.com/deploy-bot-v2.png' }),
})
const app = await response.json()
response = requests.patch(
    "https://api.gonimbus.ai/v1/apps/app_5Hc2Lq9Rv4Tx7Nb1Mz8Kw3Pd",
    headers={"Authorization": f"Bearer {os.environ['NIMBUS_API_KEY']}"},
    json={"icon_url": "https://example.com/deploy-bot-v2.png"},
)
app = response.json()

:::

Returns the updated App object.

Delete an App

Deletes the App and uninstalls it everywhere. Every install's signing secret and token stop working at once, and an app.uninstalled event is sent for each install. This can't be undone.

DELETE /v1/apps/{app}

Scope: apps:write

Parameter In Type Description
app path string The App's ID (app_…).

::: code-group

curl -X DELETE https://api.gonimbus.ai/v1/apps/app_5Hc2Lq9Rv4Tx7Nb1Mz8Kw3Pd \
  -H "Authorization: Bearer $NIMBUS_API_KEY"
const response = await fetch('https://api.gonimbus.ai/v1/apps/app_5Hc2Lq9Rv4Tx7Nb1Mz8Kw3Pd', {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${process.env.NIMBUS_API_KEY}` },
})
const deleted = await response.json()
response = requests.delete(
    "https://api.gonimbus.ai/v1/apps/app_5Hc2Lq9Rv4Tx7Nb1Mz8Kw3Pd",
    headers={"Authorization": f"Bearer {os.environ['NIMBUS_API_KEY']}"},
)
deleted = response.json()

:::

{
  "id": "app_5Hc2Lq9Rv4Tx7Nb1Mz8Kw3Pd",
  "object": "app",
  "deleted": true
}

Install an App

Installs the App on a workstream, a workspace's Conflux room, or a public room, and returns the install's credentials. They aren't shown again, so store them before you do anything else.

POST /v1/apps/{app}/installs

Scope: apps:write

Parameter In Type Description
app path string The App's ID (app_…).

The request body takes one field, surface, in one of three shapes:

Surface Shape
A workstream { "kind": "workstream", "workstream": "<id or slug>" }. The key must be able to reach the workstream.
A Conflux room { "kind": "conflux", "workspace": "default" }. workspace defaults to default.
A room { "kind": "room", "room": "room_…" }. Public rooms only: an API key isn't a member of any room, so a private room answers 404. Its owners install Apps from the room's Apps menu in Nimbus instead.

::: code-group

curl https://api.gonimbus.ai/v1/apps/app_5Hc2Lq9Rv4Tx7Nb1Mz8Kw3Pd/installs \
  -H "Authorization: Bearer $NIMBUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"surface": {"kind": "workstream", "workstream": "q4-pricing-review"}}'
const response = await fetch('https://api.gonimbus.ai/v1/apps/app_5Hc2Lq9Rv4Tx7Nb1Mz8Kw3Pd/installs', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NIMBUS_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ surface: { kind: 'workstream', workstream: 'q4-pricing-review' } }),
})
const install = await response.json()
// Store install.signing_secret and install.token now: they aren't returned again.
response = requests.post(
    "https://api.gonimbus.ai/v1/apps/app_5Hc2Lq9Rv4Tx7Nb1Mz8Kw3Pd/installs",
    headers={"Authorization": f"Bearer {os.environ['NIMBUS_API_KEY']}"},
    json={"surface": {"kind": "workstream", "workstream": "q4-pricing-review"}},
)
install = response.json()
# Store install["signing_secret"] and install["token"] now: they aren't returned again.

:::

{
  "id": "ins_8Wd3Kp6Rm1Vt4Lx9Qb2Nh7Cz",
  "object": "app_install",
  "app": "app_5Hc2Lq9Rv4Tx7Nb1Mz8Kw3Pd",
  "surface": { "kind": "workstream", "id": "3f6c2a1e-8b4d-4e2f-9a7c-5d1b0e6f2a94", "workspace": "default" },
  "hook_url": "https://api.gonimbus.ai/v1/apps/hooks/ins_8Wd3Kp6Rm1Vt4Lx9Qb2Nh7Cz",
  "mcp_url": "https://api.gonimbus.ai/v1/apps/mcp",
  "created": 1758792100,
  "rotated": null,
  "signing_secret": "whsec_3Tq9Vx2Lm8Rb5Nc1Kw7Hd4Pj6Ys0Fz2A",
  "token": "nmb_it_7Kp2Rt9Vx4Lb8Nc1Wz6Hd3Qm5Fs0Yj2Ea7Gu4Ci9"
}

An App can be installed only once on each surface. Installing it again on the same one fails with 400 app_already_installed; rotate the existing install if you need new credentials.

List an App's installs {#list-an-apps-installs}

Returns the App's current installs, newest first: its Conflux room installs, its installs in public rooms, and its installs on workstreams this key can reach. Uninstalled ones are left out.

GET /v1/apps/{app}/installs

Scope: apps:read

Parameter In Type Description
app path string The App's ID (app_…).
limit query integer, optional How many objects to return, 1–100. Default 20.
cursor query string, optional The next_cursor from the previous page.

::: code-group

curl https://api.gonimbus.ai/v1/apps/app_5Hc2Lq9Rv4Tx7Nb1Mz8Kw3Pd/installs \
  -H "Authorization: Bearer $NIMBUS_API_KEY"
const response = await fetch('https://api.gonimbus.ai/v1/apps/app_5Hc2Lq9Rv4Tx7Nb1Mz8Kw3Pd/installs', {
  headers: { Authorization: `Bearer ${process.env.NIMBUS_API_KEY}` },
})
const { data: installs } = await response.json()
response = requests.get(
    "https://api.gonimbus.ai/v1/apps/app_5Hc2Lq9Rv4Tx7Nb1Mz8Kw3Pd/installs",
    headers={"Authorization": f"Bearer {os.environ['NIMBUS_API_KEY']}"},
)
installs = response.json()["data"]

:::

Returns a list object of AppInstall objects, without credentials.

Rotate an install's credentials {#rotate-an-installs-credentials}

Issues a new signing secret and MCP token for one install. The old ones stop working immediately, with no overlap, so have the App ready to take the new ones.

POST /v1/app_installs/{app_install}/rotate

Scope: apps:write

Parameter In Type Description
app_install path string The install's ID (ins_…).

::: code-group

curl -X POST https://api.gonimbus.ai/v1/app_installs/ins_8Wd3Kp6Rm1Vt4Lx9Qb2Nh7Cz/rotate \
  -H "Authorization: Bearer $NIMBUS_API_KEY"
const response = await fetch('https://api.gonimbus.ai/v1/app_installs/ins_8Wd3Kp6Rm1Vt4Lx9Qb2Nh7Cz/rotate', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.NIMBUS_API_KEY}` },
})
const { signing_secret, token } = await response.json()
response = requests.post(
    "https://api.gonimbus.ai/v1/app_installs/ins_8Wd3Kp6Rm1Vt4Lx9Qb2Nh7Cz/rotate",
    headers={"Authorization": f"Bearer {os.environ['NIMBUS_API_KEY']}"},
)
credentials = response.json()

:::

Returns the AppInstall object with the new signing_secret and token, and rotated set.

Uninstall an App

Removes the App from one room. Its signing secret and token for that install stop working immediately; calls with them fail with 401 install_revoked.

DELETE /v1/app_installs/{app_install}

Scope: apps:write

Parameter In Type Description
app_install path string The install's ID (ins_…).

::: code-group

curl -X DELETE https://api.gonimbus.ai/v1/app_installs/ins_8Wd3Kp6Rm1Vt4Lx9Qb2Nh7Cz \
  -H "Authorization: Bearer $NIMBUS_API_KEY"
const response = await fetch('https://api.gonimbus.ai/v1/app_installs/ins_8Wd3Kp6Rm1Vt4Lx9Qb2Nh7Cz', {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${process.env.NIMBUS_API_KEY}` },
})
const deleted = await response.json()
response = requests.delete(
    "https://api.gonimbus.ai/v1/app_installs/ins_8Wd3Kp6Rm1Vt4Lx9Qb2Nh7Cz",
    headers={"Authorization": f"Bearer {os.environ['NIMBUS_API_KEY']}"},
)
deleted = response.json()

:::

{
  "id": "ins_8Wd3Kp6Rm1Vt4Lx9Qb2Nh7Cz",
  "object": "app_install",
  "deleted": true
}

Installing and uninstalling send the webhook events app.installed and app.uninstalled.

Errors

Status Code When
400 callback_url_invalid callback_url isn't a public https URL.
400 app_already_installed The App is already installed on that surface.
400 parameter_invalid A field is out of range, such as an icon_url that isn't https.
403 insufficient_scope The key lacks apps:read or apps:write.
404 resource_missing No App or install has that ID, or the install is on a workstream the key can't reach or in a private room.
503 app_secrets_unavailable Install credentials can't be created right now. Retry later.

Example scenario

A platform team builds a deploy bot. An admin creates an App named Deploy Bot with an interaction URL, then installs it on the Releases workstream and stores the signing secret in the CI system. Each production deploy posts a Frame to the install's hook_url with the version, a table of services, and a Roll back button. When someone presses it, Nimbus calls the bot's interaction URL, the bot starts the rollback and replies with an updated Frame, and the card changes in place for everyone. Months later a contractor leaves; the team rotates the install's credentials in workstream settings, and nothing else changes.

FAQ

What's the difference between an App and an API key?

An API key acts for your organization across the workstreams it's given, and reads and writes many resources. An App's install credentials only let that App post into, read, and update its own messages in one room. Use an API key to build integrations; give a tool an App when it should appear in a room under its own name.

I lost an install's signing secret. Can I see it again?

No. Rotate the install's credentials, in the room's Apps settings or with Rotate an install's credentials, and deploy the new ones.

Can one App be installed in several rooms?

Yes, once per room. Each install has its own credentials, so give each deployment of your tool the credentials for the room it posts to.

Why doesn't List an App's installs show every install?

A key sees an App's Conflux room installs, its installs in public rooms, and its installs on workstreams the key can reach. Installs on other workstreams and in private rooms are left out, as those rooms are.

What happens to an App's messages when it's uninstalled?

They stay in the room. Uninstalling stops the App from posting, updating its messages, or receiving interactions there.