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

# Integration patterns

> Three complete integration recipes — SaaS app with per-user billing, CLI with device login, and metered billing setup. Pick the one that matches your architecture.

Every PymtHouse integration is some combination of three primitives: **machine auth** (your backend talks to PymtHouse), **user provisioning** (you register end-users), and **JWT issuance** (you get signed tokens that gate AI requests). What varies is how users get into the system and how you charge them.

## Setup: install the SDK and configure env vars

All TypeScript examples below use the official Builder SDK. Install it first:

```bash theme={null}
npm install @pymthouse/builder-sdk
# Node ≥ 20 required
```

Set these four environment variables in your backend. **Never expose the M2M credentials client-side.**

```bash theme={null}
PYMTHOUSE_ISSUER_URL=https://your-pymthouse.example/api/v1/oidc
PYMTHOUSE_PUBLIC_CLIENT_ID=app_…     # starts with app_
PYMTHOUSE_M2M_CLIENT_ID=m2m_…       # starts with m2m_
PYMTHOUSE_M2M_CLIENT_SECRET=pmth_cs_…

# Local dev only — set to 1 when issuer uses http://
PYMTHOUSE_ALLOW_INSECURE_HTTP=
```

<Tip>
  See the [Builder SDK reference](/integration/sdk) for the full API, subpath exports, and method signatures.
</Tip>

Then construct the client once per request lifecycle (server-side only):

```ts theme={null}
// lib/pymthouse.ts
import "server-only";
import { PmtHouseClient } from "@pymthouse/builder-sdk";

export function createClient() {
  return new PmtHouseClient({
    issuerUrl: process.env.PYMTHOUSE_ISSUER_URL!,
    publicClientId: process.env.PYMTHOUSE_PUBLIC_CLIENT_ID!,
    m2mClientId: process.env.PYMTHOUSE_M2M_CLIENT_ID!,
    m2mClientSecret: process.env.PYMTHOUSE_M2M_CLIENT_SECRET!,
    allowInsecureHttp: process.env.PYMTHOUSE_ALLOW_INSECURE_HTTP === "1",
  });
}
```

<Note>
  `createPmtHouseClientFromEnv` from `@pymthouse/builder-sdk/env` reads these same variables automatically but may not handle `PYMTHOUSE_ALLOW_INSECURE_HTTP`. Use the explicit constructor above for local development to ensure `allowInsecureHttp` is set correctly.
</Note>

***

Pick the pattern that matches your architecture:

* [Pattern 1: SaaS app — per-user sessions](#pattern-1-saas-app-per-user-sessions) — Users log into your web app; each user gets their own metered allowance.
* [Pattern 2: CLI tool — device flow](#pattern-2-cli-tool-device-flow) — Users authenticate your CLI with their browser; no password prompts.
* [Pattern 3: Metered billing — charge users](#pattern-3-metered-billing-charge-users) — Collect payment, set usage limits, and sell plan upgrades.

***

## Pattern 1: SaaS app — per-user sessions

**When to use:** Your app has logged-in users. You want to issue a fresh JWT for each user session and track usage per user for billing.

**What you'll have after:** Every API request carries a user-scoped JWT. Usage is attributed to individual users. You can show per-user billing dashboards.

### Architecture

```
User logs in → your backend authenticates → POST /users (upsert) → POST /users/{id}/token → JWT → AI request
```

### Step 1: Mint a user JWT (lazy-provision pattern)

The most robust pattern — used by the reference dashboard — is to attempt the token mint first and only provision the user on a `404`:

```ts theme={null}
// lib/pymthouse.ts
import "server-only";
import { PmtHouseClient, PmtHouseError } from "@pymthouse/builder-sdk";

export function createClient() {
  return new PmtHouseClient({
    issuerUrl: process.env.PYMTHOUSE_ISSUER_URL!,
    publicClientId: process.env.PYMTHOUSE_PUBLIC_CLIENT_ID!,
    m2mClientId: process.env.PYMTHOUSE_M2M_CLIENT_ID!,
    m2mClientSecret: process.env.PYMTHOUSE_M2M_CLIENT_SECRET!,
    allowInsecureHttp: process.env.PYMTHOUSE_ALLOW_INSECURE_HTTP === "1",
  });
}

/**
 * Mint a user access JWT. Auto-provisions the user on first call.
 * scope defaults to "sign:job" server-side.
 */
export async function mintUserToken(externalUserId: string): Promise<string> {
  const client = createClient();
  try {
    const result = await client.mintUserAccessToken({ externalUserId });
    return result.access_token;
  } catch (err) {
    if (err instanceof PmtHouseError && err.status === 404 && err.code === "not_found") {
      // First time we've seen this user — provision and retry
      await client.upsertAppUser({ externalUserId });
      const result = await client.mintUserAccessToken({ externalUserId });
      return result.access_token;
    }
    throw err;
  }
}
```

The lazy pattern means you never need a separate sign-up hook — users are provisioned on their first request. If you prefer eager provisioning (e.g. to pre-allocate balance), call `upsertAppUser` explicitly on sign-up:

```ts theme={null}
// Eager provisioning (optional — e.g. on account creation)
await client.upsertAppUser({ externalUserId: userId });
// email and status are optional; PymtHouse accepts externalUserId alone
```

```bash theme={null}
# curl equivalent — eager provision
curl -sS -u "${M2M_ID}:${M2M_SECRET}" \
  -H "Content-Type: application/json" \
  -d "{\"externalUserId\": \"${USER_ID}\"}" \
  "${BASE_URL}/api/v1/apps/${PUBLIC_CLIENT_ID}/users"

# curl equivalent — mint JWT (scope defaults to sign:job)
curl -sS -u "${M2M_ID}:${M2M_SECRET}" \
  -H "Content-Type: application/json" \
  -d '{}' \
  "${BASE_URL}/api/v1/apps/${PUBLIC_CLIENT_ID}/users/${USER_ID}/token" \
  | jq -r '.access_token'
```

### Step 2: Gate requests on balance

Check the user's balance before allowing the request. This prevents over-billing and gives you a clean error surface.

```ts theme={null}
export async function assertUserHasBalance(userId: string) {
  const client = createClient();
  const balance = await client.getUsageBalance(userId);
  if (!balance.hasAccess) {
    throw new Error("Insufficient balance. Please upgrade your plan.");
  }
}
```

### Step 3: Show usage in your dashboard

```ts theme={null}
// Per-user usage breakdown for a specific user
const client = createClient();
const usage = await client.fetchUsageForExternalUser({
  externalUserId: "user-123",
  startDate: "2026-06-01T00:00:00.000Z",
  endDate: "2026-06-30T23:59:59.999Z",
});
console.log("Requests:", usage.requestCount);
console.log("Fee (wei):", usage.feeWei);

// App-wide breakdown across all users
const appUsage = await client.getUsage({ groupBy: "user" });
```

### Next.js example: API route

```ts theme={null}
// app/api/sign/route.ts
import "server-only";
import { PmtHouseError } from "@pymthouse/builder-sdk";
import { createClient, mintUserToken } from "@/lib/pymthouse";

export async function POST(req: Request) {
  const { userId } = await getSession(req); // your auth

  // Check balance before signing
  const client = createClient();
  const balance = await client.getUsageBalance(userId);
  if (!balance.hasAccess) {
    return Response.json({ error: "Insufficient balance" }, { status: 402 });
  }

  // Mint user JWT (auto-provisions on first call)
  const accessToken = await mintUserToken(userId);

  // Forward to AI service with the JWT
  const result = await callAiService(accessToken);
  return Response.json(result);
}
```

### Signer sessions (Option A — clearinghouse direct mint)

If your integration talks directly to the remote signer DMZ rather than a gateway, use `mintUserSignerToken` from `@pymthouse/builder-sdk/signer/server`. This mints a short-lived signer JWT and returns the user's balance in one call:

```ts theme={null}
import "server-only";
import { mintUserSignerToken, createSignerTokenManager } from "@pymthouse/builder-sdk/signer/server";

// One-shot mint (no caching)
const signerToken = await mintUserSignerToken({
  issuerUrl: process.env.PYMTHOUSE_ISSUER_URL!,
  m2mClientId: process.env.PYMTHOUSE_M2M_CLIENT_ID!,
  m2mClientSecret: process.env.PYMTHOUSE_M2M_CLIENT_SECRET!,
  externalUserId: "user-123",
  allowInsecureHttp: process.env.PYMTHOUSE_ALLOW_INSECURE_HTTP === "1",
});
// signerToken.jwt — use as Bearer token at the DMZ
// signerToken.balanceUsdMicros — current balance
// signerToken.expiresAt — Unix timestamp

// With TTL caching across requests (recommended for production)
const tokenManager = createSignerTokenManager({
  mint: async (publicClientId, externalUserId) =>
    mintUserSignerToken({
      issuerUrl: process.env.PYMTHOUSE_ISSUER_URL!,
      m2mClientId: process.env.PYMTHOUSE_M2M_CLIENT_ID!,
      m2mClientSecret: process.env.PYMTHOUSE_M2M_CLIENT_SECRET!,
      externalUserId,
      allowInsecureHttp: process.env.PYMTHOUSE_ALLOW_INSECURE_HTTP === "1",
    }),
});
```

See [Signer routing](/integration/signer-routing) for the DMZ URL, webhook config, and identity webhook setup.

***

## Pattern 2: CLI tool — device flow

**When to use:** You're building a CLI, daemon, or any tool that runs in a terminal. Users should authenticate with their browser, not type in API keys.

**What you'll have after:** Users run `my-cli login`, open a URL in their browser, and the CLI receives a signed session token without ever touching credentials.

### Architecture

```
CLI starts device flow → user opens URL → user logs in at your site → your backend mints JWT + binds grant → CLI polls and receives token
```

### Step 1: CLI requests a device code

```bash theme={null}
RESPONSE=$(curl -sS \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=${PUBLIC_CLIENT_ID}" \
  -d "scope=openid" \
  "${BASE_URL}/api/v1/oidc/device/auth")

DEVICE_CODE=$(echo $RESPONSE | jq -r '.device_code')
USER_CODE=$(echo $RESPONSE | jq -r '.user_code')
VERIFY_URL=$(echo $RESPONSE | jq -r '.verification_uri_complete')

echo ""
echo "Open this URL in your browser to log in:"
echo "  ${VERIFY_URL}"
echo ""
echo "Or go to ${BASE_URL}/oidc/device and enter: ${USER_CODE}"
```

### Step 2: CLI polls for the result

Start polling immediately and wait for the user to complete login:

```bash theme={null}
while true; do
  POLL=$(curl -sS \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
    -d "device_code=${DEVICE_CODE}" \
    -d "client_id=${PUBLIC_CLIENT_ID}" \
    "${BASE_URL}/api/v1/oidc/token")

  ERROR=$(echo $POLL | jq -r '.error // empty')

  if [ -z "$ERROR" ]; then
    ACCESS_TOKEN=$(echo $POLL | jq -r '.access_token')
    echo "✓ Logged in"
    break
  elif [ "$ERROR" = "authorization_pending" ]; then
    sleep 5
  elif [ "$ERROR" = "slow_down" ]; then
    sleep 10
  else
    echo "Login failed: $ERROR"
    exit 1
  fi
done
```

```ts theme={null}
// SDK equivalent (TypeScript CLI)
import { pollDeviceToken } from "@pymthouse/builder-sdk/device";

const token = await pollDeviceToken({
  tokenEndpoint: `${process.env.PYMTHOUSE_ISSUER_URL}/token`,
  deviceCode,
  clientId: process.env.PYMTHOUSE_PUBLIC_CLIENT_ID!,
  interval: 5,
});
```

### Step 3: Your backend binds the device grant (Option B)

When a user authenticates at your login page, your backend binds the pending device grant so the CLI poll receives the token. This is a two-step operation: mint a user JWT, then exchange it to bind the grant:

```ts theme={null}
// Your login callback handler (server-side)
import { PmtHouseClient } from "@pymthouse/builder-sdk";
import { createClient } from "@/lib/pymthouse"; // your factory from Setup above

export async function handleDeviceApproval(userId: string, userCode: string) {
  const client = createClient();

  // Step A: mint a user JWT for this user
  await client.upsertAppUser({ externalUserId: userId }); // ensure provisioned
  const { access_token: userJwt } = await client.mintUserAccessToken({ externalUserId: userId });

  // Step B: exchange the JWT to bind the pending device grant
  // approveDeviceLogin wraps the RFC 8693 token exchange
  await client.approveDeviceLogin({
    externalUserId: userId,
    userCode,                   // the ABCD-EFGH code the CLI received
    publicClientId: process.env.PYMTHOUSE_PUBLIC_CLIENT_ID!,
  });
  // The CLI's next poll will now return access_token
}
```

```bash theme={null}
# curl equivalent (two-step: mint JWT then bind grant)

# Step A: mint user JWT
USER_JWT=$(curl -sS -u "${M2M_ID}:${M2M_SECRET}" \
  -H "Content-Type: application/json" \
  -d '{ "scope": "sign:job" }' \
  "${BASE_URL}/api/v1/apps/${PUBLIC_CLIENT_ID}/users/${USER_ID}/token" \
  | jq -r '.access_token')

# Step B: bind the device grant
curl -sS -u "${M2M_ID}:${M2M_SECRET}" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
  --data-urlencode "subject_token=${USER_JWT}" \
  --data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
  --data-urlencode "resource=urn:pmth:device_code:ABCD-EFGH" \
  "${BASE_URL}/api/v1/oidc/token"
```

<Note>
  The `user_code` (e.g. `ABCD-EFGH`) is what the CLI receives and shows to the user. The `device_code` is a server-side opaque token — never display it. Pass `user_code` in the `resource` parameter when binding the grant.
</Note>

### Requirements checklist

Before this works end-to-end:

* [ ] Public client (`app_…`) has `device_code` grant enabled
* [ ] `device_third_party_initiate_login` enabled on the public client
* [ ] `initiate_login_uri` registered to your login handler's HTTPS endpoint
* [ ] M2M client has `users:token` or `device:approve` scope

***

## Pattern 3: Metered billing — charge users

**When to use:** You want to charge users based on their AI usage. You offer a free tier, paid plans, and want to collect payment via Stripe.

**What you'll have after:** Users auto-enroll in a free Starter plan. When they hit the limit, you can offer a paid plan and take payment via Stripe Connect.

### How billing works

```
User makes AI request → signed ticket emitted → Kafka → OpenMeter → usage meter updated
                                                                        ↓
                                                           balance decrements automatically
                                                                        ↓
                                        when plan limit reached: hasAccess = false
```

Billing is **async and metering-based** — you do not need to hook into each individual request. The signer records usage automatically.

### Step 1: Every new user gets a free Starter plan

No action needed. PymtHouse auto-subscribes new users to the Starter plan on first provision. The Starter allowance is `$5.00` by default.

To change the Starter allowance for all new users:

```bash theme={null}
curl -sS -X PUT \
  -b "${SESSION_COOKIE}" \
  -H "Content-Type: application/json" \
  -d '{"includedUsdMicros":"10000000"}' \
  "${BASE_URL}/api/v1/apps/${PUBLIC_CLIENT_ID}/starter-plan"
```

```ts theme={null}
// Check a user's current balance
const balance = await pmth.getUsageBalance("user-123");
// { hasAccess: true, balanceUsdMicros: "5000000", consumedUsdMicros: "0" }
```

### Step 2: Create a paid plan

Create a plan with a monthly subscription fee and included usage allowance. Done from the dashboard or via session-authenticated API.

```bash theme={null}
curl -sS -X POST \
  -b "${SESSION_COOKIE}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pro",
    "type": "subscription",
    "priceAmount": "49.00",
    "priceCurrency": "USD",
    "includedUsdMicros": "50000000",
    "billingCycle": "monthly",
    "status": "active"
  }' \
  "${BASE_URL}/api/v1/apps/${PUBLIC_CLIENT_ID}/plans"
```

Publishing with `status: active` syncs the plan to OpenMeter automatically.

### Step 3: Connect Stripe for payment collection

To collect payment from users, connect a Stripe account:

```bash theme={null}
# Start Stripe Connect flow (returns a hosted Stripe URL)
curl -sS -X POST \
  -b "${SESSION_COOKIE}" \
  -H "Content-Type: application/json" \
  "${BASE_URL}/api/v1/apps/${PUBLIC_CLIENT_ID}/billing/stripe/connect"
# → { "url": "https://connect.stripe.com/..." }
```

Complete the Stripe onboarding at the returned URL. Once `charges_enabled` and `details_submitted` are true, paid plan checkout is unlocked.

Point a Stripe webhook at `POST /webhooks/stripe` with `STRIPE_WEBHOOK_SECRET` to keep Connect status in sync automatically.

### Step 4: Let users upgrade and pay

Trigger a checkout session when a user wants to upgrade:

```bash theme={null}
curl -sS -X POST \
  -b "${SESSION_COOKIE}" \
  -H "Content-Type: application/json" \
  -d "{\"planId\": \"${PLAN_ID}\", \"externalUserId\": \"user-123\", \"successUrl\": \"https://yourapp.com/billing/success\"}" \
  "${BASE_URL}/api/v1/apps/${PUBLIC_CLIENT_ID}/billing/checkout"
# → { "checkoutUrl": "https://checkout.stripe.com/..." }
```

Redirect the user to `checkoutUrl`. After payment, their subscription in OpenMeter is updated automatically.

### Step 5: Manual credit top-ups

Grant additional balance outside of the plan subscription — useful for support credits, trials, or promotions:

```ts theme={null}
await pmth.grantUserAllowance("user-123", {
  amountUsdMicros: "5000000",  // $5.00
  source: "manual",
});
```

```bash theme={null}
curl -sS -X POST \
  -u "${M2M_ID}:${M2M_SECRET}" \
  -H "Content-Type: application/json" \
  -d '{"amountUsdMicros":"5000000","source":"manual"}' \
  "${BASE_URL}/api/v1/apps/${PUBLIC_CLIENT_ID}/users/user-123/allowances"
```

### Step 6: Show users their billing summary

Pull the full billing snapshot for your billing dashboard page:

```ts theme={null}
// App-level billing summary (your dashboard)
const billing = await fetch(`${BASE_URL}/api/v1/apps/${PUBLIC_CLIENT_ID}/billing`, {
  headers: { Authorization: `Basic ${btoa(`${M2M_ID}:${M2M_SECRET}`)}` }
}).then(r => r.json());

console.log("Total fee (USD):", Number(billing.cycle.usage.networkFeeUsdMicros) / 1e6);
console.log("Overage:", billing.cycle.overage.overageWei);
```

```bash theme={null}
curl -sS \
  -u "${M2M_ID}:${M2M_SECRET}" \
  "${BASE_URL}/api/v1/apps/${PUBLIC_CLIENT_ID}/billing" | jq '.cycle.usage'
```

### Activation gate modes

PymtHouse can automatically block new user provisioning or paid plan checkouts until Stripe Connect is ready. Set `ACTIVATION_GATE_MODE` in your environment:

| Mode              | Effect                                                      |
| ----------------- | ----------------------------------------------------------- |
| `off` (default)   | No blocking — expose activation status via API only         |
| `log`             | Log what would be blocked, but allow everything             |
| `enforce_revenue` | Block paid plan checkout/change without Connect             |
| `enforce`         | Also block new user provisioning when owner wallet is empty |

***

## Related guides

* [User management](/integration/user-management) — full CRUD for provisioned users
* [Allowances](/integration/allowances) — balance reads, top-ups, and grant history
* [Billing summary](/integration/billing) — response field reference and USD micro handling
* [Plans](/integration/plans) — plan creation, OpenMeter sync, and phase-out
* [Device flow](/integration/device-flow) — full device authorization reference
* [Token exchange](/integration/token-exchange) — RFC 8693 device binding and signer session exchange
