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

# Next.js

> Identify a visitor, send a Request ID with your application action and verify the result on your server.

Identify a visitor, send a Request ID with your application action and verify the result on your server. This guide uses the supported ShieldLabs package API, not a standalone generated client.

## Before you start

An existing Next.js 14, 15 or 16 application with React 18/19. Keep Private API Keys in Server Actions or route handlers only.

Register and verify the website domain in your [analytics dashboard](https://app.shieldlabs.ai/). Use credentials from the same domain and environment as the browser check. For a fresh account, start with [Quick Start](/quickstart).

## Get your credentials

Copy the Public Key for your domain from **Integration > API keys**. A public frontend environment variable may contain this key, but never a Private API Key or webhook signing secret.

## Install the SDK

```bash theme={null}
npm install @shieldlabs-ai/next @shieldlabs-ai/react @shieldlabs-ai/js @shieldlabs-ai/node
```

Use `yarn add` or `pnpm add` with the same package names if your project uses that package manager. Framework packages remain the application's responsibility.

## Add the integration

With the App Router: a provider in the root layout, a signup page whose form sends a `requestId`, a
Server Action that reads the verdict, a store for used request IDs, and a route handler for webhooks.
The imports use the `@/` alias that `create-next-app` sets up.

```tsx theme={null}
// app/layout.tsx (a Server Component: ShieldLabsProvider is a client component inside it)
import type { ReactNode } from 'react';
import { ShieldLabsProvider } from '@shieldlabs-ai/next';

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <ShieldLabsProvider publicKey={process.env.NEXT_PUBLIC_SHIELDLABS_PUBLIC_KEY!}>
          {children}
        </ShieldLabsProvider>
      </body>
    </html>
  );
}
```

```tsx theme={null}
// app/signup/page.tsx
import { SignupForm } from './signup-form';

export default function SignupPage() {
  return (
    <main>
      <h1>Create an account</h1>
      <SignupForm />
    </main>
  );
}
```

```tsx theme={null}
// app/signup/signup-form.tsx
'use client';

import { useState, type SyntheticEvent } from 'react';
import { useIdentify } from '@shieldlabs-ai/next';
import { signup } from './actions';

export function SignupForm() {
  const { identify, isLoading } = useIdentify();
  const [message, setMessage] = useState('');

  async function onSubmit(event: SyntheticEvent<HTMLFormElement>) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    // null when there is no identification (a content blocker, a timeout): the signup goes out
    // anyway, and the Server Action treats it as unverified.
    const result = await identify();
    if (result) formData.set('requestId', result.requestId);
    const { ok } = await signup(formData);
    setMessage(ok ? 'Account created.' : 'We could not create your account.');
  }

  return (
    <form onSubmit={onSubmit}>
      <input name="email" type="email" required />
      <button disabled={isLoading}>Sign up</button>
      <p role="status">{message}</p>
    </form>
  );
}
```

```ts theme={null}
// app/signup/actions.ts
'use server';

import { evaluateIdentification, getIdentification, type Identification } from '@shieldlabs-ai/next/server';
import { markRequestIdUsed } from '@/lib/request-ids';

export async function signup(formData: FormData): Promise<{ ok: boolean }> {
  const requestId = formData.get('requestId');
  let identification: Identification | null = null;
  if (typeof requestId === 'string' && requestId !== '') {
    try {
      // Waits until ShieldLabs has stored the verdict, for up to 10 seconds.
      identification = await getIdentification(requestId);
    } catch (error) {
      console.error('ShieldLabs verdict unavailable:', error); // a malformed ID, a wrong key, a network error
    }
  }

  // One identification authorizes one attempt: record its request ID first (the store is
  // asynchronous), then hand the answer to isReplay, which must answer synchronously.
  const firstUse = identification !== null && (await markRequestIdUsed(identification.request_id));

  // Refuses a missing, reused or stale identification, the rate-limit marker, missing device signals,
  // browser automation or disabled JavaScript, and the dangerous band.
  const verdict = evaluateIdentification(identification, { isReplay: () => !firstUse });
  if (!verdict.ok) return { ok: false };

  // Create the account here.
  return { ok: true };
}
```

```ts theme={null}
// lib/request-ids.ts
// Used request IDs, in the memory of one server process. In production, use a store that every
// instance shares: Redis (SET <request id> 1 NX EX 600) or a column with a unique index.
const used = new Map<string, number>();

/** Records a request ID. Resolves to true the first time, and to false for every repeat. */
export async function markRequestIdUsed(requestId: string): Promise<boolean> {
  const now = Date.now();
  for (const [id, forgetAt] of used) if (forgetAt <= now) used.delete(id);
  if (used.has(requestId)) return false;
  used.set(requestId, now + 10 * 60 * 1000); // twice the 5-minute freshness window
  return true;
}
```

```ts theme={null}
// app/api/shieldlabs/webhook/route.ts
import { createWebhookHandler } from '@shieldlabs-ai/next/server';

export const POST = createWebhookHandler({
  // Verified with SHIELDLABS_WEBHOOK_SECRET. webhook.ping is answered by the handler itself.
  onEvent(event) {
    if (event.event_type !== 'identification.scored') return;
    // Store the verdict here, keyed by request ID (an upsert), so a repeated delivery changes nothing.
    console.info('ShieldLabs verdict', event.data.request_id, event.data.risk_score);
  },
});
```

`identify()` from `useIdentify()` never rejects: it resolves the result, or `null` with the reason in
`error`. A second submit while the identification runs gets the same one, so a double click costs one
identification.

Add `https://<your domain>/api/shieldlabs/webhook` as a webhook endpoint in the analytics dashboard,
put its signing secret in `SHIELDLABS_WEBHOOK_SECRET` and press Verify: the handler answers the
`webhook.ping` with 200.

`getIdentification()` waits until the verdict is stored, which is usually 1 to 3 seconds after
`identify()`. Start the identification when the user begins the action to save that time
([Identify when the user begins the action ](https://github.com/ShieldLabs-ai/shieldlabs-next/blob/961629fb9f46034d6a49319ec5b736c37bc8f54e/README.md#identify-when-the-user-begins-the-action)).
[`examples/app-router`](https://github.com/ShieldLabs-ai/shieldlabs-next/tree/961629fb9f46034d6a49319ec5b736c37bc8f54e/examples/app-router) is a complete app built this way.

## Test the complete flow

1. Run the application on the registered HTTPS domain with its Public Key.
2. Trigger the protected form once and check that a Request ID is sent to your own backend.
3. Retrieve that same ID using a server SDK or locate it in the analytics dashboard.
4. Test a missing ID and an agent load failure: your backend must treat the action as unverified.

The `/api/signup` routes shown in examples belong to your application; they are not ShieldLabs API endpoints. Connect one of the [server quick starts](/api/sdks#server-packages) before testing the full action. A successful browser call does not prove scoring is complete.

## Troubleshooting

* No History row: confirm the registered domain, credential/environment match and that the browser remained open while collectors posted.
* Missing or pending verdict: scoring is asynchronous. The server helper waits within a bounded budget; handle a missing result and API errors explicitly.
* Authentication error: use the Private API Key for History, not a Public Key, Management Secret Key or MCP OAuth token.
* Invalid webhook signature: verify the original raw bytes with the endpoint's full signing secret, before trusting parsed JSON.

## Next steps

* [Runnable example](https://github.com/ShieldLabs-ai/shieldlabs-next/tree/961629fb9f46034d6a49319ec5b736c37bc8f54e/examples/app-router)
* [SDK reference and changelog](https://github.com/ShieldLabs-ai/shieldlabs-next)
* [Identification flow](/api/identification-flow)
* [Server API](/api/server-api)
* [Webhook setup](/setup/webhooks)
* [Content Security Policy](/setup/csp)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.