> Markdown version of https://authpi.com/docs/quickstarts/multi-tenant/ — fetch the complete AuthPI docs index at https://authpi.com/llms.txt to discover all available pages.

# Multi-tenant identity in 15 minutes

The golden path — create an issuer, configure a client, create an organization, authenticate an org-scoped credential, and read the identity event trail.

## Overview

This is the canonical AuthPI setup: the shortest end-to-end path from an empty project to a working multi-tenant identity model. You will hit five milestones, in order:

1. **Issuer exists** — the per-tenant OIDC authority your identity model lives under
2. **Client configured** — the application your users will sign in through
3. **Organization created** — your first tenant
4. **User or credential authenticated** — an organization-scoped API key verified against AuthPI
5. **Identity event received** — the event trail your systems can react to

Everything runs from a terminal — no browser required. At the end, your first tenant holds a user and a scoped credential, and every step you took is visible as an event. Interactive user login builds on top of this and is covered by the framework quickstarts ([Next.js](/docs/quickstarts/nextjs/), [Hono](/docs/quickstarts/hono/)).

### Prerequisites

- An AuthPI account and an account **API key** (created during [console onboarding](https://console.authpi.com) — the secret is shown once, at creation)
- Node.js 20+ or Python 3.11+

Set your credentials as environment variables:

```bash
export AUTHPI_KEY_ID=key_your_key_id
export AUTHPI_KEY_SECRET=your_key_secret
```

## Install the Admin SDK

**TypeScript:**
```bash
npm install @authpi/admin
```

**Python:**
```bash
pip install authpi-admin
```

Construct the client once; every step below reuses it. The SDK authenticates with your API key over HTTP Basic and resolves your account id automatically.

**TypeScript:**
```typescript
import { AuthPIAdmin } from "@authpi/admin";

const admin = new AuthPIAdmin({
  apiKey: { id: process.env.AUTHPI_KEY_ID!, secret: process.env.AUTHPI_KEY_SECRET! },
});
```

**Python:**
```python
import os
from authpi_admin import AuthPIAdmin

admin = AuthPIAdmin(
    api_key=(os.environ["AUTHPI_KEY_ID"], os.environ["AUTHPI_KEY_SECRET"]),
)
```

## 1. Create your issuer

The issuer is the root of your identity model: users, organizations, clients, and credentials all live under it, and it runs a standards-compliant OIDC endpoint at `https://idp.authpi.com/{issuer_id}`.

**TypeScript:**
```typescript
const issuer = await admin.issuers.create({ name: "acme-prod" });

console.log(issuer.id); // i_...
```

**Python:**
```python
issuer = await admin.issuers.create({"name": "acme-prod"})

print(issuer.id)  # i_...
```

Verify the OIDC runtime is live — this endpoint is what standard OIDC libraries will discover:

```bash
curl https://idp.authpi.com/{issuer_id}/.well-known/openid-configuration
```

**Milestone: issuer exists.**

## 2. Configure a client

A client is an application allowed to authenticate against your issuer — your web app, your CLI, your mobile app. Register one with the authorization-code flow and a local redirect URI; when you wire up interactive login later, this is the client you'll use.

**TypeScript:**
```typescript
const client = await admin.issuer(issuer.id).clients.create({
  name: "acme-web",
  type: "external",
  confidential: true,
  settings: {
    protocol: "oidc",
    scopes: ["openid", "profile"],
    openid: {
      application_type: "web",
      grant_types: ["authorization_code", "refresh_token"],
      redirect_uris: ["http://localhost:3000/api/auth/callback"],
    },
  },
});

console.log(client.id); // c_...
// client.secret is returned only on this create response.
```

**Python:**
```python
client = await admin.issuer(issuer.id).clients.create({
    "name": "acme-web",
    "type": "external",
    "confidential": True,
    "settings": {
        "protocol": "oidc",
        "scopes": ["openid", "profile"],
        "openid": {
            "application_type": "web",
            "grant_types": ["authorization_code", "refresh_token"],
            "redirect_uris": ["http://localhost:3000/api/auth/callback"],
        },
    },
})

print(client.id)  # c_...
# client.secret is returned only on this create response.
```

**Milestone: client configured.**

## 3. Create an organization

Organizations are your tenants. Each one is a boundary that holds members, groups, invitations, SSO configuration — and its own credentials. This is the part of the model that makes an API multi-tenant rather than multi-user.

**TypeScript:**
```typescript
const org = await admin.issuer(issuer.id).organizations.create({
  name: "Globex Corp",
});

console.log(org.id); // org_...
```

**Python:**
```python
org = await admin.issuer(issuer.id).organizations.create({
    "name": "Globex Corp",
})

print(org.id)  # org_...
```

**Milestone: organization created.**

## 4. Add a user with scoped membership

Create a user in the issuer, then make them a member of the organization. Membership carries **scopes**, not roles: `resource:action` grants that your API evaluates directly. Authorization is deny-by-default, so a membership must hold at least one scope (or group).

**TypeScript:**
```typescript
const user = await admin.issuer(issuer.id).users.create({
  username_type: "email",
  username: "ada@globex.example",
  profile: { first_name: "Ada", last_name: "Lovelace" },
});

await admin.issuer(issuer.id).organization(org.id).members.create({
  member_id: user.id,
  scopes: ["invoices:read", "invoices:write"],
});
```

**Python:**
```python
user = await admin.issuer(issuer.id).users.create({
    "username_type": "email",
    "username": "ada@globex.example",
    "profile": {"first_name": "Ada", "last_name": "Lovelace"},
})

await admin.issuer(issuer.id).organization(org.id).members.create({
    "member_id": user.id,
    "scopes": ["invoices:read", "invoices:write"],
})
```

When Ada signs in through the client from step 2, her tokens carry these organization scopes. In production you'd let organizations grow themselves with [invitations](/docs/guides/invitations/) instead of creating members by hand.

## 5. Mint and verify an organization API key

Organizations hold credentials of their own. An organization API key lets Globex's backend call **your** API without a user session — this is how tenants integrate machine-to-machine. Mint one, scoped to exactly what it needs:

**TypeScript:**
```typescript
const key = await admin.issuer(issuer.id).organization(org.id).apiKeys.create({
  type: "api_key",
  name: "globex-backend",
  restrictions: { scopes: ["invoices:read"] },
});

// secret_plain is present only on this create response — store it now.
console.log(key.id, key.secret_plain);
```

**Python:**
```python
key = await admin.issuer(issuer.id).organization(org.id).api_keys.create({
    "type": "api_key",
    "name": "globex-backend",
    "restrictions": {"scopes": ["invoices:read"]},
})

# secret_plain is present only on this create response — store it now.
print(key.id, key.secret_plain)
```

Now verify it the way your API will in production: present the key to `GET /v1/me` and read back who it is and what it may do — the equivalent of AWS STS `GetCallerIdentity`. Your API's job reduces to one call: forward the credential, then enforce the returned scopes.

**TypeScript:**
```typescript
import { AuthPIAdmin } from "@authpi/admin";

// In your API: construct a client from the credential the caller presented.
const who = await new AuthPIAdmin({
  apiKey: { id: key.id, secret: key.secret_plain },
}).whoami();

// who.type === "api_key" → the credential is valid
// who.organizations[0].org_id → which tenant is calling (org_...)
// who.organizations[0].scopes → what it may do (["invoices:read"])
```

**Python:**
```python
from authpi_admin import AuthPIAdmin

# In your API: construct a client from the credential the caller presented.
async with AuthPIAdmin(api_key=(key.id, key.secret_plain)) as caller:
    who = await caller.whoami()

# who["type"] == "api_key" → the credential is valid
# who["organizations"][0]["org_id"] → which tenant is calling (org_...)
# who["organizations"][0]["scopes"] → what it may do (["invoices:read"])
```

**cURL:**
```bash
curl https://api.authpi.com/v1/me \
  -u "key_the_org_key_id:the_org_key_secret"

# → { "data": { "type": "api_key", "organizations": [ { "org_id": "org_...", "scopes": ["invoices:read"] } ] } }
```

A `401` means the credential is invalid, expired, blocked, or revoked; a valid response tells you the organization and its scopes. Deny-by-default applies: a key with no matching scope should get a `403` from your API.

**Milestone: user or credential authenticated.**

## 6. Read the identity event trail

Every mutation you just made — and the verification you just performed — emitted an event. List them:

**TypeScript:**
```typescript
const events = await admin.events.list({ limit: 20 });

for (const event of events.data) {
  console.log(event.type);
}
// api-key.verified
// api-key.created
// organization.membership.created
// user.created
// organization.created
// client.created
// issuer.created
```

**Python:**
```python
page = await admin.events.list(limit=20)

for event in page.data:
    print(event.type)
# api-key.verified
# api-key.created
# organization.membership.created
# user.created
# organization.created
# client.created
# issuer.created
```

The trail is the point: identity changes in AuthPI produce events your systems can react to, not state you have to poll for. In production, subscribe to lifecycle events with [webhooks](/docs/guides/webhooks/) and AuthPI pushes signed payloads to your endpoint as they happen. (Per-request events like `api-key.verified` are metered and intentionally never fan out to webhooks — query those here.)

**Milestone: identity event received.**

## What you built

One issuer running a live OIDC endpoint. One client ready for interactive login. One tenant organization holding a member with `invoices:*` scopes and a machine credential scoped to `invoices:read` — verified with a single API call. And an event trail recording all of it.

## Next steps

- **Interactive login** — wire the client from step 2 into your app: [Next.js](/docs/quickstarts/nextjs/), [Hono](/docs/quickstarts/hono/), or a [plain TypeScript backend](/docs/quickstarts/typescript-backend/)
- **Validate user tokens** in your API with JWKS: [Validate tokens](/docs/guides/validate-tokens/)
- **Let tenants grow themselves** with [invitations](/docs/guides/invitations/) and [organization lifecycle](/docs/guides/org-lifecycle/)
- **Push, don't poll** — subscribe to identity events with [webhooks](/docs/guides/webhooks/)
- **Give AI agents an identity** with short-lived attested tokens: [Agents quickstart](/docs/quickstarts/agents/)