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

# Get Started with Custom Events

> Instrument your authentication flows with explicit Corbado Observe events on web, iOS and Android.

**Custom events** are explicit **Corbado Observe** events you emit from your own code, at the semantically correct points in your flows. You get exactly the granularity you define. Budget **1 to 3 days** for a typical login flow, plus your own testing.

<Info>
  On web, most teams run [Autocapture](/corbado-observe/get-started/autocapture) as the foundation and add custom events only where a step needs guaranteed semantics. On iOS and Android, custom events are the integration. See [Integration paths](/corbado-observe/overview/integration-paths).
</Info>

<Tip>
  Prefer an AI coding agent? See [Use with AI agents](/corbado-observe/get-started/use-with-ai-agents) and let it instrument the integration for you.
</Tip>

## 1. Prerequisites

* An active project in the [Corbado management console](https://app.corbado.com)
* Your `Project ID` and `API Base URL` from [settings](https://app.corbado.com/observe/settings/general)

## 2. Install the SDK

<Tabs>
  <Tab title="Web (npm)">
    ```bash theme={null}
    npm install @corbado/observe
    ```
  </Tab>

  <Tab title="Web (CDN)">
    Place the snippet right before your closing `</head>` tag. It loads the SDK and initializes it in one block.

    <Warning>
      Enter your `ProjectID` and `APIBaseURL` as provided in the [management console](https://app.corbado.com/observe/settings/general).
    </Warning>

    <Accordion title="Show the CDN snippet">
      ```html theme={null}
        <script>
        (function(O,e){e.Corbado=e.Corbado||{};var C="https://cdn.cloud.corbado.io/observe/sdk-latest.min.js",c,l,f,u,p,i;e.Corbado.__SV||(e.Corbado.__SV=1,e.Corbado.IQ=[],e.Corbado.MQ=[],e.Corbado.OQ=[],e.Corbado.init=function(h,d){e.Corbado.IQ.push([h,d||"default"])},e.Corbado.get=function(h){var d=h||"default",g={};c="flowStarted flowDecided flowFinished flowReset flowAutoFinished authMethodsDecisionStarted authMethodsDecisionFinished authDecisionStarted authDecisionFinished resetSession".split(" "),l="provideIdentifierOperationFull provideDataOperationFull passkeyLoginFullOperation passkeyEnrollmentFullOperation passwordLoginFullOperation passwordEnrollmentFullOperation emailLinkOperationFull emailOtpOperationFull smsOtpOperationFull socialLoginOperationFull appConfirmationOperationFull".split(" "),f="trigger subflowStart destroy clientValidation.start clientValidation.finished clientValidation.error retry.start retry.finished retry.error getOptions.start getOptions.finished getOptions.error ceremony.start ceremony.finished ceremony.error ceremony.errorTyped postResponse.start postResponse.finished postResponse.error postResponse.errorTyped send.start send.finished send.error resend.start resend.finished resend.error getRedirectUrl.start getRedirectUrl.finished getRedirectUrl.error exchangeCode.start exchangeCode.finished exchangeCode.error cui.trigger cui.getOptions.start cui.getOptions.finished cui.getOptions.error cui.ceremony.start cui.ceremony.finished cui.ceremony.error cui.ceremony.errorTyped cui.postResponse.start cui.postResponse.finished cui.postResponse.error provideIdentifier.trigger provideIdentifier.clientValidation.start provideIdentifier.clientValidation.finished provideIdentifier.clientValidation.error provideIdentifier.postResponse.start provideIdentifier.postResponse.finished provideIdentifier.postResponse.error".split(" ");function v(r,t,o){typeof e.Corbado.__dispatchMethod=="function"?e.Corbado.__dispatchMethod(d,r,t,o):e.Corbado.MQ.push([d,r,t,o])}function b(r,t,o){typeof e.Corbado.__dispatchOp=="function"?e.Corbado.__dispatchOp(r,t,o):e.Corbado.OQ.push([r,t,o])}function m(r){var t={},o,y,n,s,a;for(o=0;o<f.length;o++){for(y=f[o],n=y.split("."),s=t,a=0;a<n.length-1;a++)s[n[a]]||(s[n[a]]={}),s=s[n[a]];s[n[n.length-1]]=(function(F){return function(){b(r,F,Array.prototype.slice.call(arguments,0))}})(y)}return t}for(i=0;i<c.length;i++)(function(r){g[r]=function(){v(r,Array.prototype.slice.call(arguments,0))}})(c[i]);for(i=0;i<l.length;i++)(function(r){g[r]=function(){var t=String(Date.now())+"-"+Math.random().toString(36).slice(2);return v(r,Array.prototype.slice.call(arguments,0),t),m(t)}})(l[i]);return g},p=O.createElement("script"),p.type="text/javascript",p.async=!0,p.src=C,u=O.getElementsByTagName("script")[0],u.parentNode.insertBefore(p,u))})(document,window);
        Corbado.init({projectId: "<ProjectID>", apiBaseUrl: "<APIBaseURL>"});
        </script>
      ```
    </Accordion>
  </Tab>

  <Tab title="iOS (Swift)">
    Corbado Observe ships a native Swift SDK that sends into the same data model as the web SDK.

    <Note>
      [Contact us](mailto:support@corbado.com) for the package coordinates and the iOS integration guide.
    </Note>
  </Tab>

  <Tab title="Android (Kotlin)">
    Corbado Observe ships a native Kotlin SDK, published to Maven Central as `com.corbado:observe` (minSdk 23), that sends into the same data model as the web SDK.

    <Note>
      [Contact us](mailto:support@corbado.com) for the Android integration guide.
    </Note>
  </Tab>
</Tabs>

## 3. Initialize the tracker

Initialize as early as possible in your app startup.

<Tabs>
  <Tab title="Web (npm)">
    ```typescript theme={null}
    import { init } from "@corbado/observe";

    init({
      projectId: "<ProjectID>",
      apiBaseUrl: "<APIBaseURL>",
    });
    ```
  </Tab>

  <Tab title="Web (CDN)">
    Initialization is already included in the snippet above.
  </Tab>
</Tabs>

### Multiple projects on web

Persisted state and cross-tab locks are scoped by `projectId`. Keep a tracker for each project and send events through the intended instance:

```typescript theme={null}
import { CorbadoTracker } from "@corbado/observe";

const projectA = new CorbadoTracker({
  projectId: "<Project A ID>",
  apiBaseUrl: "<Project A API Base URL>",
});
const projectB = new CorbadoTracker({
  projectId: "<Project B ID>",
  apiBaseUrl: "<Project B API Base URL>",
});

projectA.flowStarted({ flowName: "login" });
projectB.flowStarted({ flowName: "login" });
```

Each `init()` call also returns a tracker, but replaces the default used by package-level helpers such as `getTracker()`, `resetSession()` and `destroy()`. Constructing `CorbadoTracker` directly does not replace that default. Call methods on your retained instances and use `await projectA.destroy()` when that instance is no longer needed.

With the CDN snippet, assign distinct names using `Corbado.init(options, "projectA")` and `Corbado.init(options, "projectB")`. Call `Corbado.get("projectA")` or `Corbado.get("projectB")` for each event. Instances with the same project ID share persisted state within the browser's storage boundaries; names alone do not isolate storage.

### Migrating existing project storage

When upgrading from an SDK that uses global storage, initialize your existing project first. The SDK copies legacy global storage into that project's storage before loading configuration, resolving the session or recovering queued events. It preserves valid existing session IDs, sequence counters, experiment assignments, device identity and pending events; other projects start independently. Existing project-scoped state is preserved.

Each tab migrates its own session storage when the original project initializes there. Migration is a one-time snapshot: legacy keys remain, but later writes from already-open pages running an older SDK are not imported again. Reload those pages to complete the upgrade. See [session lifetime and storage](/corbado-observe/overview/constraints#7-storage-and-consent) and verify that each project's events arrive separately.

## 4. Track your first event

When your login UI is rendered and visible, send `flowStarted`:

<Tabs>
  <Tab title="Web (npm)">
    ```typescript theme={null}
    import { getTracker } from "@corbado/observe";

    getTracker().flowStarted({
      flowName: "login",
      touchpoint: "account",
    });
    ```
  </Tab>

  <Tab title="Web (CDN)">
    ```html theme={null}
    <script>
      Corbado.get().flowStarted({
        flowName: "login",
        touchpoint: "account",
      });
    </script>
    ```

    <Warning>
      Call `Corbado.get()` for each event instead of storing the instance in a variable. This keeps SDK loading asynchronous without losing events.
    </Warning>
  </Tab>
</Tabs>

<Tip>
  Use `touchpoint` consistently, for example `account`, `checkout` or `modal`, so you can compare funnel metrics across entry points.
</Tip>

## 5. Verify

Set `debug: true` in your `init()` call while developing and Observe logs every tracked event to the console:

```typescript theme={null}
init({
  projectId: "<ProjectID>",
  apiBaseUrl: "<APIBaseURL>",
  debug: true,
});
```

Set `debug: false` in production. For the full verification workflow, including the [Corbado Observe Debugger](/corbado-observe/tools/devtools-extension) extension for Chrome DevTools, see [Verify your integration](/corbado-observe/get-started/verify).

## 6. Next steps

<CardGroup cols={2}>
  <Card title="Model your journeys" icon="sitemap" href="/corbado-observe/tracking/modeling">
    Map your flows, decisions and subflows before writing tracking code.
  </Card>

  <Card title="Tracking reference" icon="diagram-project" href="/corbado-observe/tracking/overview">
    Flows, decisions, subflows, users and tags.
  </Card>

  <Card title="Verify your integration" icon="circle-check" href="/corbado-observe/get-started/verify">
    Confirm events arrive before you ship.
  </Card>
</CardGroup>
