Skip to documentation
Docs Build a Nook bot
Browse documentation
Apps & Bots · Quickstart

Build your first Nook bot

Create an app, run the standalone JavaScript starter, verify HTTPS callbacks, publish a manifest, and install it into a workspace.

The five-minute mental model

Nook Apps are external HTTPS services. Nook never executes third-party code. Your service:

  1. receives signed JSON callbacks for commands, components, modals, lifecycle, and events;
  2. responds to interactive callbacks within three seconds;
  3. exchanges an installation callback’s single-use code for a bot token;
  4. calls /api/v1/app/* with that token;
  5. sees only the scopes and channel policy the workspace administrator granted.

One published app can be installed into multiple workspaces. Each installation gets its own bot user, policy, granted scopes, and bot token. Never share a token between installations.

Prerequisites

  • Node.js 22 for the zero-dependency starter;
  • a Nook Cloud or self-hosted instance with APPS_PLATFORM_ENABLED=true;
  • a developer account allowed by APPS_DEVELOPER_POLICY;
  • an HTTPS URL reachable by the Nook API (or local HTTP explicitly allowed only in development);
  • a workspace administrator who can review and install the app.

1. Create the app

Open Settings → Developer Portal → New App. Enter a stable name and the initial interaction/event URLs. Nook returns:

CredentialPurposeHandling
App IDPublic app identifierSafe in manifest/config and token-exchange request
Client secret (nookcs_…)Exchanges installation codesShown once; secret store only
Signing secret (nooksig_…)Verifies callbacksShown once; secret store only

Save both secrets before leaving the show-once screen. Nook stores the client secret as a hash and cannot display it again. Rotate rather than attempting to recover a lost value.

2. Download the standalone starter

No access to Nook source code or a Git provider is required. Download the public, zero-dependency Node 22 starter and its manifest directly from the Nook website:

mkdir nook-bot
cd nook-bot

curl --fail --silent --show-error \
  --output server.mjs \
  https://nook.cloudylake.io/downloads/nook-bot-starter/server.mjs
curl --fail --silent --show-error \
  --output manifest.example.json \
  https://nook.cloudylake.io/downloads/nook-bot-starter/manifest.example.json

install -m 0700 -d ./data
install -m 0600 /dev/null ./data/bot.env
${EDITOR:-vi} ./data/bot.env

set -a
. ./data/bot.env
set +a
node --check server.mjs
node server.mjs

Download server.mjs · Download manifest.example.json

Enter these values in ./data/bot.env, never directly on a shell command line:

PORT=3210
NOOK_API_BASE_URL=https://api.your-nook.example
NOOK_APP_ID=<app-id>
NOOK_CLIENT_SECRET=<client-secret>
NOOK_SIGNING_SECRET=<signing-secret>
NOOK_TOKEN_STORE_PATH=./data/tokens.json

Add data/ to your local ignore rules before entering credentials. For production, inject secrets through your secret manager instead of an environment file. The example token file must be mode 0600; a real multi-instance service should use encrypted durable storage keyed by installation ID.

The starter is intentionally one auditable file with no npm dependency. Before production, split the same boundaries into focused modules:

server       raw body, bounded HTTP routing, challenge
signature    HMAC-SHA256 and 300-second replay window
lifecycle    install/revoke and single-use token exchange
interactions commands, components, modals, follow-ups
nook-client  token exchange and scoped bot API calls
token-store  encrypted installation tokens and durable dedupe

3. Make the service reachable

Production callback URLs must use trusted HTTPS. For local development, use a tunnel such as your approved Cloudflare Tunnel/ngrok equivalent, or set APPS_ALLOW_INSECURE_URLS=true only on a local Nook development instance.

Configure distinct endpoints:

https://bot.example.com/nook/interactions
https://bot.example.com/nook/events

Interactive traffic has a strict latency budget and should not queue behind background events. Keeping the endpoints distinct also lets you scale and protect them independently.

4. Verify both endpoints

Click Verify endpoint for each URL. Nook sends a signed callback similar to:

{
  "type": "url.verification",
  "challenge": "opaque-short-lived-value"
}

Verify the signature on the raw body first, parse the shared schema, then answer within ten seconds:

{ "challenge": "opaque-short-lived-value" }

Do not create a generic unauthenticated challenge route. The verification callback uses the same signature boundary as real traffic.

5. Define the manifest

Paste a draft manifest in the Developer Portal’s raw JSON editor:

{
  "schemaVersion": "2026-06-10",
  "name": "Acme Helper",
  "description": "Answers team commands and reacts to mentions.",
  "bot": { "displayName": "Acme Helper" },
  "commands": [
    {
      "name": "echo",
      "description": "Echo a line of text into the channel",
      "options": [{ "name": "text", "type": "string", "required": true, "maxLength": 200 }]
    }
  ],
  "events": ["app.mentioned"],
  "scopes": ["commands:receive", "interactions:respond", "messages:write", "events:receive"],
  "optionalScopes": ["reactions:write"]
}

Required scopes must include every capability needed by declared commands/events. Optional scopes may be declined, so your bot must feature-detect them. Ask for the smallest useful set; a broad scope request makes installation harder and increases incident impact.

6. Publish and install

  1. Save the draft and resolve every manifest validation error.
  2. Confirm both endpoint verifications are current.
  3. Publish. A published version is immutable; later changes create a new version.
  4. Open Workspace Settings → Apps, preview the app, and review required/optional scopes.
  5. Choose the channel policy (all_public or an explicit allowlist) and install.
  6. Nook creates the installation’s bot user and sends app.installed to the event endpoint.

The callback carries a fresh single-use token-exchange code with a ten-minute TTL. The starter exchanges it automatically and stores the returned nookbot_… token exactly once.

7. Invoke the command

In an allowed channel, run:

/echo text: hello from Nook

The interaction callback contains the immutable manifest command name plus resolved typed options. Dispatch on interaction.command.name, never the workspace-local invocation alias. Return:

{ "kind": "message", "text": "Echo: hello from Nook" }

The response appears as a normal bot-authored message with an APP badge. It follows the same channel history, search, permissions, reply, and reaction rules as other messages.

Minimal message API call

After installation, send through the token-derived workspace:

const response = await fetch(`${apiBaseUrl}/api/v1/app/messages`, {
  method: 'POST',
  headers: {
    authorization: `Bearer ${botToken}`,
    'content-type': 'application/json',
    'idempotency-key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    channelId,
    body: 'Hello from my bot',
  }),
});

if (!response.ok) {
  const failure = await response.json();
  throw new Error(`Nook rejected the message: ${failure.error?.code ?? response.status}`);
}

Do not place a workspace ID in the path or body. The bot token determines its installation and workspace; the requested channel must also pass the installation’s current channel policy.

Quickstart acceptance checklist

  • Both endpoints reject missing, stale, and invalid signatures before JSON parsing.
  • URL verification succeeds on interaction and event URLs.
  • Manifest publishes with the minimum required scopes.
  • Workspace admin sees the exact requested scopes and channel policy before install.
  • app.installed exchange stores one installation token without logging it.
  • /echo answers within three seconds in an allowed channel.
  • The same command is denied or hidden outside installation policy.
  • Duplicate event delivery does not repeat side effects.
  • Revoking the installation immediately kills bot API access and deletes the local token copy.

Next, implement the non-negotiable security and callback rules.

Was this guide clear?

Keep commands tied to your Customer Portal values and never paste secrets into support requests.