Developers

Your app, already working — open it on Motion too

You do not need to be a Motion customer to publish. Bring an app people already use, keep serving everyone else as today, and add Motion as a parallel way in for companies that want it in their workspace.

The idea

One product, two surfaces

Motion is a distribution channel and a shell — not a rewrite of your app.

Your product stays yours

You already run an app people use. Motion does not replace it: you keep your servers, your accounts, your own website or product UI.

A parallel market

List the same app in the Motion App Store. Companies that live in Motion install it beside everything else they use — without leaving their portal.

Same app, another interface

Motion users and your other users hit the same product. Motion only changes how it opens: a portal tab, a signed launch, and the company's theme.

See it on a real app

Kudos, in examples/kudos, is a complete store app in one file: it checks the launch token, gives its page a session, calls the API and checks the webhooks. Same pattern as wiring Motion next to a product you already ship.

See apps already in the store
# Motion, with its demo companies
npm run dev

# Kudos, on http://localhost:4100
npm run example:kudos

# Sign in as admin@northwind.test, open Kudos

You do not need to be a Motion customer to publish. Most publishers already ship a working product elsewhere: Motion is a parallel channel. The same app keeps serving people on your own site or product UI; companies that use Motion open that same product inside their portal — another interface, not another codebase.

Your app still runs on your servers. Motion shows it in a tab (or a new browser tab), tells it who opened it, and gives it access to that company's data — only what the company's administrator granted when they installed it.

A complete example with no dependency lives in examples/kudos (npm run example:kudos).

The life of an app

  1. Become a publisher — open a publisher profile (Administration → Developers if you already have a Motion workspace, or ask the Motion team). The name you choose is shown on every app you list; you do not migrate your existing customers onto Motion.
  2. Point Motion at the app you already run — its address (/apps/<slug> in every company, unique in the store) and how it opens: in a portal tab (embedded) or in a new browser tab (link). entryUrl is your product, with a Motion-aware launch path.
  3. Write its first release — the listing (name, tagline, description, icon, category, price, privacy policy), where it runs (entryUrl), its webhook (optional) and the permissions it asks for.
  4. Try the Motion surface — Install in my workspace installs the release you are writing so you can check the portal shell. Your non-Motion users keep using the app as before; nobody else on Motion can install it until it is published.
  5. Send it for review — the Motion team checks every release before it goes live: what it asks for, where it runs, what the listing promises. Approved, it is in the store; sent back, you get a note in your bell and on the app's page.
  6. New versions — each release has a higher version. When it is approved, every company that installed the app moves to it at once, unless it asks for a new permission: then each company's administrator accepts it first (they are told in their bell), and until then they keep the previous version.

Credentials

Each app has a client id (mapp_…) and a client secret (msec_…), shown in Administration → Developers → your app. The secret never leaves your server. It:

  • signs the launch tokens Motion gives your app (HS256),
  • buys API tokens for an installation,
  • signs the webhooks Motion sends you.

New secret replaces it at once, in every company.

Opening: the launch token

Motion opens entryUrl?motion_launch=<token>. The token is a JWT:

ClaimValue
algHS256, signed with your client secret
issmotion
audyour client id
expfive minutes after it was made
subthe person's id
installationthe company's installation of your app: use it for the API
organization{ id, slug, name }
user{ id, name, isAdmin }, plus email, jobTitle, department with profile.read
permissionswhat the company granted
brandColorthe company's color
portalthe portal's origin: the only window the SDK talks to

Check it on your server (signature, iss, aud, exp), then give the page a session of your own. Third-party cookies are often blocked in frames: prefer a token your page sends in a header (as Kudos does) over a cookie.

The SDK

<script src="https://<the portal>/sdk/motion-apps.js"></script>
<script>
  MotionApp.init().then((motion) => {
    // motion.user, motion.organization, motion.permissions, motion.theme, motion.path
  });
</script>

Load it from the portal origin of the launch token. init() reads the token from the address (and takes it out of it), then asks the portal for its context.

CallWhat it does
MotionApp.init()Resolves with { token, installation, organization, user, permissions, theme, brandColor, path, embedded }
MotionApp.token()The latest launch token, for your server's requests
MotionApp.refreshToken()A new launch token (the first one lasts five minutes)
MotionApp.toast(message, "success" | "error")A short message in the portal, under your app
MotionApp.navigate(path)Keeps your app's page in the portal's address (?at=): a reload or a shared link reopens it
MotionApp.open(url)Opens an https:// address in a new browser tab
MotionApp.onTheme(listener)Called with "light" or "dark" when the person changes the portal's theme

The SDK sets data-motion-theme on your <html> and the CSS variable --motion-brand. Your app is framed with sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads", on its own origin: it never reaches the portal's page, cookies or storage. Allow Motion to frame it (frame-ancestors in your Content-Security-Policy, no X-Frame-Options: DENY).

The API

Base: https://<the portal>/api/v1. Every answer is JSON; an error is { "error": { "code", "message" } }.

An access token, for one installation

POST /api/v1/token
Content-Type: application/json

{ "client_id": "mapp_…", "client_secret": "msec_…", "installation_id": "<from the launch token or a webhook>" }

→ { "access_token": "…", "token_type": "Bearer", "expires_in": 3600, "permissions": [...] }. Send it as Authorization: Bearer <access_token>. It stops working when the company removes your app, when its workspace is suspended, or when your app is suspended.

Endpoints

CallPermissionWhat it gives
GET /installation—{ id, app, organization, permissions }
GET /users?q=&limit=&cursor=directory.readActive people: id, name, email, jobTitle, department, country; nextCursor for the next page (100 at most)
GET /users/<id>directory.readOne person
GET /groupsgroups.readid, name, description, memberIds
POST /notificationsnotifications.send{ userIds: [...], title, body?, path? } → a notification in their bell, leading to path in your app (100 people a call)
POST /announcementsannouncements.publish{ title, summary, body? } → an announcement on the home page and in News, in your app's name

Permissions

KeyAsks the company to let the app…
profile.readsee the name, email address, job title and department of the person who opens it
directory.readread the directory
groups.readread the groups and who is in each
notifications.sendput notifications in people's bell
announcements.publishpublish news on the home page

Ask for what the app needs, nothing more: the review checks each one against the listing.

Webhooks

If the release has a webhookUrl, Motion posts the installation events to it:

{
  "id": "…",
  "event": "app.installed",
  "createdAt": "2026-10-25T14:03:00.000Z",
  "app": { "id": "…", "slug": "kudos", "clientId": "mapp_…" },
  "installation": { "id": "…", "permissions": ["…"], "organization": { "id": "…", "slug": "acme", "name": "Acme" } }
}

Events: app.installed, app.updated (a company moved to a new release), app.uninstalled (delete what you kept for that installation).

Each request carries Motion-Event and Motion-Signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of "<t>.<raw body>" with your client secret. Check it in constant time and refuse a t more than five minutes away. Answer 2xx within five seconds; there is one try, and every delivery shows on your app's page for 30 days.

Where an app may run

https:// on a public host. While developing, http://localhost works on a Motion that runs in development; a production Motion never calls into a private network (webhooks included).