{
  "openapi": "3.0.1",
  "info": {
    "title": "Authentication API",
    "version": "v1",
    "description": "Single sign-in for every Cloud Humans API."
  },
  "servers": [
    {
      "url": "https://api.cloudhumans.com/auth",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Authentication",
      "description": "Exchange email and password for a token that works across every service."
    }
  ],
  "paths": {
    "/v1/signin": {
      "post": {
        "operationId": "signIn",
        "summary": "Sign in and get a token",
        "description": "Authenticate with email and password and receive the token used by every other API. One token covers Claudia, Cloud Chat and the services that follow — the account you see is derived from it, and the same token is valid against production and staging.\n\n**A `200` does not always carry 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 at `POST /v1/signin/challenge`. Branch on the body, not on the status — `id_token` present means done, `challenge` present means one more step.",
        "tags": [
          "Authentication"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SignInRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Either a token, or a challenge to answer before one is issued.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Token"
                    },
                    {
                      "$ref": "#/components/schemas/Challenge"
                    }
                  ]
                },
                "examples": {
                  "token": {
                    "summary": "Signed in — token issued",
                    "value": {
                      "id_token": "eyJraWQiOiJhYmMxMjMiLCJhbGciOiJSUzI1NiJ9...",
                      "access_token": "eyJraWQiOiJkZWY0NTYiLCJhbGciOiJSUzI1NiJ9...",
                      "expires_in": 3600,
                      "token_type": "Bearer"
                    }
                  },
                  "sms_mfa": {
                    "summary": "Two-step verification — a code was texted to the user",
                    "description": "The account has SMS verification on. A code is already on its way to the phone registered on the account; collect it and post it back with this `session`.",
                    "value": {
                      "challenge": "SMS_MFA",
                      "session": "AYABeF7x..."
                    }
                  },
                  "new_password_required": {
                    "summary": "Temporary password — a new one must be chosen",
                    "description": "First access, or a password that was reset for the user. No code is sent; the user picks a password and it is posted back with this `session`.",
                    "value": {
                      "challenge": "NEW_PASSWORD_REQUIRED",
                      "session": "AYABeF7x..."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Wrong email or password, the account cannot sign in, or the body was missing `email`/`password`. Other `4xx` codes are possible and carry this same envelope, so match on the envelope rather than on a fixed status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many attempts. Wait before trying again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/signin/challenge": {
      "post": {
        "operationId": "respondToSignInChallenge",
        "summary": "Answer a sign-in challenge",
        "description": "Finish a sign-in that came back with a `challenge` instead of a token. Send the `session` from that response together with the answer the challenge asks for — the SMS code, or the new password.\n\nAnswer with the token in mind, not the step: this endpoint returns the **same body shape** as `/v1/signin`, so it can hand back another challenge rather than a token. A user on first access *with* two-step verification really does go `NEW_PASSWORD_REQUIRED` → `SMS_MFA` → token, each step handing you a fresh `session`. Loop until `id_token` shows up.\n\nThe `session` belongs to the sign-in attempt that produced it and is not built to be kept — there is nothing to persist and nothing to resume. Once it stops being accepted, call `/v1/signin` again, which sends a new code.",
        "tags": [
          "Authentication"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChallengeAnswer"
              },
              "examples": {
                "sms_mfa": {
                  "summary": "Answer SMS_MFA with the texted code",
                  "value": {
                    "challenge": "SMS_MFA",
                    "session": "AYABeF7x...",
                    "email": "you@company.com",
                    "code": "123456"
                  }
                },
                "new_password_required": {
                  "summary": "Answer NEW_PASSWORD_REQUIRED with the chosen password",
                  "value": {
                    "challenge": "NEW_PASSWORD_REQUIRED",
                    "session": "AYABeF7x...",
                    "email": "you@company.com",
                    "new_password": "the-new-password"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Either a token, or the next challenge in the chain.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Token"
                    },
                    {
                      "$ref": "#/components/schemas/Challenge"
                    }
                  ]
                },
                "examples": {
                  "token": {
                    "summary": "Challenge cleared — token issued",
                    "value": {
                      "id_token": "eyJraWQiOiJhYmMxMjMiLCJhbGciOiJSUzI1NiJ9...",
                      "access_token": "eyJraWQiOiJkZWY0NTYiLCJhbGciOiJSUzI1NiJ9...",
                      "expires_in": 3600,
                      "token_type": "Bearer"
                    }
                  },
                  "chained_challenge": {
                    "summary": "Cleared, but another step follows",
                    "description": "The new password was accepted and the account also has two-step verification, so a code went out. Note the `session` is a new one — answer with this, not with the one you just used.",
                    "value": {
                      "challenge": "SMS_MFA",
                      "session": "AYABgH2p..."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The answer was rejected, or the request was malformed. The two are told apart by `error`, and the common one is `CodeMismatchException` — see the table in the description of `error` below. Other `4xx` codes are possible and carry this same envelope, so match on the envelope rather than on a fixed status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "code_mismatch": {
                    "summary": "Wrong code, or the session already died",
                    "value": {
                      "error": "CodeMismatchException",
                      "message": "Invalid code or auth state for the user."
                    }
                  },
                  "invalid_password": {
                    "summary": "New password rejected by the password policy",
                    "value": {
                      "error": "InvalidPasswordException",
                      "message": "Password does not conform to policy."
                    }
                  },
                  "missing_field": {
                    "summary": "A required field never arrived",
                    "value": {
                      "error": "AuthError",
                      "message": "code is required for SMS_MFA, and must be a string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many attempts. Wait before trying again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "SignInRequest": {
        "type": "object",
        "required": [
          "email",
          "password"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "The same email you use to sign in to Cloud Chat.",
            "example": "you@company.com"
          },
          "password": {
            "type": "string",
            "format": "password",
            "description": "Your password. It travels only on this call and is never logged.",
            "example": "••••••••"
          }
        }
      },
      "Token": {
        "type": "object",
        "description": "Sign-in completed.",
        "required": [
          "access_token",
          "id_token",
          "expires_in",
          "token_type"
        ],
        "properties": {
          "id_token": {
            "type": "string",
            "description": "Send THIS one as `Authorization: Bearer <id_token>`. It carries the identity the APIs authorize on — your email and the accounts your credentials cover. The access token does not.",
            "example": "eyJraWQiOiJhYmMxMjMiLCJhbGciOiJSUzI1NiJ9..."
          },
          "access_token": {
            "type": "string",
            "description": "Issued by Cognito alongside the id token. Not what the Cloud Humans APIs read.",
            "example": "eyJraWQiOiJkZWY0NTYiLCJhbGciOiJSUzI1NiJ9..."
          },
          "expires_in": {
            "type": "integer",
            "description": "Lifetime in seconds.",
            "example": 3600
          },
          "token_type": {
            "type": "string",
            "description": "Always `Bearer`.",
            "example": "Bearer"
          }
        }
      },
      "Challenge": {
        "type": "object",
        "description": "No token yet — one more step first. Answer it at `POST /v1/signin/challenge`.",
        "required": [
          "challenge",
          "session"
        ],
        "properties": {
          "challenge": {
            "type": "string",
            "description": "What has to be answered.\n\n| Value | Means | You send back |\n|---|---|---|\n| `SMS_MFA` | Two-step verification is on for this account, and a code has just been texted to the phone registered on it. | `code` |\n| `NEW_PASSWORD_REQUIRED` | The password is temporary — first access, or it was reset. Nothing is sent to the user. | `new_password` |\n\nThose two are the ones this API can answer. Cognito has further challenge types and this field passes through whatever it returns, so treat an unrecognised value as \"cannot finish here\" rather than as impossible: an account configured that way — an authenticator app, for instance — has to use the Cloud Chat interface to get a token.",
            "example": "SMS_MFA"
          },
          "session": {
            "type": "string",
            "description": "Identifies this sign-in attempt, and is what proves the next call belongs to it. Pass it back unchanged. Scoped to a single sign-in attempt, and not something to store.",
            "example": "AYABeF7x..."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "The Cognito exception type, or `AuthError` when the request was rejected before reaching Cognito.\n\n| `error` | What it means |\n|---|---|\n| `NotAuthorizedException` | On `/v1/signin`: wrong email or password. On `/v1/signin/challenge`: the `session` is no longer accepted. |\n| `CodeMismatchException` | Wrong SMS code **or** a session that has already expired — Cognito answers identically for both, so a client cannot tell them apart. Offer a retry, and a way back to sign-in for when retrying keeps failing. |\n| `ExpiredCodeException` | The code was right once, but too much time passed. Sign in again to get a fresh one. |\n| `InvalidPasswordException` | The new password does not meet the account's policy. |\n| `AuthError` | The gateway rejected the body: a missing field, or a `code` sent as a number instead of a string. `message` says which. |\n",
            "example": "NotAuthorizedException"
          },
          "message": {
            "type": "string",
            "description": "What went wrong, in words. Written for a developer reading a log, not for an end user — don't put it on screen verbatim.",
            "example": "Incorrect username or password."
          }
        }
      },
      "ChallengeAnswer": {
        "type": "object",
        "description": "Which field carries the answer depends on `challenge`. Sending the wrong one — or leaving it out — is a `400` before the request ever reaches Cognito.",
        "required": [
          "challenge",
          "session",
          "email"
        ],
        "properties": {
          "challenge": {
            "type": "string",
            "enum": [
              "SMS_MFA",
              "NEW_PASSWORD_REQUIRED"
            ],
            "description": "Copy the `challenge` value from the response you are answering.",
            "example": "SMS_MFA"
          },
          "session": {
            "type": "string",
            "description": "The `session` from that same response, unchanged.",
            "example": "AYABeF7x..."
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "The same email that was signed in with. The `session` alone does not identify the user to Cognito.",
            "example": "you@company.com"
          },
          "code": {
            "type": "string",
            "description": "**`SMS_MFA` only.** The code from the text message.\n\nSend it as a **string**, always. Codes can begin with a zero, and `\"012345\"` arriving as the JSON number `12345` is a different code — which is why a number here is rejected outright instead of being coerced into an intermittent \"wrong code\" for one user in ten.",
            "example": "123456"
          },
          "new_password": {
            "type": "string",
            "format": "password",
            "description": "**`NEW_PASSWORD_REQUIRED` only.** The password the user chose. It has to satisfy the account's password policy — if it does not, the call comes back `400 InvalidPasswordException` and the user needs to pick another one. Retry with the same `session`; if that is refused too, the attempt is spent and the way back is `/v1/signin`.",
            "example": "••••••••"
          }
        }
      }
    }
  }
}
