Packaging & distribution

Packaging & distribution

You have built and tested your app — now you need to ship it. This page covers turning your app folder into an uploadable package with npx telenow build, the size and file-count limits the validator enforces, the two ways to get it to users (a private install in just your own org, or a public marketplace listing), the readiness checklist the marketplace requires, how versions are immutable and how the publish guard works, staged rollout (who sees the app), minting app API keys and the signing secret your backend needs, the uninstall data lifecycle, your publisher profile, and the cross-org developer analytics console.

New here? Start with the Quickstart to scaffold and run an app first, then come back to ship it. For exact quotas (rows, blobs, schedules, …), see Limits & quotas.

Build the package — npx telenow build

From inside your app folder (the one with telenow.app.json), run:

npx telenow build

build does three things, in order:

  1. Validates the manifest — it runs the same checks the server runs at upload (the telenow validate rules: bad id/version, duplicate object types, tools referencing undeclared objects, http tools missing base_url, duplicate page ids, unknown scopes). If anything is an error, the build stops at the keyboard instead of failing later at upload.
  2. Bundles your React UI with esbuild — your source entry (e.g. ui/index.tsx) is compiled to a single minified ui/index.js (plus ui/index.css if you have styles). It carries an inline sourcemap and preserves component names, so the in-dashboard error overlay and your browser DevTools show real .tsx file:line. React is de-duplicated to your app's single copy so hooks work.
  3. Assembles the ZIP — it writes <id>-<version>.telenow.zip, e.g. clinic-crm-1.0.0.telenow.zip, where <id> and <version> come from your manifest.
✓ wrote clinic-crm-1.0.0.telenow.zip — upload it in the Telenow dashboard (Apps → Upload custom app).

If you have no ui (a pure data/tools/automation app), build skips the bundling step and just packages the manifest and listing files.

What's inside the .telenow.zip

This is exactly the package format the platform's validator (validate_zip) accepts. Nothing else is included.

EntryWhere it comes fromNotes
telenow.app.jsonyour manifest, rewritten$schema is stripped (editor-only); ui.entry is rewritten from your source path (ui/index.tsx) to the built ui/index.js; ui.styles is set to ui/index.css if styles were produced; screenshots is filled in from the screenshots/ folder if you didn't list them by hand.
ui/index.jsesbuild outputThe bundled, minified React UI with an inline sourcemap. Only present if your manifest has a ui.entry.
ui/index.cssesbuild outputOnly present if your UI produced a stylesheet.
README.mdyour README.mdBecomes the marketplace listing copy (bundled into the manifest's readme at install). Required for a marketplace submission (see below).
CHANGELOG.mdyour CHANGELOG.mdPer-version "what's new" notes, shown on the listing and the update prompt. Optional.
screenshots/*your screenshots/ folderListing images. Picked up automatically if you don't declare screenshots in the manifest. At least one is required for a marketplace submission.
icon.png / icon.jpg / icon.jpeg / icon.webpbeside the manifestOptional custom app logo on the listing (see Listing assets).
handlers/*.jsyour sandbox tool codeFilesPure-compute JS for js/sandbox tools, bundled at build (see Tools).

You never edit readme, changelog, or the rewritten paths by hand — the CLI fills them from the files on disk. Author README.md and CHANGELOG.md as normal Markdown next to your manifest and they ride along.

Package size & file-count limits

The validator rejects (it never silently truncates) a package that exceeds any of these. Keep your bundle lean — these also protect installing orgs from a zip-bomb. See Limits & quotas for the full quota picture.

LimitValueWhat it caps
PACKAGE_UPLOAD_LIMIT32 MBThe whole multipart upload body. Over this → 413 package exceeds the 32 MB limit.
MAX_ENTRIES400 filesTotal entries in the zip.
MAX_TOTAL_UNCOMPRESSED40 MBTotal uncompressed size (zip-bomb guard).
MAX_BUNDLE_BYTES8 MBThe built UI bundle (ui.entryui/index.js).
MAX_STYLES_BYTES2 MBThe built UI stylesheet (ui.styles).
MAX_SCREENSHOT_BYTES4 MBEach individual screenshot image (and the custom icon.*).
MAX_SCREENSHOTS5Number of screenshots.
MAX_README_BYTES64 KBREADME.md (and CHANGELOG.md each).
MAX_CODE_BYTES64 KBEach sandbox tool codeFile body.
MAX_MANIFEST_BYTES512 KBtelenow.app.json itself.

When upload fails

Anything other than the body-size limit comes back as HTTP 422 with a per-file list:

{
  "success": false,
  "error": "package validation failed",
  "errors": [
    { "file": "nope.js", "message": "tool 'calc': codeFile 'nope.js' not found in the package" }
  ]
}

The file field tells you exactly where to look:

  • A real path (e.g. "nope.js", "ui/index.js") — that entry is missing or too large.
  • "telenow.app.json" — the manifest is missing, isn't valid JSON, or failed a manifest rule.
  • "(package)" — a zip-level problem: not a valid zip, too many entries, or over the uncompressed cap.

Entries whose name contains .. or starts with / are rejected as unsafe path (directory traversal)telenow build never produces those, so you'll only see this if you hand-assemble a zip.

Two ways to distribute

1. Private upload (your org only)

The fastest path — no review, live in seconds. Use this for in-house apps, or to test the real install before you list publicly. A private upload does not require a README or screenshots.

  1. In the Telenow dashboard go to Apps → Your apps.
  2. Click Upload app zip.
  3. Pick your clinic-crm-1.0.0.telenow.zip.
  4. Review the scopes the app requests and consent to them.
  5. The app installs into your org only — its pages appear in the sidebar, its agents/KBs/workflows/hooks are wired up (see Install & uninstall below).

Under the hood this is POST /api/apps/:orgId/upload (no ?review), which publishes the version with status published and visibility private. Re-uploading a higher version upgrades the install. A private upload is never visible to any other org.

2. Publish to the marketplace (public listing)

When you want any Telenow org to discover and install your app, you submit it for admin review. There are two ways to submit — both end in the same admin queue:

  • Re-upload with reviewPOST /api/apps/:orgId/upload?review=true with your zip. This validates the package and requires a README + screenshot (below), then sets the new version's status to in_review.
  • Submit an already-uploaded versionPOST /api/apps/:orgId/:appId/submit-review. This re-runs the readiness checklist against your newest uploaded version (no zip needed) and moves it to in_review.
  • JSON manifest routePOST /api/apps/:orgId/publish with { "manifest": …, "review": true }. Same review queue; use this from CI/SDK integrations that publish manifests rather than zips. (Without review: true this route self-publishes privately — and is rejected for apps already live in the marketplace.)

Only one version per app can sit in review at a time — withdraw the pending one (POST /api/apps/:orgId/:appId/withdraw-review) if you need to swap it.

On approval the app becomes a public listing in the marketplace, where any org can install it (and consent to its scopes at install time).

Apps are free to install; if your app places calls or sends messages, those run on the installing org's own Telenow usage billing — make sure your listing says so.

Readiness checklist

Before a marketplace submission is accepted it must pass a checklist. Hit GET /api/apps/:orgId/:appId/readiness to see it at any time — it returns { version, ready, checklist } where each item is { key, label, required, ok, hint }:

KeyLabelRequiredComes from
nameApp namemanifest name (non-empty)
blurbShort descriptionmanifest blurb (non-empty)
readmeREADME / overviewREADME.md bundled into readme (non-empty)
screenshotsAt least one screenshotmanifest screenshots[] not empty
iconApp icon / logooptionalmanifest icon name or a bundled icon.*

ready is true only when every required item is ok.

If you submit before the listing is ready, you get a 422. The two submission paths report it slightly differently:

// upload?review=true with a blank README and no screenshots
{
  "success": false,
  "error": "marketplace submission needs a README and screenshots",
  "errors": [
    { "file": "README.md",     "message": "a README is required to publish to the marketplace" },
    { "file": "screenshots/",  "message": "at least one screenshot is required to publish to the marketplace" }
  ]
}
// submit-review on a not-yet-ready stored version
{
  "success": false,
  "error": "this app isn't ready for the marketplace yet",
  "checklist": [ /* the full {key,label,required,ok,hint} list so the UI shows what to fix */ ]
}

A private upload skips this check entirely — README and screenshots are only mandatory for the marketplace.

Listing assets

These come from your project, mostly automatically:

AssetSourcePurpose
Namemanifest nameTitle in the catalog + sidebar.
Blurbmanifest blurbOne-line catalog tagline.
Categorymanifest categoryOne of crm, productivity, healthcare, ecommerce, finance, support, marketing, telephony, other.
Iconmanifest icon or icon.* fileA whitelisted built-in icon name (e.g. box, calendar, stethoscope), or drop an icon.png / icon.jpg / icon.jpeg / icon.webp (≤ 4 MB) beside the manifest for a custom logo.
Screenshotsscreenshots/ folderListing images (max 5). build collects them.
Listing copyREADME.mdThe full description shown on the listing page.
What's newCHANGELOG.mdPer-version release notes, shown on the listing and the in-app update prompt.

Icon names are a fixed whitelist. An unknown icon name does not error — it silently renders the default box. The valid built-in names are: bell, book, box, briefcase, calendar, chart, clipboard, clock, card, file, heart, home, inbox, grid, list, mail, map, message, package, phone, settings, cart, star, stethoscope, ticket, user, users. See the full whitelist on Manifest reference and Dashboard UI. For a logo of your own, ship an icon.* file instead.

A clinic-crm-style telenow.app.json header gives you most of the listing for free:

// telenow.app.json
{
  "id": "clinic-crm",
  "version": "1.0.0",
  "name": "Doctor CRM",
  "category": "healthcare",
  "blurb": "Patient records, appointments & follow-up calls for clinics.",
  "icon": "calendar"
  // … objects, tools, ui, agents, workflows, inboundHooks …
}

Versioning, immutability & the publish guard

Your version should be a semantic version like 1.2.0 (major.minor.patch). telenow build warns if it isn't semver, and a non-semver version cannot move the marketplace channel forward.

"version": "1.1.0"   // bump this every release

Versions are immutable. Before any publish or submission, the platform runs precheck_publish, which rejects these with a clear error:

SituationResult
The version already exists with status published or in_review409 Conflict version X already exists — publish under a new version number
You try to publish an id already owned by another developer (app ids are global, first-come)409 Conflict app id 'X' is already taken by another developer
You self-publish (private, review=false) a new version of an app that is already public in the marketplace409 Conflict this app is published in the marketplace — submit updates for review
Your developer account is suspended by a platform admin403 Forbidden your developer account is suspended

So: to ship a change, bump the version, rebuild, and upload. You can never overwrite a 1.0.0 that is already live or in review — pick 1.0.1/1.1.0/etc.

  • Installers see an update prompt. When a new version is approved (or re-uploaded privately), orgs that already have the app installed are offered the update, and your CHANGELOG.md for that version is shown as the "what's new" notes.
  • Adding scopes requires re-consent. Scopes are frozen at install. If a new version requests additional scopes, the installing org must re-consent before the update applies — so add scopes deliberately and call them out in your changelog. See Scopes & permissions.
## 1.1.0
- New "lead callback" workflow auto-calls web leads within 5 minutes.
- Appointment list now supports semantic search.

## 1.0.0
- Initial release.

Private and public are independent

It is worth being precise about visibility, because publishing never makes an app public on its own:

  • publish_app (a private upload, or the dev-portal publish) writes the version and never changes the app's visibility. A private app stays private no matter how many versions you upload.
  • submit_for_review only moves your newest version to status in_review. Crucially, latest_version (the live pointer) is not advanced — so if your app already has a live public version, that version keeps serving to installers during the review.
  • Only a platform admin approving the app (approve_app) sets visibility = 'public' and advances latest_version to the approved version. That is the only path to the public marketplace. (Approving does not lift a platform kill switch — a disabled app stays disabled until an admin re-enables it.)
  • A rejected version goes to status rejected with a review_note (the reviewer's reason), which is surfaced back in your developer console so you can fix it and re-submit a new version.

In short: an app can be private-and-installed-in-your-org and separately have a version sitting in review — the two states do not interfere.

Staged rollout (visibility)

Within an org, you control who sees the app's sidebar pages with a rollout stage — handy for testing a new install with a few teammates before the whole org gets it. PATCH /api/apps/:orgId/:appId/visibility with:

PATCH /api/apps/:orgId/:appId/visibility
Content-Type: application/json

{ "stage": "staging", "audienceMembers": ["<user-uuid>", "<user-uuid>"] }
stageWho sees the appNotes
devJust you (the uploader) + org admins/ownersThe default "private to me" stage.
stagingAdmins/owners + the named audienceMembers allow-listThe audienceMembers (a Uuid[]) is only kept on staging; switching to any other stage clears it.
prodEveryone in the orgFull rollout.

Any other stage value → 400 stage must be dev, staging, or prod. Admins and owners always see the app regardless of stage; other members see it per the stage/audience rule. The change can be made by an org owner/admin or by the app's own uploader (install.installed_by), so a developer can stage their own app without admin rights. The response echoes { stage, audienceMembers }.

App API keys & the signing secret

An external or backend app cannot be built without an app key — that is the bearer credential your server uses to call the app-key REST API (Data, Files, Agents, Campaigns). Mint and manage them on your installed app in the dashboard (your app → API keys / Signing secret), which call these endpoints:

Method & pathWhat it does
GET /api/apps/:orgId/:appId/keysList the keys you've minted for this app (metadata only — never the secret).
POST /api/apps/:orgId/:appId/keysCreate a key → returns { key, secret }. The secret is shown ONCE; only its hash is stored. Optional { "label": "..." }.
DELETE /api/apps/:orgId/:appId/keys/:keyIdRevoke a key.
GET /api/apps/:orgId/:appId/signing-secretRead the install's signing secret → { signingSecret } (owner/admin).

Key facts:

  • You must install the app first. Creating a key before install fails with 400 install the app before creating a key.
  • A key is bound to one (org, app) — it can never touch another tenant's or another app's data.
  • The signing secret is the HMAC key your backend uses to verify the X-Telenow-Signature header on every tool call and event webhook we send you. It's the same secret used to verify app session tokens. Read it with the endpoint above (owner/admin) — see External backends for verifySignature / verifyAppToken.
# Mint a key, then use it as a bearer token against the Data API
curl -s -X POST https://api.telenow.ai/api/apps/$ORG/clinic-crm/keys \
  -H "Authorization: Bearer $DASHBOARD_JWT" -H 'Content-Type: application/json' \
  -d '{"label":"prod backend"}'
# → { "success": true, "data": { "key": "tk_live_…", "secret": "…shown once…" } }

Install & uninstall lifecycle

Understanding what happens at install/uninstall helps you reason about what a new version changes.

On install, the platform wires up everything your manifest declares:

  • UI pages become sidebar items (each ui.pages[] entry with menu: true).
  • Agents & teams from agents[] / agentTeams[] are available to build with one click, auto-bound to the app's tools.
  • Knowledge bases from knowledgeBases[] are created, chunked + embedded, and auto-attached to the app's agents.
  • Workflows, schedules, events, and inbound hooks are registered and start running on their triggers.
  • Tools are namespaced as {app_id}_{name} and offered to the org's agents.

The org consents to your scopes once, at this point. Everything runs under that consent, scoped to the installing org.

Uninstall & the data lifecycle

Uninstall takes a query flag: DELETE /api/apps/:orgId/:appId?deleteData=<bool> (default false). The confirmation dialog shows the record count from GET /api/apps/:orgId/:appId/object-count{ count }.

In every case, uninstall:

  • removes the app's sidebar pages (the dashboard returns to its prior state),
  • busts app_access (the install is no longer live), and
  • deletes the install's app_schedules (scheduled jobs stop).

Then the deleteData flag decides what happens to the data:

deleteDataWhat happens to records / blobs / KBsResponse
false (default)Kept. Objects, blobs (app_blobs), and bundled KBs all remain; a later re-install restores them, and the signing secret is preserved on the conflicting re-install.{ "dataDeleted": false }
truePurged. Blob storage and app_blobs rows are deleted, and the app's bundled KBs are soft-deleted (their chunks physically remain but become unreachable — list/search both exclude deleted KBs).{ "dataDeleted": true }

Re-installing a previously kept app brings its pages, data, and capabilities back exactly as they were.

Publisher profile

Give your apps a consistent author identity. PATCH /api/apps/:orgId/developer-profile (owner/admin) accepts:

FieldRule
displayNametext, ≤ 80 chars
taglinetext, ≤ 140 chars
supportEmailtext
websitemust be an https:// URL
supportUrlmust be an https:// URL
logomust be an https:// URL

GET /api/apps/:orgId/developer-profile reads it back. A public publisher page at GET /api/apps/:orgId/publisher/:slug lists the publisher's catalog-visible apps alongside the profile. The verified badge and suspended status are controlled by platform admins — they are not editable here.

Developer analytics

Once your app is published, the dashboard gives you a Developer analytics console (under Developers → Analytics) showing how your app performs across every org that installed it. It's backed by GET /api/apps/:orgId/:appId/dev-analytics?days=30 (publisher owner/admin only; days is clamped to 1–365), distinct from GET /:appId/metrics, which is your own org's usage. The response:

{
  "days": 30,
  "installs": { "total": 0, "active": 0, "errored": 0 },
  "activeOrgs": 0,
  "totals": { "tool_call": 0, "page_view": 0, "error": 0, "webhook_fail": 0 },
  "errorRate": 0.0,
  "series": [ { "date": "2026-06-01", "tool_call": 0 } ],
  "installsByDay": [ { "date": "2026-06-01", "installs": 0 } ]
}
  • installs — total / active (enabled) / errored installs across all orgs.
  • activeOrgs — distinct orgs with any app_metrics in the window.
  • totals — summed per metric_type across every install.
  • errorRate(totals.error + totals.webhook_fail) / sum(all metric totals).
  • series / installsByDay — daily charts (usage per day, new installs per day).

Everything here is aggregate only. You see counts and rates across the install base — you do not see any individual org's identity, data, or per-org breakdown.

Indexing tip. At publish time the platform best-effort creates expression indexes for your objects' index: true fields and for every inbound-hook dedup key (each inbound POST upserts by it). Mark a field index: true only when you actually filter or upsert on it — object-store indexes are shared platform-wide. See Data & objects.

Next