Use cases

Create an organization and invite a teammate

Create a tenant organization in your issuer and send its first member invitation with the AuthPI Admin SDK — the core of B2B multi-tenant onboarding.

Last updated 2026-07-14

Outcome

A new organization (one tenant) in your issuer, with a pending invitation sent to its first member. By the end you'll have the organization's id, an invitation your teammate can accept, and the call to confirm both exist.

Prerequisites

  • An AuthPI account with an Issuer configured — note its issuer_id.
  • An account API key with the scopes issuers.organizations:write and issuers.organizations.invitations:write.
  • The Admin SDK installed:
npm install @authpi/admin
pip install authpi-admin
# No install needed — set your key as environment variables.
export AUTHPI_KEY_ID=key_...
export AUTHPI_KEY_SECRET=...

Implementation

Create the organization, then create an invitation scoped to it. The Admin SDK resolves your account id from the API key, so you only supply the issuer_id.

import { AuthPIAdmin } from "@authpi/admin";

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

const issuer = admin.issuer("iss_your_issuer_id");

// 1. Create the tenant organization
const org = await issuer.organizations.create({
  name: "Acme, Inc.",
});

// 2. Invite its first member
const invitation = await issuer.organization(org.id).invitations.create({
  email_invited: "founder@acme.example",
  scopes: ["member"],
});

console.log(org.id, invitation.status); // -> org_..., "pending"
import os
from authpi_admin import AuthPIAdmin

async with AuthPIAdmin(
    api_key=(os.environ["AUTHPI_KEY_ID"], os.environ["AUTHPI_KEY_SECRET"]),
) as admin:
    issuer = admin.issuer("iss_your_issuer_id")

    # 1. Create the tenant organization
    org = await issuer.organizations.create({"name": "Acme, Inc."})

    # 2. Invite its first member
    invitation = await issuer.organization(org.id).invitations.create({
        "email_invited": "founder@acme.example",
        "scopes": ["member"],
    })

    print(org.id, invitation.status)  # -> org_..., "pending"
# 1. Create the organization
ORG_ID=$(curl -s -X POST \
  "https://api.authpi.com/v1/accounts/$ACCOUNT_ID/issuers/$ISSUER_ID/organizations" \
  -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Acme, Inc." }' | jq -r '.data.id')

# 2. Invite its first member
curl -X POST \
  "https://api.authpi.com/v1/accounts/$ACCOUNT_ID/issuers/$ISSUER_ID/organizations/$ORG_ID/invitations" \
  -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "email_invited": "founder@acme.example", "scopes": ["member"] }'

Verify

List the organization's invitations and confirm yours is there with status pending:

const page = await issuer.organization(org.id).invitations.list({ status: "pending" });
console.log(page.data.map((i) => i.email_invited)); // -> ["founder@acme.example"]
page = await issuer.organization(org.id).invitations.list(status="pending")
print([i.email_invited for i in page.data])  # -> ["founder@acme.example"]
curl "https://api.authpi.com/v1/accounts/$ACCOUNT_ID/issuers/$ISSUER_ID/organizations/$ORG_ID/invitations?status=pending" \
  -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET"

Success looks like: one entry, your invitee's email, "status": "pending". The invitee also receives an email with a hosted accept link.

Common failures

  • 403 forbidden on create — the API key is missing a scope. Confirm what it has with GET /v1/me (the key's scopes are listed under accounts[0]); you need issuers.organizations:write to create the org and issuers.organizations.invitations:write to invite.
  • 400 invalid_request on the invitationA pending invitation already exists for this email: one pending invitation per email per organization. Resend the existing one to nudge the invitee, or revoke it and re-create to change the scopes. A different 400, Invitations are disabled for this organization, means the org-level invitation toggle is off.
  • 404 not_found on the organization — the org_id belongs to a different issuer. Organizations are issuer-scoped: the issuer_id in the path must own the org_id.

Production notes

  • Idempotency. Network retries can double-create. Send an Idempotency-Key header — organizations.create(body, { idempotencyKey }) in TypeScript, organizations.create(body, idempotency_key=...) in Python — so a retried create returns the original result instead of a second org. See idempotency.
  • React with webhooks, not polling. Subscribe to the events organization.invitation.created and organization.invitation.accepted rather than polling the list endpoint.
  • Least privilege. Give this key only issuers.organizations:write and issuers.organizations.invitations:write — one minimally-scoped key per service. See API keys.
  • Rate limits apply to these management calls — batch tenant provisioning accordingly. See rate limits.

Next step