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

# Sign in with PeridotID

> Let users sign into your app with their PeridotID (Google or passkey)

Add "Sign in with PeridotID" to any web app in three steps. Your users authenticate
against PeridotID production — no local setup, no secrets to manage.

## 1. Register your app

Call `POST /v1/apps` with your PeridotID session (cookie) to get a public `client_id`.
List every URL PeridotID may return users to — codes are only issued to these:

```ts theme={null}
import { Peridot } from '@peridotvault/pid-sdk-js';

const peridot = Peridot({ clientId: 'pidapp_…' });
// Register with just a name — allowed origins are managed after, in the dashboard
// (local dev: http://localhost:3303) (or PATCH /v1/apps).
const res = await peridot.post('/v1/apps', { name: 'My Game' });
// res.data.clientId → "pidapp_..." — pass it as client_id in step 2
```

Then add your websites under **Allowed origins** (bare `https://…` origins, no paths).
Pass the exact return URL in code — its origin must be listed:

Your redirect URI origins are automatically allowed for browser API calls (within
\~60s of registering, no redeploy needed). Local development needs nothing at all:
`http://localhost`, `http://127.0.0.1` and `http://[::1]` (any port, any path) are
always allowed — arbitrary production hosts are never accepted without registration
(an open redirect would let anyone steal users' login codes).

## 2. Open the login popup

```bash theme={null}
npm install @peridotvault/pid-sdk-js
```

```ts theme={null}
import { Peridot } from '@peridotvault/pid-sdk-js';

const peridot = Peridot({ clientId: 'pidapp_…' }); // environment auto-detected

async function signIn() {
  const { pidCode } = await peridot.auth.loginPopup(); // opens a popup (tab fallback)
  if (pidCode) {
    const identity = await peridot.auth.exchange(pidCode);
    // { pid, profile: { displayName, avatarUrl }, credentials: [{ provider, email }] }
  }
}
```

Auth opens a centered **popup** on the PeridotID origin (Google, plus the PID
picker/claim if the visitor has no PeridotID yet). It stays open through the
whole flow and closes only once the user allows (your page receives the
one-time `pid_code` via `window.opener`) or denies/cancels. If the popup is
blocked, the SDK falls back to a new tab automatically; `peridot.auth.loginTab()`
is available if you prefer a tab outright. You never handle a password and
nothing runs in your DOM. Passkeys run on the PeridotID origin (browsers bind
WebAuthn to our domain); Google uses the redirect round-trip. No API calls happen
before login, so there is nothing CORS-related to configure.

> **First sign-in creates a PID.** If the visitor has no PeridotID yet, the
> window shows the emission-free **Create your PID** step (the handle is permanent
> and user-chosen). Login only resolves once a PID exists and the user allows.
> `popupBaseUrl` is reused as the auth host; approval **popups** (a smaller window
> on the PeridotID origin) are used for confirmations like transfers and payments.

In React, wrap the same two calls in your own button + hook — there is no
provider component to install.

## Who owns what: auth vs session

**We authenticate the human, you sessionize your user.** PeridotID proves
"this browser is `rani@pid`" and hands you proof (`pid_code` → exchange →
`{ pid, profile, accessToken }`). Everything session-shaped is yours: your
cookie, your expiry, your logout route, your users table keyed by `pid`.
Our session cookies (`pid_access`/`pid_refresh`) never leave our origin — no
third-party app can or should read them.

One rule covers most mistakes: **keep the `accessToken` server-side.** It is a
read-only bearer (identity, profile, balance, ledger — never writes), and
anything in browser JS or `localStorage` is stealable via XSS. Exchange in the
browser while prototyping if you like; production backends exchange
server-side (step 3) and hand the browser only their own `httpOnly` cookie.

## 3. Handle the login

After login the tab returns a one-time `pid_code` (posted to your opener; a
full-page `redirect_uri` fallback is also supported). Already signed in? A
consent screen appears first — who you're continuing as, with Allow, a
different account, or Deny — and only Allow issues the code. Denying returns
with `?error=access_denied`.

For production, exchange server-side instead of calling
`peridot.auth.exchange` in the browser: verify the code against PeridotID,
store the bearer server-side, then mint your app's own session:

```ts theme={null}
// your backend: verify the code against PeridotID, then create your session
const r = await fetch('https://api.pid.peridotvault.com/v1/auth/exchange', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ code, clientId: 'pidapp_...' }),
});
const identity = await r.json(); // single-use code, 5-minute expiry
// identity.accessToken → keep server-side, send as Authorization: Bearer
// for reads (GET /v1/identity/me, /v1/fiat/balance, …). Set your own
// httpOnly cookie for the browser — that cookie is your session, not ours.
```

Know the limits before promising anything to your users:

* The bearer is **read-only** (`scope: "read"`): balance/ledger/identity/profile
  are reachable, transfers and writes always return 403.
* It lasts **30 days, non-refreshable** — afterwards the user re-clicks login
  (one tap if their PeridotID session is alive). Never log the bearer.
* There is **no single-token revoke**: a leaked bearer stays valid until expiry
  unless the whole identity is deactivated. The read-only scope is the
  containment — the reason this pattern is blessed for reads only.
* **Verify-or-cache is your call**: live-verify every request (fail-closed when
  we're down, deactivation honored immediately) or cache identity up to \~15 min
  per route — never longer.
* **Logout doesn't propagate**: our sign-out ends our session, not yours —
  delete your own cookie server-side on your logout route.
* **Don't confuse it with the machine token**: the user bearer reads *their*
  data; `POST /v1/auth/token` reads *your app's* data (escrow balance).

## Notes

* Passkey login always runs on a PeridotID-hosted page (browsers bind WebAuthn to our
  domain) and returns with `pid_code` like Google does.
* Codes are single-use, expire in \~5 minutes, and are bound to your `client_id`.
* No framework? The snippet in step 2 is already framework-free: `loginPopup`
  (popup, tab fallback) or `openLoginTab`/`openLoginPopup`, plus
  `loginWithPasskey({ clientId, returnTo })` and `exchange(code, clientId)`.

## Verified partners

Admins can mark your app as a **verified partner** (in the dashboard). Verified
apps show a ✓ badge on the login consent screen — and your users skip the
PeridotID fiat fee on top-ups, sends, and withdrawals (0%). Your own app fee,
DOKU provider fees, and on-chain fees still apply. Interested? Talk to us —
verification is granted, never self-issued.

## Next: money

Once users can sign in, you can fund their balance, hold escrow on your app's
account, and charge your own fee:

* [Fiat & payments](/fiat) — top-up, balance, sends, escrow.
* [Apps & workspace](/apps) — manage your app, set per-app fees, read the
  escrow balance with a machine token.


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