> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blnkfinance.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Set Up Install and Uninstall Events

> Handle install and uninstall events for your Custom App.

export const CtaCallout = props => {
  const {title, buttonLabel, href, trackingEvent, buttonTarget, rel = "noopener noreferrer", children} = props;
  const handleCtaClick = () => {
    if (typeof window === "undefined" || !trackingEvent) {
      return;
    }
    try {
      window.dispatchEvent(new CustomEvent("blnk:docs-cta", {
        detail: {
          name: trackingEvent,
          href
        }
      }));
    } catch {}
    try {
      window.posthog?.capture?.(trackingEvent, {
        href
      });
    } catch {}
    const gaPayload = {
      cta_href: href
    };
    try {
      window.gtag?.("event", trackingEvent, gaPayload);
    } catch {}
    try {
      window.dataLayer = window.dataLayer || [];
      window.dataLayer.push({
        event: trackingEvent,
        ...gaPayload
      });
    } catch {}
  };
  const isExternal = typeof href === "string" && (/^https?:\/\//i).test(href);
  const target = buttonTarget ?? (isExternal ? "_blank" : undefined);
  const linkRel = isExternal ? rel : undefined;
  return <section className="cta-callout not-prose relative my-8 w-full min-w-0 overflow-hidden rounded-xl border border-zinc-200 p-5 dark:border-white/10">
      <div className="cta-callout-noise" aria-hidden="true" />
      <div className="cta-callout-layout">
        {title ? <div className="cta-callout-title-row">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" width="14" height="14" className="cta-callout-icon shrink-0 text-zinc-800 dark:text-zinc-200" aria-hidden="true">
              <g fill="none" fillRule="nonzero">
                <path d="M28 0v28H0V0h28ZM14.691833333333335 27.134333333333334l-0.012833333333333334 0.0023333333333333335 -0.08283333333333333 0.04083333333333334 -0.023333333333333334 0.004666666666666667 -0.016333333333333335 -0.004666666666666667 -0.08283333333333333 -0.04083333333333334c-0.011666666666666667 -0.004666666666666667 -0.022166666666666668 -0.0011666666666666668 -0.028000000000000004 0.005833333333333334l-0.004666666666666667 0.011666666666666667 -0.019833333333333335 0.49933333333333335 0.005833333333333334 0.023333333333333334 0.011666666666666667 0.015166666666666667 0.12133333333333333 0.08633333333333333 0.0175 0.004666666666666667 0.014000000000000002 -0.004666666666666667 0.12133333333333333 -0.08633333333333333 0.014000000000000002 -0.018666666666666668 0.004666666666666667 -0.019833333333333335 -0.019833333333333335 -0.4981666666666667c-0.0023333333333333335 -0.011666666666666667 -0.0105 -0.019833333333333335 -0.019833333333333335 -0.021Zm0.3091666666666667 -0.13183333333333336 -0.015166666666666667 0.0023333333333333335 -0.21583333333333335 0.1085 -0.011666666666666667 0.011666666666666667 -0.0035000000000000005 0.012833333333333334 0.021 0.5016666666666667 0.005833333333333334 0.014000000000000002 0.009333333333333334 0.008166666666666668 0.23450000000000004 0.1085c0.014000000000000002 0.004666666666666667 0.026833333333333334 0 0.03383333333333334 -0.009333333333333334l0.004666666666666667 -0.016333333333333335 -0.03966666666666667 -0.7163333333333334c-0.0035000000000000005 -0.014000000000000002 -0.011666666666666667 -0.023333333333333334 -0.023333333333333334 -0.025666666666666667Zm-0.8341666666666667 0.0023333333333333335a0.026833333333333334 0.026833333333334334 0 0 0 -0.0315 0.007000000000000001l-0.007000000000000001 0.016333333333333335 -0.03966666666666667 0.7163333333333334c0 0.014000000000000002 0.008166666666666668 0.023333333333333334 0.019833333333333335 0.028000000000000004l0.0175 -0.0023333333333333335 0.23450000000000004 -0.1085 0.011666666666666667 -0.009333333333333334 0.004666666666666667 -0.012833333333333334 0.019833333333333335 -0.5016666666666667 -0.0035000000000000005 -0.014000000000000002 -0.011666666666666667 -0.011666666666666667 -0.21466666666666667 -0.10733333333333334Z" strokeWidth="1.1667" />
                <path fill="currentColor" d="M14 2.916666666666667A1.75 1.75 0 0 1 15.750000000000002 4.666666666666667v6.302333333333334L21.207666666666668 7.816666666666667a1.75 1.75 0 0 1 1.75 3.031L17.5 14l5.457666666666667 3.151166666666667a1.75 1.75 0 0 1 -1.75 3.031l-5.457666666666667 -3.1500000000000004V23.333333333333336a1.75 1.75 0 0 1 -3.5 0v-6.302333333333334L6.792333333333334 20.183333333333337a1.75 1.75 0 1 1 -1.75 -3.031L10.5 14 5.042333333333334 10.848833333333333a1.75 1.75 0 0 1 1.75 -3.031l5.457666666666667 3.1500000000000004V4.666666666666667A1.75 1.75 0 0 1 14 2.916666666666667Z" strokeWidth="1.1667" />
              </g>
            </svg>
            <p className="cta-callout-title min-w-0 font-semibold text-zinc-800 dark:text-zinc-200">
              {title}
            </p>
          </div> : null}
        <div className={`cta-callout-body text-sm leading-normal text-zinc-800 dark:text-zinc-200${title ? " cta-callout-body--indented" : ""}`}>
          {children}
        </div>
        <a href={href} target={target} rel={linkRel} onClick={handleCtaClick} data-docs-cta={trackingEvent || undefined} className="cta-callout-button inline-flex items-center justify-center gap-1 rounded-full bg-white px-3 py-1.5 text-sm font-semibold transition hover:bg-zinc-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white/50 dark:bg-white dark:hover:bg-zinc-200">
          {buttonLabel}
          <span className="cta-callout-button-arrow" aria-hidden="true">
            →
          </span>
        </a>
      </div>
    </section>;
};

<Note>
  This feature is in private beta. If you want access, please [contact Support](mailto:support@blnkfinance.com?subject=Interested%20in%20Custom%20Apps).
</Note>

A user finds your app in the Apps library, reviews the permissions it requests, and confirms the install. After that, Blnk Cloud sends an event to your app so you can provision the installation on your side.

Here's what happens during installation:

1. The user clicks `Install` from the app details page.
2. The user reviews and approves the permissions.
3. Blnk Cloud creates a scoped API key for the installation.
4. Blnk Cloud sends the install event to your backend via the [callback URL](/cloud/apps/codebase-setup#set-up-app-routes).
5. Your app stores the install details and returns a `2xx` response.
6. If the response succeeds, the app becomes active in Cloud.

<Tip>
  See [Best practices](/cloud/apps/best-practices) for how to protect API keys, handle retries safely, and prepare for production.
</Tip>

***

## Handling the install event

An install event tells your app that a user has connected it to a specific workspace and Cloud instance. The install is already scoped to that context in Blnk"you do not receive a separate organization field in the callback.

Blnk Cloud sends a payload like this:

```json install_payload.json {8} theme={"system"}
{
  "installed_app_id": "instapp_...",
  "app_id": "app_...",
  "instance_id": "inst_...",
  "api_key": "blnk_...",
  "api_key_prefix": "blnk_abc",
  "granted_permissions": ["data:read", "data:write"],
  "idempotency_key": "install:instapp_..."
}
```

| Field                 | Description                                                                           |
| --------------------- | ------------------------------------------------------------------------------------- |
| `installed_app_id`    | The ID for this specific app installation.                                            |
| `app_id`              | The ID of the app that was installed.                                                 |
| `instance_id`         | The Cloud instance where the app was installed.                                       |
| `api_key`             | The scoped API key your backend uses to call Cloud APIs for this install.             |
| `api_key_prefix`      | A safe prefix for identifying the key without exposing the full secret.               |
| `granted_permissions` | The permissions the user approved during installation.                                |
| `idempotency_key`     | A unique key for this install event. Use it to avoid processing the same event twice. |

Once you receive the install event, you need to save the install details so your backend can use them later.

<Note>
  `Note:` If your app does not return a `2xx` response within 10 seconds, Blnk cancels the installation, revokes the API key, and the install fails.
</Note>

For our Stripe Sync app, our install handler looks like this:

```js routes.ts wrap theme={"system"}
app.post("/api/callback", async (req, res) => {
  const event = req.body;

  if (event.idempotency_key?.startsWith("install:")) {

    // Never persist the raw `api_key` in your database " encrypt it at rest (see Codebase setup).
    await installs.save({
      installed_app_id: event.installed_app_id,
      app_id: event.app_id,
      instance_id: event.instance_id,
      api_key_encrypted: encryptSecret(event.api_key),
      api_key_prefix: event.api_key_prefix,
      granted_permissions: event.granted_permissions,
      status: "active",
      idempotency_key: event.idempotency_key
    });

    return res.status(200).json({ ok: true });
  } 
});
```

<Note>
  Use whatever encryption or key-management approach fits your stack before you write the secret to your database.
</Note>

***

## Handling the uninstall event

When a user uninstalls your app, Blnk Cloud revokes the app's API key and sends an uninstall event to the same [callback URL](/cloud/apps/codebase-setup#set-up-app-routes).

Your app should use this event to stop treating the installation as active. You can also clean up any resources you created for that organization or instance.

```json uninstall_payload.json {6} theme={"system"}
{
  "installed_app_id": "instapp_...",
  "app_id": "app_...",
  "instance_id": "inst_...",
  "uninstalled_at": "2026-04-21T12:00:00Z",
  "idempotency_key": "uninstall:instapp_..."
}
```

For the Stripe Sync app, uninstalling should mark the installation as inactive so the backend stops using the stored install record.

```js routes.ts wrap theme={"system"}
app.post("/api/callback", async (req, res) => {
  const event = req.body;

  if (event.idempotency_key?.startsWith("uninstall:")) {
    await installs.update({
      installed_app_id: event.installed_app_id,
      status: "inactive"
    });

    return res.status(200).json({ ok: true });
  }
});
```

<Note>
  You can also choose to delete encrypted keys, remove portal sessions, or clean up instance-specific resources.
</Note>

***

## Handling retries

Cloud may retry an install or uninstall event if it does not receive a successful response.

Your handler should be safe to run more than once. Use the `idempotency_key` to check whether you have already processed the event:

```js routes.ts wrap theme={"system"}
app.post("/api/callback", async (req, res) => {
  const event = req.body;

  const existingEvent = await checkIfEventProcessed(event.idempotency_key);

  if (existingEvent) {
    return res.status(200).json({ ok: true });
  }

  // Process the event
});
```

***

## Test the installation flow

Before moving to app development, test the install and uninstall flow from Cloud.

1. [Register your app](/cloud/apps/register-app).
2. [Install the app](/cloud/apps/install-app) from the Apps library.
3. Confirm your callback URL receives the install event.
4. Confirm your callback URL returns a `2xx` response.
5. Confirm the install is active in your database.
6. Uninstall the app from Cloud.
7. Confirm your callback URL receives the uninstall event.
8. Confirm the install is no longer active in your database.

***

<Card title="Run the example Stripe Sync app" icon="github" href="https://github.com/blnkfinance/apps-demo">
  Reference Stripe sync implementation.
</Card>

***

<CtaCallout title="Need help building your app?" href="https://blnkfinance.com/contact/us?utm_source=blnk_docs&utm_medium=documentation&utm_campaign=apps-help" buttonLabel="Get Pro Support" trackingEvent="clicked_pro_support">
  We help you build custom apps for your use case or get help building your own from scratch.
</CtaCallout>
