> Source: https://kaidn.io/docs/quickstart/angular
> Full documentation index: https://kaidn.io/llms.txt

Docs menuAll pages, quickstarts and this page’s contentsDocs
- [Introduction](https://kaidn.io/docs)
- [Quickstarts](https://kaidn.io/docs/quickstart)
- [Core concepts](https://kaidn.io/docs/concepts)
- [API reference](https://kaidn.io/docs/api)
- [Guides](https://kaidn.io/docs/guides)
- [Keys & dashboard](https://kaidn.io/docs/keys)
- [Glossary](https://kaidn.io/glossary)

Web
- [JavaScript](https://kaidn.io/docs/quickstart/javascript)
- [React](https://kaidn.io/docs/quickstart/react)
- [Next.js](https://kaidn.io/docs/quickstart/nextjs)
- [Preact](https://kaidn.io/docs/quickstart/preact)
- [Vue](https://kaidn.io/docs/quickstart/vue)
- [Nuxt](https://kaidn.io/docs/quickstart/nuxt)
- [Angular](https://kaidn.io/docs/quickstart/angular)
- [Svelte](https://kaidn.io/docs/quickstart/svelte)

Mobile
- Androidsoon
- iOSsoon
- React Nativesoon
- Fluttersoon

Server
- [Node.js](https://kaidn.io/docs#quickstart)
- [PHP](https://kaidn.io/docs/quickstart/php)
- C#/.NETsoon
- Gosoon
- Javasoon
- [Python](https://kaidn.io/docs/quickstart/python)

On this page
- [1. Get your publishable key](#step-1)
- [2. Set up your project](#step-2)
- [3. Provide Kaidn](#step-3)
- [4. Build the signup component](#step-4)
- [5. Collect on submit](#step-5)
- [6. Score it on your server](#step-6)
- [7. Test it](#step-7)
- [The rest of the service](#more)
- [Next steps](#next)

# Angular quickstart

Add Kaidn to an Angular app and give every visitor a stable device id you can send to your backend. The example is the one most people start with: stopping the same person opening account after account.

`@kaidn/angular` contains **no Angular decorators**, so there is no ng-packagr step and nothing for an Angular major to relink.

Estimated time: under 10 minutes

### Before you start

- **Node 20+ and npm.**
- **Angular 16 or later**, for signals. Standalone components are assumed; NgModule apps work the same way.
- **A free Kaidn account.** [Register](https://kaidn.io/register): 10,000 events a month, no card.

**This is the frontend half, and on its own it blocks nothing.** By the end you will have a device id. That only becomes fraud prevention when your server sends it to `/v1/score` and acts on the verdict, so finish with a backend quickstart: [Python](https://kaidn.io/docs/quickstart/python), [PHP](https://kaidn.io/docs/quickstart/php) or [Node.js](https://kaidn.io/docs#quickstart).

## 1. Get your publishable key

- [Create an account](https://kaidn.io/register) if you do not have one.
- Go to **Fraud Scoring API → Device trackers**, create a tracker, and list the domains it may run on. Include `localhost` while you build.
- Copy the **publishable key**. It starts `pk_live_`.

**This key is meant to be visible in your bundle.** It is domain-locked and can only send fingerprints: it cannot score, read your data, or work from a site you did not authorise. Your `kdn_live_` secret key is the opposite, and passing one to `provideKaidn()` throws where you wrote it rather than letting it ship to every visitor.

## 2. Set up your project

Skip to step 3 if you have a project already.

Terminal

```
npx @angular/cli new kaidn-angular-quickstart --standalone --style css
cd kaidn-angular-quickstart
npm install @kaidn/angular
```

## 3. Provide Kaidn

One line in your application config. It holds configuration and **collects nothing on its own**, so it is safe in an app whose routes mostly have no signup on them.

src/app/app.config.ts

```
import { ApplicationConfig } from "@angular/core";
import { provideKaidn } from "@kaidn/angular";

export const appConfig: ApplicationConfig = {
  providers: [
    provideKaidn({ publishableKey: "pk_live_your_key_here" }),
  ],
};
```

On an NgModule app the same call goes in `providers` on your `@NgModule`. `provideKaidn` returns a plain `Provider[]`, so both work without a second entry point.

#### Why there is no NgModule, and no @Injectable

You will notice reading the source that `KaidnService`carries no decorator. That is deliberate. An Angular decorator has to be processed by Angular's own compiler, which is why libraries that use them ship partial-Ivy output from `ng-packagr`and get relinked against each consumer's Angular version, and why such libraries break on majors.

This package is built from runtime API only: `InjectionToken`, `inject`, `signal`, and one factory provider. Plain `tsc` compiles it, there is nothing to relink, and no version of Angular can break the build. The whole cost is the `provideKaidn(…)` line above, which every other Angular library asks for anyway.

**Validation happens where you wrote it.** `provideKaidn()` checks the key when it is called, in your app config, rather than lazily on first injection three navigations later. A bad key gives you a stack trace pointing at the mistake.

## 4. Build the signup component

src/app/signup.component.ts

```
import { Component, signal } from "@angular/core";
import { FormsModule } from "@angular/forms";

@Component({
  selector: "app-signup",
  standalone: true,
  imports: [FormsModule],
  template: `
    <form class="wrap" (ngSubmit)="submit()">
      <h1>Create an account</h1>

      <label for="email">Email</label>
      <input id="email" name="email" type="email" required
             [(ngModel)]="email" placeholder="you@example.com" />

      <label for="password">Password</label>
      <input id="password" name="password" type="password" required
             [(ngModel)]="password" />

      <button type="submit" [disabled]="busy()">
        {{ busy() ? "Checking…" : "Create account" }}
      </button>
    </form>
  `,
  styles: [`
    .wrap {
      max-width: 380px; min-height: 100vh; margin: 0 auto;
      display: flex; flex-direction: column; justify-content: center; gap: .5rem;
      padding: 1rem; text-align: left;
    }
    input { padding: .6rem; border: 1px solid #ccc; border-radius: 6px; font: inherit; }
    label { font-size: .85rem; color: #555; }
    button {
      margin-top: .75rem; padding: .7rem 1.2rem; font: inherit; cursor: pointer;
      background: #111; color: #fff; border: 0; border-radius: 6px;
    }
    button:disabled { opacity: .6; cursor: not-allowed; }
  `],
})
export class SignupComponent {
  email = "";
  password = "";
  readonly busy = signal(false);

  submit() {
    // fills in next step
  }
}
```

## 5. Collect on submit

Inject the service and ask it for an id. It collects when you call it, not when it is constructed. That puts the work at the moment somebody acts, which is when the signal is freshest, and keeps fingerprinting off routes that do not need it.

src/app/signup.component.ts

```
import { Component, inject } from "@angular/core";
import { KaidnService } from "@kaidn/angular";

export class SignupComponent {
  readonly kaidn = inject(KaidnService);

  email = "";
  password = "";

  async submit() {
    // Never throws. A blocked script, an ad blocker, or server rendering all
    // resolve to null, and your signup carries on with one signal fewer.
    const deviceId = await this.kaidn.getDeviceId();

    await fetch("/api/signup", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        email: this.email,
        password: this.password,
        deviceId,          // your server scores THIS
      }),
    });
  }
}
```

Bind the template straight to the service, since the state is exposed as signals:

src/app/signup.component.ts

```
<button type="submit" [disabled]="kaidn.isLoading()">
  {{ kaidn.isLoading() ? "Checking…" : "Create account" }}
</button>
```

The service gives you four things:

- `getDeviceId()` collects and resolves to `string | null`. Never throws.
- `deviceId` is a read-only signal holding the last id, or null before the first call.
- `isLoading` is a read-only signal, true while a collection is in flight.
- `error` is a read-only signal holding whatever went wrong last, for your logging rather than your user.

They are read-only on purpose: a consumer that could `.set()` them would be writing a device id nothing collected.

Note what is *not* here: no verdict and no decision. The browser is an untrusted place to make one, so it never sees a score. It produces an id; your server produces the judgement.

**A double-clicked button fingerprints once, not twice.** The service keeps one collection in flight at a time, so an impatient visitor costs you one beacon rather than two.

## 6. Score it on your server

This is the step that turns a device id into fraud prevention. Everything before it collects; nothing before it decides.

Your `/api/signup` handler takes the id and passes it to `/v1/score`:

C#/.NET*soon*Go*soon*Java*soon*

npm install @kaidn/sdk

api/signup.js

```
import { Kaidn } from "@kaidn/sdk";

// Your SECRET key. Server only, never the browser.
const kaidn = new Kaidn({ apiKey: process.env.KAIDN_API_KEY });

// Express, Fastify, Hono, a Next.js route handler: the call is the same.
app.post("/api/signup", async (req, res) => {
  let r;
  try {
    r = await kaidn.score({
      event: "signup",
      ip: req.ip,
      email: req.body.email,
      device_id: req.body.kaidn_device_id,   // the id the browser collected
    });
  } catch {
    // FAIL OPEN. An outage in your fraud vendor must never become an
    // outage in your signup form.
    return res.json({ ok: true });
  }

  if (r.verdict === "block") {
    // Do not name the signal. It teaches the next attempt.
    return res.status(403).json({ error: "We could not create that account." });
  }

  const user = await createUser(req.body);
  if (r.verdict === "review") await flagForReview(user.id, r.event_id, r.reason_text);

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

Three answers, and **you** decide what each one means. Kaidn never blocks anybody on your behalf.

## 7. Test it

Terminal

```
ng serve
```

Open `http://localhost:4200`, fill the form, submit, and look at the network tab. Your `/api/signup` request carries the id:

Output

```
{ "email": "you@example.com", "deviceId": "8f1c2ae9d4b7c3e05a1f6b28d9074e3c" }
```

**Turn ad blockers off on localhost while you test.** Any fingerprinting script can be blocked by an extension, and when that happens you get nothing rather than a cautious answer. We measured what that does to a competitor and [published the numbers](https://kaidn.io/blog/fingerprinting-blocked-agent). That is why it resolves to null instead of throwing, and why the device id is one signal in a score rather than the whole thing.

## The rest of the service

#### watchSession: catch what one fingerprint cannot

`getDeviceId`answers "who is this" at one instant. Some of the most useful signals only exist across *time*, because they are a change rather than a value: a VPN that drops mid-session and leaks the real home IP, a session that starts masking partway through, one device seen from a dozen addresses. None of those can be read from a single page load.

src/app/account-layout.component.ts

```
import { Component, DestroyRef, OnInit, inject } from "@angular/core";
import { KaidnService } from "@kaidn/angular";

@Component({
  selector: "app-account-layout",
  standalone: true,
  template: `<router-outlet />`,
})
export class AccountLayoutComponent implements OnInit {
  private readonly kaidn = inject(KaidnService);
  private readonly destroyRef = inject(DestroyRef);

  ngOnInit() {
    // Re-beacons the same device about once a minute while the tab is open,
    // so a connection change becomes visible.
    this.kaidn.watchSession({ destroyRef: this.destroyRef });
  }
}
```

The `DestroyRef` is asked for explicitly rather than injected inside the service, because `inject()` only works in an injection context and `ngOnInit` is not one. A method that silently required a context you could not see would fail at runtime complaining about the wrong thing. If you would rather not pass it, `watchSession()` returns a handle with a `stop()` you can call yourself.

**It costs nothing.** The `/v1/fp` beacon is free and rate-limited; you are billed per scored decision. Watching a session adds signal without adding events, which is why it belongs on a logged-in layout rather than being rationed.

#### Server rendering

`getDeviceId()` resolves to null when there is no `window`, so the same call is safe in a component that renders under Angular SSR and in the browser. There is no `isPlatformBrowser` check for you to remember.

#### Consent

One flag stops everything collecting:

src/app/app.config.ts

```
provideKaidn({ publishableKey: "pk_live_…", enabled: hasConsent })
```

With `enabled: false`, `getDeviceId()` resolves to null and `watchSession()`hands back an inert handle rather than nothing, so no caller has to branch on whether it got one. Fingerprinting reads properties of a visitor's device, which in several jurisdictions needs a lawful basis *before* it happens rather than a note in a policy afterwards.

#### What this package will never do

One service is the whole surface, and that is a boundary rather than a gap. The rest of the API, `config`, `lists`, `label`, `forget`, `events`, `stats`, `batch`, is reachable only with your **secret key**, and a secret key in a browser bundle is a leak found by whoever reads the bundle first. Every one of them lives in the server clients: [full reference](https://kaidn.io/docs/api).

## Next steps

**Nothing is being scored yet.** Send that `deviceId` to your backend and pass it to `/v1/score`, which is where a verdict and a decision actually happen.

[Node.js backend@kaidn/sdk, the oldest and most complete client](https://kaidn.io/docs#quickstart)[Python backendFastAPI, six steps, the other half of this](https://kaidn.io/docs/quickstart/python)[PHP backendPlain PHP on any host, including shared](https://kaidn.io/docs/quickstart/php)[Device identityWhy a fingerprint is not a person, and the three rungs](https://kaidn.io/docs/concepts#identity)

Worth reading once the loop is closed: [the device token](https://kaidn.io/docs/concepts#device-token), which upgrades a guessed identity to a remembered one and is the single biggest accuracy win available to you.
