---
title: "Create Onboarding Organization"
method: POST
path: "/api/organization/onboarding/create"
tags: ["organization"]
---

# Create Onboarding Organization

`POST /api/organization/onboarding/create`

Initialize a new org on the server: one transaction, one definitive answer.

Onboarding used to write `orgs` from the browser under RLS, where
`orgs_crud_user` (`requesting_org_id() = org_id`) only passes once Clerk has
minted a JWT carrying the brand-new org, and then made two more client calls
to enqueue the signup job chain and the structural defaults. A signup could
lose the RLS race and discard the user's business description and links, or
win it and still land a populated row with no Business DNA.

So this endpoint owns all of it:

1. `onboarding_initialize_org` writes the `orgs` row AND a durable
   job-handoff intent in ONE Postgres transaction. Either both land or
   neither does — there is no "row exists but nothing will build it" state.
2. The signup job chain is enqueued FROM that committed intent, and the
   structural defaults follow after the response. Anything left unfinished
   is completed by `drain_pending_initializations`, so an org row always
   converges to a fully initialized workspace.

Two deliberate departures from the rest of this file:

1. It does NOT raise 401 when the JWT carries no `org_id` claim — that
   claim missing is precisely this endpoint's case.
   `get_current_user_and_org` already returns `(user_id, None)` without
   raising in that situation.
2. The org comes from the BODY. That is an explicit exception to
   `internal-docs/fastapi/org-context-contract.md` ("org comes from the
   JWT, never a header"), and it is safe only because the body value is
   AUTHORIZED, never trusted: Clerk must confirm the caller holds an
   admin role in that org before anything is written, an answer Clerk
   cannot enumerate fails closed with 503 rather than falling through to a
   write, and an existing row is neither proof of completion nor
   permission to overwrite. No org header is read; the header guard is
   untouched.

Responses:
    200  { org, jobs_enqueued, initialization } — initialized, replayed
         identically, or re-initialized by the same admin after an attempt
         that never completed. A genuine resume is never a 409: dead-ending
         recovery is the failure mode this endpoint exists to remove.
    409  the same key with a DIFFERENT payload, or an initialization that
         belongs to another actor / an org that already completed one
    403  authenticated, but not permitted to initialize this org
    503  authorization undeterminable — fail closed, never a write
    422  a malformed body — including a `signup_platform` that is not a
         recognised client surface, which is refused rather than coerced
    401  no user in the token

Web and mobile both use it. Everything a mobile signup needs beyond the web
payload — its platform stamp, its theme colour, its inline survey answers —
is optional and absent from a web request, so the web client's request is
unchanged down to the payload fingerprint.

## Request body

- OnboardingCreateOrgRequest — The onboarding form's payload, plus the Clerk org it belongs to. Mirrors what `createOrgRecordInSupabase` used to write from the browser (every-react `packages/auth/OrgContext.tsx`), plus the two values the two now-collapsed follow-up calls carried.
  - `org_id` string, required — Clerk organization id the caller just created
  - `guided_onboarding_version` 1, nullable
  - `name` string, nullable
  - `address` string, nullable
  - `currency` string, nullable
  - `sales_tax_default` string, nullable
  - `org_prompt` string, nullable
  - `email` string, nullable
  - `phone` string, nullable
  - `vertical` string, nullable
  - `signup_source` string, nullable
  - `org_links` object, nullable — Same shape OrgContext.saveOrgLinks writes: {files, links, driveFiles}
  - `timezone` string, nullable — IANA timezone from the browser, for the Daily Brief schedule
  - `daily_brief` object, nullable — Daily Brief defaults to apply during the structural bootstrap
  - `signup_platform` string, nullable — Client surface that created this org: 'web' or 'mobile'. Absent means 'web'. Any other value is refused, never coerced — this column keys the lazy free-trial grant.
  - `brand_color` string, nullable — Client theme colour as hex (#RGB/#RRGGBB/#RRGGBBAA). Fill-if-null.
  - `onboarding_profile` object, nullable — Mobile's inline onboarding-survey answers, in the same JSON shape business_dna_service reads. One of the three sources the Business DNA gate accepts.

## Response `200`

Successful Response

- unknown

## Other responses

- `422` — Validation Error

## Changes

- **2026-09-26** `e1ba6d9dba94` — 1 info
  - added the new optional request property `guided_onboarding_version`
- **2026-09-18** `a15aa5b1bb56` — 1 info
  - endpoint added

[Change history](https://skmtc.dev/every/apis/every-api/changes/api/organization/onboarding/create/post.md)

---

[API](https://skmtc.dev/every/apis/every-api.md) · [All operations](https://skmtc.dev/every/apis/every-api/llms.txt) · [OpenAPI document](https://skmtc.dev/every/apis/every-api/revisions/e1ba6d9dba94?raw)
