{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/elaichi-ai/json-schema/elaichi-ai-organization-schema.json",
  "title": "Organization",
  "x-generated": "2026-11-06",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/elaichi-ai-openapi.json#/components/schemas/Organization",
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "description": "Organization id (`org_…`) — the value for `X-Organization-Id`."
    },
    "name": {
      "type": "string"
    },
    "slug": {
      "type": "string",
      "description": "Immutable after creation."
    },
    "plan": {
      "type": "string",
      "description": "The stored plan: `personal`, `gold`, `black`, or `none` while on hold. This is not the same as the effective entitlement — read `GET /organization/{id}/entitlements` before gating a feature on it."
    },
    "region": {
      "type": [
        "string",
        "null"
      ],
      "description": "Data location fixed at creation: `us`/`eu` pin a hard jurisdiction, `apac` is a placement hint."
    },
    "trial_ends_at": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time"
    },
    "trial_indefinite_at": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time",
      "description": "Set when Elaichi granted this organization an indefinite trial, otherwise null. Unlocked with no end date: no countdown, no expiry email, billing still reachable."
    },
    "settings": {
      "type": "object",
      "additionalProperties": true,
      "description": "Free-form org settings. `PATCH /organization/{id}` REPLACES this object wholesale."
    },
    "logo": {
      "type": [
        "string",
        "null"
      ],
      "format": "uri",
      "description": "Public URL of the logo image, or null."
    },
    "can_delete": {
      "type": "boolean",
      "description": "Whether the caller may delete this organization: the `org:delete` permission, which only the Org Owner role holds. Server-computed, so branch on this rather than inspecting roles. Always `false` on rows that have no member context to compute it from, such as the organization list `GET /organization`. It never overstates: trust it when true, and read `GET /user/me` or `GET /organization/{id}` (both compute it) when you need it for a list row."
    },
    "can_manage": {
      "type": "boolean",
      "description": "Whether the caller may change this organization's settings: the `org:manage` permission that `PATCH /organization/{id}` requires, and the organization is neither suspended nor in its deletion grace period. Server-computed, and `false` wherever `can_delete` is false for lack of a member context; it never overstates."
    },
    "allow_staff_impersonation": {
      "type": "boolean",
      "description": "Whether Elaichi support may sign in as this organization's members to help them. `true` by default. When `false`, every request made through such a support session 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`)."
    },
    "mfa_required": {
      "type": "boolean",
      "description": "Whether members must hold a second factor to act in this organization. `false` by default. When `true`, a signed-in session that has not shown a second factor (no TOTP challenge at login, no 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 support 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 until the person connects again. Turning it off only lifts the organization requirement: a member who set up an authenticator app of their own is still challenged for it at every non-passkey sign-in. Changed with `PATCH /organization/{id}` (`org:manage`, interactive session only)."
    },
    "can_require_mfa": {
      "type": "boolean",
      "description": "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_reason": {
      "type": [
        "string",
        "null"
      ],
      "description": "A sentence saying why `can_require_mfa` is `false` for a caller who can otherwise manage the organization. `null` otherwise."
    },
    "deletion_scheduled_at": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time",
      "description": "Set when a deletion has been scheduled (`DELETE /organization/{id}`). The organization is read-only until `purge_after`. Null otherwise."
    },
    "purge_after": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time",
      "description": "When a scheduled deletion becomes permanent — 30 days after `deletion_scheduled_at`. Null unless scheduled."
    },
    "can_restore": {
      "type": "boolean",
      "description": "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` on rows with no member context. Read `purge_after` to tell \"too late\" from \"not yours to undo\"."
    },
    "can_authorize_apps": {
      "type": "boolean",
      "description": "Whether the CALLER may connect an app (MCP OAuth consent) to this organization right now: `false` while it is suspended, scheduled for deletion, has no active plan, or is on Personal and the caller is not its primary owner. 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."
    },
    "authorize_apps_blocked_reason": {
      "type": [
        "string",
        "null"
      ],
      "enum": [
        "blocked",
        "pending_deletion",
        "no_plan",
        "plan_single_user",
        null
      ],
      "description": "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`, then `plan_single_user` (Personal admits only the primary owner). `null` when connecting is open."
    },
    "created_at": {
      "type": "string",
      "format": "date-time"
    },
    "updated_at": {
      "type": "string",
      "format": "date-time"
    }
  }
}