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:writeandissuers.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 forbiddenon create — the API key is missing a scope. Confirm what it has withGET /v1/me(the key'sscopesare listed underaccounts[0]); you needissuers.organizations:writeto create the org andissuers.organizations.invitations:writeto invite.400 invalid_requeston the invitation —A 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_foundon the organization — theorg_idbelongs to a different issuer. Organizations are issuer-scoped: theissuer_idin the path must own theorg_id.
Production notes
- Idempotency. Network retries can double-create. Send an
Idempotency-Keyheader —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.createdandorganization.invitation.acceptedrather than polling the list endpoint. - Least privilege. Give this key only
issuers.organizations:writeandissuers.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
- Manage the organization lifecycle — suspend and delete tenants
- Validate tokens in your API — read the active-organization claim once members sign in