# App launch from the merchant dashboard

What your launch URL receives on install and on open, and how to confirm that LARM issued the URL just now.

## When the launch URL is opened

LARM opens the `launch_url` registered in your app version in two cases:

| `larm_intent` | When | What to do |
|---|---|---|
| `install` | The merchant clicked "Install" | Start [authorization](/guides/authentication) for this store |
| `open` | The merchant opened the installed app from the dashboard | Start the merchant's session in your app |

There is no separate `app.installed` webhook; you learn about the install from this URL and then from the code exchange.

## Parameters

LARM removes any parameters starting with `larm_` from your URL, then appends:

| Parameter | Value |
|---|---|
| `larm_store_id` | Store UUID |
| `larm_installation_id` | Installation UUID, with `open` only |
| `larm_intent` | `install` or `open` |
| `larm_issued_at` | Issue time in Unix seconds |
| `larm_nonce` | 24 random base64url characters |
| `larm_signature` | 43 base64url characters |

Example:

```http
https://app.example.com/larm/launch?larm_store_id=5b1d7a62-0c4e-4f38-9a51-2e7b3c9d8f10&larm_installation_id=c7e4a1f2-6b3d-4e8a-9c05-1d2f3a4b5c6d&larm_intent=open&larm_issued_at=1792060800&larm_nonce=Vx3mQ0aT9kLz2pR8sN4wYb1c&larm_signature=8m2Kq…
```

## Verify the URL

The signature uses a key that only LARM holds, so you cannot verify it locally. Send the parameters exactly as received to [`POST /oauth/launch/verify`](/api/oauth-launch-verify) from your server:

```js
const params = Object.fromEntries(
  [...new URL(req.url, 'https://app.example.com').searchParams].filter(([key]) => key.startsWith('larm_')),
)
const response = await fetch('https://larm.sa/oauth/launch/verify', {
  method: 'POST',
  headers: {
    Authorization: 'Basic ' + Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64'),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(params),
})
if (!response.ok) return res.status(401).send('Invalid launch URL')
const launch = await response.json()
// { valid: true, store_id, installation_id, intent, issued_at }
```

- The body carries `larm_*` keys only, all as strings. Any other key returns 400 `INVALID_LAUNCH`.
- The URL is valid for **5 minutes** from `larm_issued_at`. After that: 401 `LAUNCH_EXPIRED`.
- With `open`, the installation must be active; otherwise 401 `INSTALLATION_INACTIVE`.
- A wrong signature returns 401 `LAUNCH_SIGNATURE_INVALID`.

> **Important:** LARM does not store `larm_nonce`, so the same URL is accepted again within the five minutes. Store every `larm_nonce` you accept for at least 5 minutes and reject repeats. This stops a URL copied from browser history from opening a second session.

## Start the merchant's session

The launch URL carries no access token and grants no access. After verification:

1. Look up the `installation_id` in your database. If it is not there, or `intent=install`, start authorization.
2. Create a session in your app bound to `store_id`, with an `HttpOnly`, `Secure`, `SameSite=Lax` cookie.
3. Redirect the browser to your app page without the `larm_*` parameters, so they do not stay in history or in the `Referer` header.

Your app opens in its own window, not in a frame inside the merchant dashboard.
