> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cloudhumans.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sign in

> Get the token every Claudia API call needs, including the two-step path.

export const SignInFlow = () => {
  const SIGNIN_URL = "https://api.cloudhumans.com/auth/v1/signin";
  const CHALLENGE_URL = "https://api.cloudhumans.com/auth/v1/signin/challenge";
  const REQUEST_TIMEOUT_MS = 15000;
  const ANSWERABLE = {
    SMS_MFA: {
      field: "code",
      label: "Code from the text message",
      hint: "Six digits, sent to the phone registered on your account.",
      placeholder: "123456",
      type: "text",
      autoComplete: "one-time-code",
      width: "max-w-xs"
    },
    NEW_PASSWORD_REQUIRED: {
      field: "new_password",
      label: "Choose a new password",
      hint: "Your current one is temporary. This has to satisfy your account's password policy.",
      placeholder: "",
      type: "password",
      autoComplete: "new-password",
      width: ""
    }
  };
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [challenge, setChallenge] = useState(null);
  const [answer, setAnswer] = useState("");
  const [token, setToken] = useState(null);
  const [status, setStatus] = useState({
    state: "idle"
  });
  const [copied, setCopied] = useState("");
  const busy = status.state === "busy";
  const inFlight = useRef(null);
  const generation = useRef(0);
  const post = async (url, body) => {
    const controller = new AbortController();
    inFlight.current = controller;
    const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
    try {
      const response = await fetch(url, {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify(body),
        signal: controller.signal
      });
      let parsed = null;
      try {
        parsed = await response.json();
      } catch (error) {
        parsed = null;
      }
      if (!parsed) return {
        kind: "unreachable"
      };
      if (parsed.id_token) return {
        kind: "token",
        value: parsed
      };
      if (parsed.challenge) return {
        kind: "challenge",
        value: parsed
      };
      return {
        kind: "error",
        status: response.status,
        value: parsed
      };
    } catch (error) {
      return {
        kind: "unreachable"
      };
    } finally {
      clearTimeout(timer);
      if (inFlight.current === controller) inFlight.current = null;
    }
  };
  const postCurrent = async (url, body) => {
    const gen = ++generation.current;
    const result = await post(url, body);
    return gen === generation.current ? result : null;
  };
  const applyResult = result => {
    if (result.kind === "token") {
      setChallenge(null);
      setAnswer("");
      setPassword("");
      setToken(result.value);
      setStatus({
        state: "done"
      });
      return;
    }
    if (result.kind === "challenge") {
      setChallenge({
        challenge: result.value.challenge,
        session: result.value.session
      });
      setAnswer("");
      setStatus({
        state: "idle"
      });
      return;
    }
    if (result.kind === "unreachable") {
      setStatus({
        state: "unreachable"
      });
      return;
    }
    setStatus({
      state: "error",
      code: result.value.error || "AuthError",
      message: result.value.message || "The request was rejected.",
      httpStatus: result.status
    });
  };
  const signIn = async event => {
    event.preventDefault();
    if (!email.trim() || !password) return;
    setStatus({
      state: "busy"
    });
    setToken(null);
    setChallenge(null);
    const result = await postCurrent(SIGNIN_URL, {
      email: email.trim(),
      password
    });
    if (result) applyResult(result);
  };
  const answerChallenge = async event => {
    event.preventDefault();
    const shape = ANSWERABLE[challenge.challenge];
    if (!shape || !answer.trim()) return;
    setStatus({
      state: "busy"
    });
    const result = await postCurrent(CHALLENGE_URL, {
      challenge: challenge.challenge,
      session: challenge.session,
      email: email.trim(),
      [shape.field]: answer.trim()
    });
    if (result) applyResult(result);
  };
  const startOver = () => {
    generation.current += 1;
    if (inFlight.current) inFlight.current.abort();
    setChallenge(null);
    setAnswer("");
    setToken(null);
    setStatus({
      state: "idle"
    });
  };
  const copy = async (text, which) => {
    try {
      await navigator.clipboard.writeText(text);
      setCopied(which);
      setTimeout(() => setCopied(""), 2000);
    } catch (error) {
      setCopied("");
    }
  };
  const curlForStep = () => {
    if (challenge) {
      const shape = ANSWERABLE[challenge.challenge];
      const field = shape ? shape.field : "code";
      return [`curl -X POST ${CHALLENGE_URL} \\`, `  -H "Content-Type: application/json" \\`, `  -d '${JSON.stringify({
        challenge: challenge.challenge,
        session: challenge.session,
        email: email.trim(),
        [field]: answer.trim() || `<YOUR_${field.toUpperCase()}>`
      })}'`].join("\n");
    }
    return [`curl -X POST ${SIGNIN_URL} \\`, `  -H "Content-Type: application/json" \\`, `  -d '${JSON.stringify({
      email: email.trim() || "you@company.com",
      password: "<YOUR_PASSWORD>"
    })}'`].join("\n");
  };
  const label = "text-sm font-medium text-gray-900 dark:text-gray-100";
  const muted = "text-sm text-gray-600 dark:text-gray-400";
  const fieldLabel = "block text-sm text-gray-600 dark:text-gray-400";
  const linkButton = "inline-flex cursor-pointer items-center py-3 text-sm leading-5 text-gray-600 underline underline-offset-2 dark:text-gray-400";
  const box = "rounded-xl border border-gray-200 dark:border-white/10 p-4";
  const input = "mt-2 w-full rounded-lg border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5 px-3 py-3 text-base text-gray-900 dark:text-gray-100 focus:border-primary disabled:opacity-60";
  const button = "inline-flex cursor-pointer items-center rounded-lg bg-primary px-5 py-3 text-sm font-medium leading-5 text-white disabled:cursor-not-allowed disabled:opacity-50 dark:bg-primary-light dark:text-gray-900";
  const codeBlock = "mt-2 overflow-x-auto rounded-lg border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5 p-3 font-mono text-xs text-gray-900 dark:text-gray-100";
  const shape = challenge ? ANSWERABLE[challenge.challenge] : null;
  const shortSession = challenge ? `${challenge.session.slice(0, 24)}… (${challenge.session.length} chars)` : "";
  return <div className="not-prose flex flex-col gap-4">
      {}
      <form className={box} onSubmit={signIn} aria-busy={busy}>
        <p className={label}>1 · Sign in</p>
        <p className={`${muted} mt-1`}>
          Your password is sent to <code>api.cloudhumans.com</code> and nowhere else, over HTTPS,
          to get you a token. This page stores nothing and logs nothing — you can confirm both in
          your browser's network tab.
        </p>

        <div className="mt-3 grid gap-3 sm:grid-cols-2">
          <div>
            <label className={fieldLabel} htmlFor="sf-email">
              Email
            </label>
            <input id="sf-email" type="email" value={email} onChange={event => setEmail(event.target.value)} disabled={busy || !!challenge} autoComplete="username" spellCheck={false} placeholder="you@company.com" className={input} />
          </div>
          <div>
            <label className={fieldLabel} htmlFor="sf-password">
              Password
            </label>
            <input id="sf-password" type="password" value={password} onChange={event => setPassword(event.target.value)} disabled={busy || !!challenge} autoComplete="current-password" className={input} />
          </div>
        </div>

        {!challenge && <div className="mt-3 flex items-center gap-3">
            <button type="submit" className={button} disabled={busy || !email.trim() || !password}>
              {busy ? "Signing in…" : "Sign in"}
            </button>
            {token && <button type="button" onClick={startOver} className={linkButton}>
                Start over
              </button>}
          </div>}
      </form>

      {}
      {challenge && <form className={box} onSubmit={answerChallenge} aria-busy={busy}>
          <p className={label}>
            2 ·{" "}
            {challenge.challenge === "SMS_MFA" ? "Two-step verification" : "Your password is temporary"}
          </p>
          <p className={`${muted} mt-1`}>
            Sign-in came back with <code>{challenge.challenge}</code> instead of a token.
            {challenge.challenge === "SMS_MFA" ? " A code is already on its way to your phone." : " Nothing was sent to you — pick a new password and you are done."}
          </p>

          <dl className="mt-3 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs">
            <dt className={muted}>challenge</dt>
            <dd className="font-mono text-gray-900 dark:text-gray-100">{challenge.challenge}</dd>
            <dt className={muted}>session</dt>
            <dd className="break-all font-mono text-gray-600 dark:text-gray-400">{shortSession}</dd>
            <dt className={muted}>email</dt>
            <dd className="font-mono text-gray-900 dark:text-gray-100">{email.trim()}</dd>
          </dl>

          {shape ? <>
              <div className="mt-3">
                <label className={fieldLabel} htmlFor="sf-answer">
                  {shape.label}
                </label>
                <input id="sf-answer" type={shape.type} value={answer} onChange={event => setAnswer(event.target.value)} disabled={busy} autoComplete={shape.autoComplete} inputMode={challenge.challenge === "SMS_MFA" ? "numeric" : undefined} spellCheck={false} placeholder={shape.placeholder} className={`${input} ${shape.width}`} autoFocus />
                <p className={`${muted} mt-1`}>{shape.hint}</p>
              </div>
              <div className="mt-3 flex items-center gap-3">
                <button type="submit" className={button} disabled={busy || !answer.trim()}>
                  {busy ? "Checking…" : "Continue"}
                </button>
                <button type="button" onClick={startOver} className={linkButton}>
                  Start over
                </button>
              </div>
            </> : <>
              <p className="mt-3 text-sm text-amber-700 dark:text-amber-300">
                This account needs a <code>{challenge.challenge}</code> challenge, which this API
                cannot answer. Sign in through the Cloud Chat interface to get a token.
              </p>
              <button type="button" onClick={startOver} className={`${linkButton} mt-1`}>
                Start over
              </button>
            </>}
        </form>}

      {}
      {status.state === "error" && <div role="alert" className="rounded-xl border border-red-200 dark:border-red-400/30 bg-red-50 dark:bg-red-400/10 p-4">
          <p className="text-sm font-medium text-red-800 dark:text-red-200">
            {status.code === "CodeMismatchException" || status.code === "ExpiredCodeException" ? "That code was not accepted" : status.code === "InvalidPasswordException" ? "That password does not meet your account's policy" : status.httpStatus === 429 ? "Too many attempts" : "Sign-in failed"}
          </p>
          <p className="mt-1 text-sm text-red-800 dark:text-red-200">
            {}
            {status.code === "CodeMismatchException" ? "Either the code is wrong or this sign-in attempt has expired — the API answers the same way for both. Try the code again, or start over to get a new one." : status.httpStatus === 429 ? "Wait a moment before trying again." : status.message}
          </p>
          <p className="mt-2 font-mono text-xs text-red-700 dark:text-red-300">{status.code}</p>
          {challenge && <button type="button" onClick={startOver} className="mt-2 text-sm text-red-800 underline underline-offset-2 dark:text-red-200">
              Start over
            </button>}
        </div>}

      {}
      {status.state === "unreachable" && <div role="status" aria-live="polite" className={box}>
          <p className={label}>This page could not reach the API from your browser</p>
          <p className={`${muted} mt-1`}>
            Nothing is wrong with your credentials — the request never completed. Run the call
            yourself instead; every value below is already filled in.
          </p>
          <p className={`${muted} mt-1`}>
            With devtools open you will also see a CORS error in the console. Nothing in this page
            can catch that one, so it is worth saying plainly rather than leaving you to conclude
            the page is broken.
          </p>
          <pre className={codeBlock}>{curlForStep()}</pre>
          <div className="mt-2 flex items-center gap-3">
            <button type="button" onClick={() => copy(curlForStep(), "curl")} className={linkButton}>
              {copied === "curl" ? "Copied" : "Copy"}
            </button>
            <button type="button" onClick={startOver} className={linkButton}>
              Start over
            </button>
          </div>
        </div>}

      {}
      {status.state === "done" && token && <div role="status" aria-live="polite" className="rounded-xl border border-emerald-200 dark:border-emerald-400/30 bg-emerald-50 dark:bg-emerald-400/10 p-4">
          <p className="text-sm font-medium text-emerald-900 dark:text-emerald-100">
            Signed in. Use the <code>id_token</code>.
          </p>
          <p className="mt-1 text-sm text-emerald-800 dark:text-emerald-200">
            Send it as <code>Authorization: Bearer &lt;id_token&gt;</code> — not the{" "}
            <code>access_token</code>, which does not carry the identity we authorize on. It
            expires in {token.expires_in} seconds.
          </p>
          <pre className={`${codeBlock} break-all whitespace-pre-wrap`}>{token.id_token}</pre>
          <div className="mt-2 flex items-center gap-3">
            <button type="button" onClick={() => copy(token.id_token, "token")} className="text-sm text-emerald-900 underline underline-offset-2 dark:text-emerald-100">
              {copied === "token" ? "Copied" : "Copy id_token"}
            </button>
            <button type="button" onClick={startOver} className={linkButton}>
              Start over
            </button>
          </div>
        </div>}
    </div>;
};

Every Claudia API call carries the `id_token` from `POST /auth/v1/signin` as
`Authorization: Bearer <id_token>` — not the `access_token`, which does not carry the identity we
authorize on. One token works across every Cloud Humans API, and the account you reach is derived
from it, so there is nothing else to configure.

Sign in below to get one.

<SignInFlow />

<Note>
  **Your password goes to `api.cloudhumans.com`, and nowhere else.** It is sent there once, over
  HTTPS, to get you a token — this page makes no other request with it, stores nothing, and logs
  nothing. You can confirm that in your browser's network tab.

  `api.cloudhumans.com` is also the only place your token should *ever* go. Any other site offering
  to "check" or "decode" it for you is not one to trust.
</Note>

## Not every sign-in ends in a token

Accounts with a temporary password, or with two-step verification turned on, come back with a
**challenge** instead: no token yet, and a `session` to answer it with. The form above handles
that for you — the second step appears only when there is one, already carrying what the first
step returned.

Building this into your own client means writing a loop rather than a single call, because
answering a challenge can return **another** one:

| The response contains                | What it is                                                 | What you send back                 |
| ------------------------------------ | ---------------------------------------------------------- | ---------------------------------- |
| `id_token`                           | Done.                                                      | Nothing. Use it.                   |
| `challenge: "SMS_MFA"`               | A code was just texted to the phone on the account.        | `code`, with the `session`         |
| `challenge: "NEW_PASSWORD_REQUIRED"` | The password is temporary — first access, or it was reset. | `new_password`, with the `session` |
| `error`                              | Nothing to answer.                                         | Start over.                        |

**Branch on the body, not on the status** — both outcomes are `200`. A first-access user who also
has two-step verification really does go `NEW_PASSWORD_REQUIRED` → `SMS_MFA` → token, and each
step returns a **new** `session` that replaces the one before it.

Three details worth knowing before your own client meets a real user:

**Send the code as a string.** Codes can start with a zero, and `"012345"` parsed into a JSON
number is a different code. A number is rejected with a `400` rather than coerced, precisely so
this shows up on your first test instead of as one user in ten who "can't log in".

**A wrong code and an expired session look identical.** Both come back
`400 CodeMismatchException`, so your UI cannot honestly say which happened. Offer a retry, and
next to it a way back to the start — retrying against an expired session never recovers.

**The session is not yours to keep.** It belongs to one sign-in attempt, so there is nothing to
persist and nothing to resume. When it stops being accepted, the only move is `/auth/v1/signin`
again, which sends a fresh code.

Full request and response shapes are on the
[sign-in](/api-reference/claudia/v1/auth/authentication/sign-in-and-get-a-token) and
[challenge](/api-reference/claudia/v1/auth/authentication/answer-a-sign-in-challenge) pages.
