Skip to content
POST /organization

Creates an organization and makes the caller its first member, holding the immutable Org Owner role; the predefined role set is seeded at the same time. Requires an interactive session — an API token is refused with 403, since a token scoped to one org must not be able to create another. Also requires this deployment to have public signup enabled (`PUBLIC_SIGNUP`), otherwise 403 `signup_disabled`.

Request Body

namestring
referral_codestring

Optional referral code, for tracking only. Whitespace is trimmed, a blank value is treated as absent, and it is stored upper-cased. Recorded once at creation; not returned by this API and not editable.

regionstring

Data location, fixed at creation: eu/us pin a hard Durable Object jurisdiction, apac is a placement hint. It cannot be changed afterwards.

Possible values:
useuapac
signup_sourceobject

Optional attribution for where the signup came from, for tracking only. Every field is optional. Malformed input is dropped, never a 400: unknown keys, non-string or blank values, and a referrer_host that is not a hostname are ignored. Values are cut to 100 characters; referrer_host is reduced to a lowercase hostname (a full URL is accepted and stripped to its host). Recorded once at creation; not returned by this API and not editable.

refstring
referrer_hoststring
utm_campaignstring
utm_contentstring
utm_mediumstring
utm_sourcestring
utm_termstring
slugstring

Lowercase alphanumeric with hyphens. Generated from the name when omitted.

Response Body

allow_staff_impersonationboolean

Whether Elaichi staff may impersonate this organization's members for support. true by default. When false, every request an impersonated session makes against this organization is refused with 403 staff_impersonation_blocked, reads included, and the organization is left out of that session's GET /user/me and GET /organization. Changed with PATCH /organization/{id} (org:manage).

authorize_apps_blocked_reasonstring,null

Why can_authorize_apps is false, in the gate's own order: blocked, then pending_deletion (even when the subscription is also gone, since a soft delete cancels it), then no_plan. null when connecting is open.

Possible values:
blockedpending_deletionno_plannull
can_authorize_appsboolean

Whether the CALLER may connect an app (MCP OAuth consent) to this organization right now: false while it is suspended, scheduled for deletion, or has no active plan. Server-computed by the same gate POST /oauth/authorize-request/{id}/approve calls; present only on member-scoped responses. authorize_apps_blocked_reason says which.

can_deleteboolean

Whether the CALLER may delete this organization — the org:delete permission, which only the Org Owner role holds, and never for the platform root organization. Server-computed: branch on this rather than inspecting roles. Always false where the response has no member context to compute it from — the org switcher list GET /organization (one member-context lookup per row would be a per-row round trip), the staff console, and the invite-accept response. It never overstates: trust it when true, and read GET /user/me or GET /organization/{id} (both of which compute it) when you need it for a list row.

can_manageboolean

Whether the CALLER may change this organization's own settings — the org:manage permission that PATCH /organization/{id} requires, and the organization neither staff-suspended nor in its deletion grace period. Server-computed, and false wherever can_delete is for lack of a member context; it never overstates.

can_require_mfaboolean

Whether the CALLER may turn mfa_required ON right now: can_manage, from an interactive session that would itself pass the check it is about to create. false — with the reason in can_require_mfa_reason — when switching it on would lock the caller out of this organization. The PATCH refuses that regardless with 409 mfa_setup_required. Turning it OFF is not governed by this field; it needs a step-up.

can_require_mfa_reasonstring,null

A sentence saying why can_require_mfa is false for a caller who can otherwise manage the organization. null otherwise.

can_restoreboolean

Whether the CALLER may cancel a scheduled deletion: the org:manage permission, and only while purge_after is still ahead. Server-computed, like can_delete, and false for the same reasons — no member context (the org switcher list, the staff console) — plus once the window has closed. Read purge_after to tell "too late" from "not yours to undo".

created_atstring · date-time
deletion_scheduled_atstring,null · date-time

Set when a deletion has been scheduled (DELETE /organization/{id}). The organization is read-only until purge_after. Null otherwise.

idstring

Organization id (org_…) — the value for X-Organization-Id.

logostring,null · uri

Public URL of the logo image, or null.

mfa_requiredboolean

Whether members must hold a second factor to act in this organization. false by default, for existing and new organizations alike. When true, a signed-in session that has not shown a second factor — no TOTP challenge at login, not a passkey sign-in, no factor enrolled since — is refused on every organization-scoped route, reads included, with 403 mfa_setup_required (error.details.has_second_factor says whether the person already has one and only needs to sign in with it). Exempt: org API tokens, Elaichi staff impersonation sessions, and a session signed in through THIS organization's own SSO connection. MCP connections made before it was switched on, or from a session without a factor, are refused too until the person connects again. Turning it OFF only lifts that requirement: a member who set up an authenticator app of their own is still challenged for it at every non-passkey sign-in, because the factor belongs to the member and no organization setting can switch it off. Changed with PATCH /organization/{id} (org:manage, interactive session only).

namestring
planstring

The stored plan: gold, black, or none when locked. This is not the same as the effective entitlement — read GET /organization/{id}/entitlements before gating a feature on it.

purge_afterstring,null · date-time

When a scheduled deletion becomes permanent — 30 days after deletion_scheduled_at. Null unless scheduled.

regionstring,null

Data location fixed at creation: us/eu pin a hard jurisdiction, apac is a placement hint.

settingsRecord<string, any>

Free-form org settings. PATCH /organization/{id} REPLACES this object wholesale.

slugstring

Immutable after creation.

trial_ends_atstring,null · date-time
trial_indefinite_atstring,null · date-time

When staff granted an indefinite trial, or null. Unlocked with no end date: no countdown, no expiry email, billing still reachable.

updated_atstring · date-time
curl -X POST 'https://api.elaichi.ai/organization' \
  -H 'Authorization: Bearer $ELAICHI_API_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"name":"your_name","slug":"your_slug","region":"us","referral_code":"your_referral_code","signup_source":{}}'
const body = {
  "name": "your_name",
  "slug": "your_slug",
  "region": "us",
  "referral_code": "your_referral_code",
  "signup_source": {}
};

const response = await fetch('https://api.elaichi.ai/organization', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + process.env.ELAICHI_API_TOKEN,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(body),
});

const data = await response.json();
console.log(data);
import os
import requests

url = "https://api.elaichi.ai/organization"
headers = {
    "Authorization": f"Bearer {os.environ['ELAICHI_API_TOKEN']}",
    "Content-Type": "application/json",
}
payload = {
    "name": "your_name",
    "slug": "your_slug",
    "region": "us",
    "referral_code": "your_referral_code",
    "signup_source": {}
}

response = requests.post(url, headers=headers, json=payload)
print(response.json())