> ## 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.

# Find your credentials

> Sign in, then get the cloudchat-instance header and account id every call needs.

export const CloudChatInstanceFinder = () => {
  const PRODUCTION_INSTANCES = ["1", "2", "3", "5", "6"];
  const STORAGE_KEY = "cloudchat-api-selected-account";
  const LOOKUP_URL = "https://api.cloudhumans.com/auth/v1/me/cloudchat-accounts";
  const LOOKUP_TIMEOUT_MS = 8000;
  const CLAIM_ROLE_NAMES = {
    0: "agent",
    1: "administrator",
    2: "supervisor",
    3: "assistant",
    4: "cx_engineer"
  };
  const LOOKUP_DEBOUNCE_MS = 400;
  const decodeSegment = segment => {
    const padded = segment.replace(/-/g, "+").replace(/_/g, "/");
    const binary = atob(padded + ("=").repeat((4 - padded.length % 4) % 4));
    const bytes = Uint8Array.from(binary, char => char.charCodeAt(0));
    return new TextDecoder().decode(bytes);
  };
  const dedupe = entries => {
    const seen = new Set();
    return entries.filter(entry => {
      const key = `${entry.instance}:${entry.account}`;
      if (seen.has(key)) return false;
      seen.add(key);
      return true;
    });
  };
  const readClaims = input => {
    const token = input.trim().replace(/^Bearer\s+/i, "");
    if (!token) return {
      state: "empty",
      token: ""
    };
    const segments = token.split(".");
    if (segments.length !== 3) {
      return {
        state: "error",
        token,
        message: "That does not look like a JWT. A token has three parts separated by dots — paste the whole id_token."
      };
    }
    let claims;
    try {
      claims = JSON.parse(decodeSegment(segments[1]));
    } catch (error) {
      return {
        state: "error",
        token,
        message: "The middle part of the token could not be decoded. Copy it again, making sure nothing was truncated."
      };
    }
    const parsed = [];
    for (const entry of String(claims["custom:cloudchat_accounts"] || "").split(",")) {
      const parts = entry.split(":").map(part => part.trim());
      if (parts.length < 3 || !parts[0] || !parts[1]) continue;
      parsed.push({
        instance: parts[0],
        account: parts[1],
        role: Object.prototype.hasOwnProperty.call(CLAIM_ROLE_NAMES, parts[2]) ? CLAIM_ROLE_NAMES[parts[2]] : null,
        name: null
      });
    }
    const entries = dedupe(parsed);
    if (entries.length === 0) {
      return {
        state: "error",
        token,
        message: "This token carries no Cloud Chat accounts, so it cannot call the Cloud Chat API. Either it is an access_token rather than an id_token, or your user has no Cloud Chat membership yet — ask your Cloud Humans contact."
      };
    }
    return {
      state: "ok",
      token,
      email: typeof claims.email === "string" ? claims.email : null,
      expiresAt: typeof claims.exp === "number" ? new Date(claims.exp * 1000) : null,
      entries
    };
  };
  const [raw, setRaw] = useState("");
  const [picked, setPicked] = useState(null);
  const [copied, setCopied] = useState(false);
  const [lookup, setLookup] = useState(null);
  const result = readClaims(raw);
  const token = result.token;
  useEffect(() => {
    if (result.state !== "ok" || !token) {
      setLookup(null);
      return;
    }
    let active = true;
    const controller = new AbortController();
    let timeout;
    setLookup({
      token,
      state: "loading",
      entries: []
    });
    const debounce = setTimeout(() => {
      timeout = setTimeout(() => controller.abort(), LOOKUP_TIMEOUT_MS);
      fetch(LOOKUP_URL, {
        headers: {
          Authorization: `Bearer ${token}`
        },
        signal: controller.signal
      }).then(response => response.ok ? response.json() : Promise.reject(new Error("lookup failed"))).then(body => {
        if (!active) return;
        const named = dedupe((Array.isArray(body) ? body : []).filter(row => row && row.instance != null && row.accountId != null).map(row => ({
          instance: String(row.instance),
          account: String(row.accountId),
          name: typeof row.accountName === "string" && row.accountName ? row.accountName : null,
          role: typeof row.role === "string" ? row.role : null
        })));
        if (named.length === 0) {
          setLookup({
            token,
            state: "unavailable",
            entries: []
          });
          return;
        }
        setLookup({
          token,
          state: "named",
          entries: named
        });
      }).catch(() => {
        if (active) setLookup({
          token,
          state: "unavailable",
          entries: []
        });
      }).finally(() => clearTimeout(timeout));
    }, LOOKUP_DEBOUNCE_MS);
    return () => {
      active = false;
      clearTimeout(debounce);
      clearTimeout(timeout);
      controller.abort();
    };
  }, [token, result.state]);
  const claimEntries = result.state === "ok" ? result.entries : [];
  const lookupState = lookup && lookup.token === token && result.state === "ok" ? lookup.state : "pending";
  const named = lookupState === "named";
  const unavailable = lookupState === "unavailable";
  const looking = lookupState === "loading" || lookupState === "pending";
  const entries = named ? lookup.entries : claimEntries;
  const selected = entries.length === 0 ? null : entries.length === 1 ? entries[0] : entries.find(entry => `${entry.instance}:${entry.account}` === picked) || null;
  const expired = result.state === "ok" && result.expiresAt && result.expiresAt < new Date();
  const routable = selected && PRODUCTION_INSTANCES.includes(selected.instance);
  useEffect(() => {
    if (!selected || !routable) return;
    try {
      window.localStorage.setItem(STORAGE_KEY, JSON.stringify({
        instance: selected.instance,
        account: selected.account,
        name: selected.name || null
      }));
    } catch (error) {}
  }, [selected && selected.instance, selected && selected.account, selected && selected.name, routable]);
  const optionLabel = entry => {
    const head = entry.name ? `Instance ${entry.instance} — ${entry.name} (id ${entry.account})` : `Instance ${entry.instance} — account ${entry.account}`;
    const tail = entry.role ? ` · ${entry.role}` : "";
    return PRODUCTION_INSTANCES.includes(entry.instance) ? `${head}${tail}` : `${head}${tail} — not on the production API`;
  };
  const curl = selected ? [`curl https://api.cloudhumans.com/cloudchat/v1/accounts/${selected.account}/canned-responses \\`, `  -H "Authorization: Bearer $ID_TOKEN" \\`, `  -H "cloudchat-instance: ${selected.instance}"`].join("\n") : "";
  const copy = async () => {
    try {
      await navigator.clipboard.writeText(curl);
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch (error) {
      setCopied(false);
    }
  };
  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 box = "rounded-xl border border-gray-200 dark:border-white/10 p-4";
  return <div className="not-prose flex flex-col gap-4">
      <div className={box}>
        <label className={label} htmlFor="ch-id-token">
          Paste your <code>id_token</code>
        </label>
        <p className={`${muted} mt-1`}>
          The one <code>POST /auth/v1/signin</code> returned. It is decoded in your browser, and
          sent only to <code>api.cloudhumans.com</code> to look up your account names.
        </p>
        <textarea id="ch-id-token" value={raw} onChange={event => {
    setRaw(event.target.value);
    setPicked(null);
  }} spellCheck={false} autoComplete="off" rows={4} placeholder="eyJraWQiOiJhYmMxMjMiLCJhbGciOiJSUzI1NiJ9.eyJlbWFpbCI6..." className="mt-3 w-full resize-y 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 outline-none focus:border-primary" />
        {raw.trim() !== "" && <button type="button" onClick={() => {
    setRaw("");
    setPicked(null);
  }} className={`${muted} mt-2 underline underline-offset-2`}>
            Clear
          </button>}
      </div>

      {result.state === "error" && <div 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 text-red-800 dark:text-red-200">{result.message}</p>
        </div>}

      {result.state === "ok" && <div className={box}>
          <p className={label}>
            {entries.length === 1 ? "This token covers one Cloud Chat account" : `This token covers ${entries.length} Cloud Chat accounts`}
          </p>
          {result.email && <p className={`${muted} mt-1`}>Signed in as {result.email}</p>}
          {expired && <p className="mt-1 text-sm text-amber-700 dark:text-amber-300">
              Heads up: this token expired at {result.expiresAt.toLocaleString()}. The values below
              are still right — sign in again for a token the API will accept.
            </p>}
          {looking && <p className={`${muted} mt-1`}>Looking up account names…</p>}
          {unavailable && <p className={`${muted} mt-1`}>
              Account names are not available right now, so these come from your token: ids only.
              Everything below is still correct.
            </p>}

          {entries.length > 1 && <div className="mt-3">
              <label className={muted} htmlFor="ch-account-pick">
                Pick the account you want to call
              </label>
              <select id="ch-account-pick" value={picked || ""} onChange={event => setPicked(event.target.value)} className="mt-2 w-full rounded-lg border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5 p-2 text-sm text-gray-900 dark:text-gray-100">
                <option value="">Select an account…</option>
                {entries.map(entry => <option key={`${entry.instance}:${entry.account}`} value={`${entry.instance}:${entry.account}`}>
                    {optionLabel(entry)}
                  </option>)}
              </select>
            </div>}

          {selected && <div className="mt-4 flex flex-col gap-3">
              {selected.name && <p className={label}>
                  {selected.name}
                  {selected.role ? <span className={muted}> · {selected.role}</span> : null}
                </p>}
              <div className="grid gap-3 sm:grid-cols-2">
                <div className="rounded-lg bg-gray-50 dark:bg-white/5 p-3">
                  <p className="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
                    cloudchat-instance
                  </p>
                  <p className="mt-1 font-mono text-lg text-gray-900 dark:text-gray-100">
                    {selected.instance}
                  </p>
                </div>
                <div className="rounded-lg bg-gray-50 dark:bg-white/5 p-3">
                  <p className="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
                    accountId
                  </p>
                  <p className="mt-1 font-mono text-lg text-gray-900 dark:text-gray-100">
                    {selected.account}
                  </p>
                </div>
              </div>

              {routable ? <div>
                  <div className="flex items-center justify-between">
                    <p className={muted}>Your first call, ready to run</p>
                    <button type="button" onClick={copy} className="rounded-md border border-gray-200 dark:border-white/10 px-2 py-1 text-xs text-gray-700 dark:text-gray-200">
                      {copied ? "Copied" : "Copy"}
                    </button>
                  </div>
                  <pre className="mt-2 overflow-x-auto rounded-lg bg-gray-900 p-3 text-xs leading-relaxed text-gray-100">
                    <code>{curl}</code>
                  </pre>
                  <p className={`${muted} mt-2`}>
                    Export the token first, so it stays out of your shell history:{" "}
                    <code>read -rs ID_TOKEN && export ID_TOKEN</code>, then paste and press
                    enter.
                  </p>
                </div> : <p className="text-sm text-amber-700 dark:text-amber-300">
                  Instance <code>{selected.instance}</code> is not one the production API routes to
                  — it is an internal instance, so there is no call to generate for this account.
                </p>}
            </div>}
        </div>}
    </div>;
};

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 Cloud Chat API call needs three things: a token, your `cloudchat-instance` header, and one of your account ids. This page gets you all three, and ends with a request you can run as-is.

## Sign in

<SignInFlow />

Some accounts do not get a token on the first try — a temporary password or two-step verification comes back as a **challenge** instead. The form handles that: the second step appears only when there is one, already carrying what the first step returned, so the only thing left to type is the code from the text message.

Writing this into your own client means writing a loop, not a single call. [Signing in is a loop, not a call](/api-reference/cloudchat/overview#signing-in-is-a-loop-not-a-call) on the overview page has the state machine and the three things worth knowing before a real user meets it.

## Find your instance and account

Paste the `id_token` from above. The values are remembered, so they show up on every endpoint page from here on.

<CloudChatInstanceFinder />

<Note>
  **Your password and your token go to `api.cloudhumans.com`, and nowhere else.** Each is sent there over HTTPS — the password once, to get you a token; the token once, to look up your account names. This page makes no other request with either, stores neither, and logs nothing. You can confirm that in your browser's network tab.

  That is also why the token is pasted rather than carried over for you: nothing here is kept, so there is no credential of yours sitting at rest in your browser.

  `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>

## If the values are missing or don't work

| Symptom                                            | What it means                                                                                                                                                                                                            |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Account names never appear, only ids               | The name lookup is unavailable from your browser. Harmless — the ids shown are correct and every call works with them.                                                                                                   |
| The box says the token carries no accounts         | You pasted the `access_token` instead of the `id_token`, or your user has no Cloud Chat access yet.                                                                                                                      |
| An account you expect is missing                   | If the access was granted after you signed in, your token predates it. Sign in again for a fresh one.                                                                                                                    |
| An account you no longer have still appears        | Same cause in reverse: your token predates the change. It disappears once the name lookup answers, or after you sign in again.                                                                                           |
| `400` with `cloudchat-instance header is required` | The header never arrived. It is rejected before authentication, so this is not a token problem.                                                                                                                          |
| `401` on every call                                | Expired or malformed token. Sign in again; `expires_in` in the sign-in response says how long the new one lasts.                                                                                                         |
| `404` with a correct-looking token                 | Right token, wrong pairing — and this is the common one. Either the account id isn't on the instance you sent, or your user has no access to it. Those two answer identically on purpose, so re-check both values above. |
| `403` with a correct-looking token                 | Different problem: you *do* have access and the account is **suspended**. Nothing will work on it until that's resolved.                                                                                                 |
| An account the box won't generate a call for       | The token names an internal instance, which this API does not serve.                                                                                                                                                     |

<Accordion title="Getting these values without this page">
  Both of the box's sources are things you can call yourself.

  **Ask the API** — this one includes account names:

  ```bash theme={null}
  curl https://api.cloudhumans.com/auth/v1/me/cloudchat-accounts \
    -H "Authorization: Bearer $ID_TOKEN"
  ```

  ```json theme={null}
  [{ "instance": "1", "accountId": 1, "accountName": "Cloudhumans" }]
  ```

  **Read the token** — ids only, and no network call. A JWT payload is base64url JSON, so the claim is one command away:

  <CodeGroup>
    ```bash jq theme={null}
    echo "$ID_TOKEN" | cut -d. -f2 \
      | tr '_-' '/+' \
      | base64 -d 2>/dev/null \
      | jq -r '."custom:cloudchat_accounts"'
    ```

    ```python python theme={null}
    import base64, json, os

    payload = os.environ["ID_TOKEN"].split(".")[1]
    payload += "=" * (-len(payload) % 4)
    claims = json.loads(base64.urlsafe_b64decode(payload))
    print(claims["custom:cloudchat_accounts"])
    ```
  </CodeGroup>

  You get something like `1:1:1,1:2:0`, read as `instance:account:role`. The role is a number — `0` agent, `1` administrator, `2` supervisor, `3` assistant, `4` cx\_engineer — and it has no effect on the values you send; it is your permission level inside the account, which Cloud Chat enforces on its own.

  **The endpoint is the authority; the claim is an offline approximation of it.** Cloud Chat authorizes on your live access, which is what the endpoint reads. The claim is a photograph taken when you signed in, so it can still list an account whose access was revoked since, or miss one granted since. That is why the box shows the endpoint's answer whenever it gets one, rather than merging the two — a merged list would offer you accounts your calls would only 404 on.

  Use the claim when you want an answer with no network call, and re-read it after signing in again.

  If you have devtools open you may see the name lookup fail with a CORS error while that endpoint is still rolling out. Nothing is broken — it is why the box falls back to reading your token, and the ids it shows are the ones the API accepts.
</Accordion>
