Invitations
Invite someone to an organization by email; verify the token, then accept or decline.
Joining an organization is a three-step handshake:
Admin POST /v1/organization/{org_id}/invite → email with a token
Recipient GET /v1/organization/verify-invitation → invitation_id + details
Recipient POST /v1/organization/invitations/{id}/accept (or /decline)The email carries a one-time token; verifying it returns the invitation ID used to accept or decline. Invitations expire after 30 days.
Send an invitation
Requires the organization.members.invite permission — organization
admins (and the owner):
| Field | Meaning |
|---|---|
email required | Who to invite. |
role optional | admin, billing, member, or viewer. Defaults to the organization's default_member_role policy (member unless changed). |
curl -X POST "$API_BASE/v1/organization/$ORG_ID/invite" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "email": "[email protected]", "role": "member" }'from predictai import PredictAI
client = PredictAI(token="pa_live_…")
data = client.organizations.post_organization_by_organization_id_invite(ORG_ID, json={"email": "[email protected]", "role": "member"})
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…" });
const data = await client.organizations.postOrganizationByOrganizationIdInvite(ORG_ID, { json: { email: "[email protected]", role: "member" } });
{
"data": {
"message": "Invitation sent successfully",
"invitation_id": "inv_9c2e4f6a-1b83-4d57-a0e9-6f24c81d3b90",
"email": "[email protected]",
"role": "member",
"expires_at": "2026-08-14T12:00:00+00:00"
}
}If the address has no predictAI account yet, one is created on the Free plan so the invitation has somewhere to land — the recipient claims it when they first sign in. The invitation email is always delivered, regardless of the recipient's notification preferences.
Errors
| Status | Why | Example message |
|---|---|---|
400 | Malformed email | "Invalid email address" |
400 | Unknown role | "Role must be one of: admin, billing, member, viewer" |
400 | Already in the organization | "User is already a member" |
403 | Not an admin | "Insufficient permissions" |
403 | Email outside allowed_email_domains | "This organization only allows members from: acme.dev" |
403 | No free seats on the plan | "All 5 seats on this organization's plan are in use. Add seats to your subscription (or remove a member) before inviting more people." |
The seat-cap error comes back structured:
{
"error": {
"code": "seat_limit_reached",
"message": "All 5 seats on this organization's plan are in use. Add seats to your subscription (or remove a member) before inviting more people.",
"details": { }
},
"status": "All 5 seats on this organization's plan are in use. Add seats to your subscription (or remove a member) before inviting more people."
}Verify an invitation
The recipient's email link carries a token — this is the one route in
the section that needs no authentication, so it works before the
recipient has signed in:
curl "$API_BASE/v1/organization/verify-invitation?token=$INVITATION_TOKEN"from predictai import PredictAI
client = PredictAI(token="pa_live_…")
data = client.organizations.get_organization_verify_invitation(params={"token": INVITATION_TOKEN})
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…" });
const data = await client.organizations.getOrganizationVerifyInvitation({ params: { token: INVITATION_TOKEN } });
{
"data": {
"valid": true,
"invitation_id": "inv_9c2e4f6a-1b83-4d57-a0e9-6f24c81d3b90",
"organization_name": "Acme Analytics",
"role": "member",
"email": "[email protected]"
}
}| Parameter | Meaning |
|---|---|
token required | The one-time token from the invitation email. |
Keep the invitation_id — it's what accept and decline take.
Errors
| Status | Why | Example message |
|---|---|---|
400 | No token in the query string | "Token is required" |
401 | Token is invalid, expired, or already used | "Invalid, expired, or already used invitation" |
{
"error": {
"code": "invalid_token",
"message": "Invalid, expired, or already used invitation",
"details": { }
},
"status": "Invalid, expired, or already used invitation"
}Accept or decline
Both calls authenticate as the recipient and take the invitation_id:
curl -X POST "$API_BASE/v1/organization/invitations/$INVITATION_ID/accept" \
-H "Authorization: Bearer $TOKEN"from predictai import PredictAI
client = PredictAI(token="pa_live_…")
data = client.organizations.post_organization_invitations_by_invitation_id_accept(INVITATION_ID)
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…" });
const data = await client.organizations.postOrganizationInvitationsByInvitationIdAccept(INVITATION_ID);
{ "data": { "message": "Invitation accepted successfully" } }Accepting adds you to the organization with the invited role, effective immediately.
curl -X POST "$API_BASE/v1/organization/invitations/$INVITATION_ID/decline" \
-H "Authorization: Bearer $TOKEN"from predictai import PredictAI
client = PredictAI(token="pa_live_…")
data = client.organizations.post_organization_invitations_by_invitation_id_decline(INVITATION_ID)
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…" });
const data = await client.organizations.postOrganizationInvitationsByInvitationIdDecline(INVITATION_ID);
{ "data": { "message": "Invitation declined successfully" } }Errors
| Status | Why | Example message |
|---|---|---|
400 | Invitation not found, not pending, or the organization's seats filled up since the invite was sent | "Failed to accept invitation or invitation not found" |
400 | (decline) Invitation not found or not pending | "Failed to decline invitation or invitation not found" |
List an organization's invitations
Any member can list invitations:
| Parameter | Meaning |
|---|---|
status optional | pending (default), accepted, declined, or expired. |
curl "$API_BASE/v1/organization/$ORG_ID/invitations?status=pending" \
-H "Authorization: Bearer $TOKEN"from predictai import PredictAI
client = PredictAI(token="pa_live_…")
data = client.organizations.get_organization_by_organization_id_invitations(ORG_ID, params={"status": "pending"})
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…" });
const data = await client.organizations.getOrganizationByOrganizationIdInvitations(ORG_ID, { params: { status: "pending" } });
{
"data": {
"invitations": [
{
"uid": "inv_9c2e4f6a-1b83-4d57-a0e9-6f24c81d3b90",
"email": "[email protected]",
"role": "member",
"status": "pending",
"created_at": "2026-07-15T12:00:00+00:00",
"expires_at": "2026-08-14T12:00:00+00:00",
"inviter_email": "[email protected]",
"inviter_name": "Ada Norberg"
}
],
"count": 1
}
}Revoke an invitation
Requires organization.members.invite. Only still-pending invitations
can be revoked; the emailed link stops working immediately:
curl -X DELETE "$API_BASE/v1/organization/$ORG_ID/invitations/$INVITATION_ID" \
-H "Authorization: Bearer $TOKEN"from predictai import PredictAI
client = PredictAI(token="pa_live_…")
data = client.organizations.delete_organization_by_organization_id_invitations_by_invitation_id(ORG_ID, INVITATION_ID)
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…" });
const data = await client.organizations.deleteOrganizationByOrganizationIdInvitationsByInvitationId(ORG_ID, INVITATION_ID);
{ "data": { "message": "Invitation revoked", "invitation_id": "inv_…" } }Errors
| Status | Why | Example message |
|---|---|---|
403 | Not allowed to manage invites | "Insufficient permissions" |
404 | Invitation unknown or no longer pending | "Invitation not found or no longer pending" |

