> ## 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.

# JavaScript SDK

> The official PeridotID SDK for the browser

The SDK (`@peridotvault/pid-sdk-js`) wraps the API for browsers. It sends credentials with every
request, so the API's cookies are handled automatically.

## Install

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

## Initialize

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

const peridot = Peridot({
  clientId: 'pidapp_…', // your registered app id
  onUnauthorized: async () => {
    // Access token expired — try to refresh silently before redirecting to login
    const ok = await peridot.auth.refresh();
    if (!ok) {
      const url = await peridot.auth.login(); // Google OAuth URL — navigate to it
      if (url) window.location.assign(url);
    }
  },
});
```

That's the whole config. `clientId` is applied to login, exchange, and fiat calls
automatically. The environment is auto-detected: `NODE_ENV === 'production'` →
production, everything else (unset, `development`, `staging`, `test`, …) →
sandbox. The API origin, popup host, Solana RPC, and program id all resolve from
there — chains come from the API registry, not your config.

* Force an environment for a staging build with `env: 'sandbox'`.
* Self-host / localhost: pass advanced `baseUrl` (and `popupBaseUrl`).
* Server-side only: pass `clientSecret` (used by `auth.exchange`) — **never** in
  browser code.

## Sign in

`auth.login()` resolves the Google OAuth URL — navigate to it. The user signs in and is sent back to
`CLIENT_SUCCESS_URL` with the session cookies already set — or, for a new credential,
to the PID picker (`?claim=1`), where they choose the permanent `<handle>@pid`
(check `auth.pidAvailable(handle)` first; the handle can never be changed, reused,
or reassigned):

```ts theme={null}
const url = await peridot.auth.login();
if (url) window.location.assign(url);
const { available, pid } = await peridot.auth.pidAvailable("ifal");
// { available: true, pid: "ifal@pid" }
const status = await peridot.auth.claimStatus();
// { pending: true, email, displayName, avatarUrl } when a claim is waiting
const claimed = await peridot.auth.claim("ifal");
// { ok: true, pid: "ifal@pid", pidCode? } — session cookies set
```

## Identity

```ts theme={null}
const me = await peridot.identity.me();
// { id: "ifal@pid", status: "active", createdAt: "2026-08-03T13:30:00.000Z" }
```

### Login credentials

```ts theme={null}
const credentials = await peridot.identity.credentials();
// [{ id: "3f9c...", provider: "google", email: "a@b.com", linkedAt, lastLoginAt }]

await peridot.identity.unlinkCredential(credentials[0].id); // true if unlinked
```

Unlinking the identity's last credential fails (returns `false`) — every identity must
keep at least one way to log in.

## Profile

```ts theme={null}
const profile = await peridot.profile.me();
// { pid, displayName, avatarUrl, locale, createdAt, updatedAt }

const updated = await peridot.profile.update({
  displayName: 'PeridotPlayer',
  locale: 'en-US',
});
```

The PID (`pid`, e.g. `ifal@pid`) is permanent and immutable — only the mutable
labels above can be updated. There is no endpoint that changes, reuses, or reassigns it.

## Wallet

Each identity owns **one personal wallet** — its chain rows (Solana smart account

* EVM counterfactuals), all derived from the PID. Custody is **record-only**:
  Peridot never generates, holds, or returns any key material.

```ts theme={null}
const rows = await peridot.wallet.me();
// [{ id, chainNamespace, chainReference, address, accountType, status, ... }]
// or ApiError { statusCode: 404 } when the PID has no wallet yet

const created = await peridot.wallet.createAccount();
// idempotent ensure — same rows back on repeat calls
```

* Creating again returns the **existing** wallet — a PID can never have more than one.
* The wallet belongs to the PID, not to Google (or any other provider). Unlinking a
  credential leaves it untouched, and logging in from another device does not change it.
* `create` returns the API error object on validation failure (`400`).

## Money (fiat)

`peridot.fiat` covers top-up, balance, statement, and sends on the internal IDR
ledger. Full guide: [Fiat & payments](/fiat).

```ts theme={null}
const bal = await peridot.fiat.balance();          // { balanceIdr, source: 'fiat-ledger', ... }
const view = await peridot.fiat.ledger();          // statement rows
const deposit = await peridot.fiat.checkoutDeposit('100000'); // → { paymentUrl, grossIdr, feeIdr, netIdr }
await peridot.fiat.syncTransaction(deposit.id);    // corroborate payment → credits the ledger
await peridot.fiat.transferViaPopup({ amountIdr: '100000', beneficiaryPid: 'live2dev@pid' });
```

* `checkoutDeposit`, `transferViaPopup`, and `transferConfirm` are trust-critical:
  in popup mode they run on the PeridotID origin (see [Popup flow](#popup-flow-third-party-origins)).
* Fiat methods **throw** `Error` on failure (not the `T | ApiError` union that reads return).
* Pass the initiating app's `clientId` as the second argument to apply that app's
  own fee — it stacks on the global 0.1% (min Rp100, no cap) and credits the app.

## Apps, fees & raw calls

`PeridotClient` also exposes `get` / `post` / `patch` / `put` / `delete` for
endpoints without a dedicated wrapper — app management, per-app fees, and machine
auth use these:

```ts theme={null}
await peridot.get('/v1/apps');
await peridot.patch(`/v1/apps/${id}`, { allowedOrigins: ['https://mygame.dev'] });
await peridot.put(`/v1/apps/${id}/fees/topup`, {
  percentBps: 200, minIdr: '0', maxIdr: '10000', enabled: true,
});
```

See [Apps & workspace](/apps) for the machine token and the full fee model.

## Log out

```ts theme={null}
await peridot.auth.logout();
```

## Refreshing

Usually you never call `refresh()` directly — the `onUnauthorized` callback does it. If
you need to force a refresh:

```ts theme={null}
const ok = await peridot.auth.refresh(); // true if a new session was issued
```

## Errors

Methods return the API error object instead of throwing for failed requests:

```ts theme={null}
const result = await peridot.profile.me();
if ('statusCode' in result && result.statusCode === 401) {
  // handle unauthenticated
}
```

See [Errors](/errors) for the full error contract.


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