{
  "openapi": "3.1.0",
  "info": {
    "title": "MooverApp Sign-up API",
    "version": "v1",
    "description": "The unauthenticated way into the platform: create an account, watch the provisioning Operation until it is `done`, and continue on the Tenant API with the credentials it hands back.\n\nThese are the only endpoints on the platform that need no bearer token. Everything a site is then made of \u2014 websites, pages, blog, media, settings \u2014 is on the Tenant API."
  },
  "servers": [
    {
      "url": "https://api.moover.app",
      "description": "Production."
    }
  ],
  "tags": [
    {
      "name": "User Register",
      "description": "Creating a MooverApp account without one: `POST users` starts signup and names an Operation whose id is the token itself, `:check_email_availability` says whether an address is free, and the Operation reports each provisioning hop until it is `done`."
    }
  ],
  "x-tagGroups": [
    {
      "name": "Sign-up",
      "tags": [
        "User Register"
      ]
    }
  ],
  "paths": {
    "/site-api/v1/operations/{operation}": {
      "get": {
        "operationId": "siteOperationGetHandler",
        "summary": "Get operation",
        "description": "Polls the provisioning Operation that signup started (\u00a717). Its id is the `signup_token` the Create handed back, so the poll authenticates itself and needs no bearer token.\n\nPoll until `done` is true, then read exactly one of `response` / `error`. `metadata.step` reports which hop of the provisioning is running, which is what a progress bar shows.",
        "tags": [
          "User Register"
        ],
        "x-method-kind": "GET",
        "parameters": [
          {
            "name": "operation",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "6f3b1c8e2a4d"
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "description": "A matching etag answers 304 with no body (\u00a716).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The resource.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true,
                  "description": "An Operation (\u00a717) whose id is the signup token itself, so the poll authenticates itself. `metadata.step` reports the provisioning hop."
                }
              }
            }
          },
          "304": {
            "description": "Not modified."
          },
          "404": {
            "description": "NOT_FOUND \u2014 absent, or present but invisible to this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Status"
                }
              }
            }
          },
          "500": {
            "description": "INTERNAL \u2014 carries REQUEST_INFO with the reference id from the log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Status"
                }
              }
            }
          }
        }
      }
    },
    "/site-api/v1/users": {
      "post": {
        "operationId": "siteUserCreateHandler",
        "summary": "Create user",
        "description": "A MooverApp account, seen from the one place where somebody with no account can create one. Creating it also creates the account and starts provisioning the first website, so the response carries three things the user resource itself does not have: the `account_id`, a one-time `signup_token` to exchange for a bearer JWT, and the `operation` to poll until the site is up.",
        "tags": [
          "User Register"
        ],
        "x-method-kind": "CREATE",
        "responses": {
          "201": {
            "description": "The created resource.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SignupUser"
                }
              }
            },
            "headers": {
              "Location": {
                "description": "URL of the created resource (D4).",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_ARGUMENT, FAILED_PRECONDITION or OUT_OF_RANGE.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Status"
                }
              }
            }
          },
          "409": {
            "description": "ALREADY_EXISTS, or ABORTED when an `If-Match` etag no longer matches.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Status"
                }
              }
            }
          },
          "500": {
            "description": "INTERNAL \u2014 carries REQUEST_INFO with the reference id from the log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Status"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SignupUserCreate"
              }
            }
          }
        }
      }
    },
    "/site-api/v1/users:check_email_availability": {
      "post": {
        "operationId": "siteUserCheckEmailAvailabilityHandler",
        "summary": "Check email availability (users)",
        "description": "Says whether an address can still be registered, so a signup form can tell someone before they fill the rest of it in. Send `{\"email\": \"...\"}`. It is a question, not a reservation: the answer can go stale, and the Create is what actually decides.",
        "tags": [
          "User Register"
        ],
        "x-method-kind": "CUSTOM",
        "responses": {
          "202": {
            "description": "An Operation (\u00a717), when the work outlives the call.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "headers": {
              "Location": {
                "description": "URL of the Operation.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "200": {
            "description": "The affected resource, or the method\u2019s own response object (\u00a710).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true,
                  "description": "The answer of `:check_email_availability` \u2014 the affected resource, or the method\u2019s own response object (\u00a710). A custom method\u2019s answer is not the resource, so it declares no ResourceSpec."
                }
              }
            }
          },
          "400": {
            "description": "INVALID_ARGUMENT, FAILED_PRECONDITION or OUT_OF_RANGE.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Status"
                }
              }
            }
          },
          "404": {
            "description": "NOT_FOUND \u2014 absent, or present but invisible to this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Status"
                }
              }
            }
          },
          "500": {
            "description": "INTERNAL \u2014 carries REQUEST_INFO with the reference id from the log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Status"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Arguments of `:check_email_availability`. May carry `request_id` (\u00a715) when the method provisions, sends mail or is otherwise not repeatable."
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Status": {
        "type": "object",
        "description": "The error body of every non-2xx response on every surface (\u00a713).",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "description": "The one and only member of an error body \u2014 there is never a partial resource beside it.",
            "required": [
              "code",
              "status",
              "message"
            ],
            "properties": {
              "code": {
                "type": "integer",
                "description": "The HTTP status."
              },
              "status": {
                "type": "string",
                "description": "The canonical status.",
                "enum": [
                  "INVALID_ARGUMENT",
                  "FAILED_PRECONDITION",
                  "OUT_OF_RANGE",
                  "UNAUTHENTICATED",
                  "PERMISSION_DENIED",
                  "NOT_FOUND",
                  "ALREADY_EXISTS",
                  "ABORTED",
                  "RESOURCE_EXHAUSTED",
                  "CANCELLED",
                  "INTERNAL",
                  "NOT_IMPLEMENTED",
                  "UNAVAILABLE",
                  "DEADLINE_EXCEEDED"
                ]
              },
              "message": {
                "type": "string",
                "description": "For a developer reading a log. Never internal detail."
              },
              "details": {
                "type": "array",
                "description": "Structured detail, of the closed set of types below.",
                "items": {
                  "type": "object",
                  "required": [
                    "type"
                  ],
                  "properties": {
                    "type": {
                      "type": "string",
                      "description": "Which kind of detail this is.",
                      "enum": [
                        "BAD_REQUEST",
                        "PRECONDITION_FAILURE",
                        "QUOTA_FAILURE",
                        "RESOURCE_INFO",
                        "RETRY_INFO",
                        "REQUEST_INFO",
                        "LOCALIZED_MESSAGE"
                      ]
                    },
                    "field_violations": {
                      "type": "array",
                      "description": "On a BAD_REQUEST: one entry per field the request got wrong.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "field": {
                            "type": "string",
                            "description": "Field path in the REQUEST body."
                          },
                          "description": {
                            "type": "string",
                            "description": "What is wrong with that field."
                          }
                        }
                      }
                    }
                  },
                  "additionalProperties": true
                }
              }
            }
          }
        }
      },
      "SignupUser": {
        "type": "object",
        "description": "A MooverApp account, seen from the one place where somebody with no account can create one. Creating it also creates the account and starts provisioning the first website, so the response carries three things the user resource itself does not have: the `account_id`, a one-time `signup_token` to exchange for a bearer JWT, and the `operation` to poll until the site is up.",
        "properties": {
          "name": {
            "type": "string",
            "description": "The resource name (\u00a75) \u2014 the identity of this resource, and the path that addresses it: `websites/{website}/cms_pages/{cms_page}`. It is the handle to store, not a human label.",
            "readOnly": true,
            "x-field-behavior": "OUTPUT_ONLY",
            "examples": [
              "users/6f3b1c8e2a4d"
            ]
          },
          "id": {
            "type": "string",
            "description": "The last segment of `name` on its own, for a client that needs to build a child URL. Unique inside its parent, never across the platform.",
            "readOnly": true,
            "x-field-behavior": "OUTPUT_ONLY",
            "examples": [
              "6f3b1c8e2a4d"
            ]
          },
          "create_time": {
            "type": "string",
            "format": "date-time",
            "description": "When the resource was created (RFC 3339, UTC).",
            "readOnly": true,
            "x-field-behavior": "OUTPUT_ONLY",
            "x-filterable": true,
            "x-sortable": true,
            "examples": [
              "2026-01-15T09:30:00Z"
            ]
          },
          "update_time": {
            "type": "string",
            "format": "date-time",
            "description": "When the resource last changed (RFC 3339, UTC).",
            "readOnly": true,
            "x-field-behavior": "OUTPUT_ONLY",
            "examples": [
              "2026-01-15T09:30:00Z"
            ]
          },
          "etag": {
            "type": "string",
            "description": "Version of the persisted state. Send it back in `If-Match` on Update or Delete and the write is rejected with ABORTED if anyone else changed the resource meanwhile (\u00a714).",
            "readOnly": true,
            "x-field-behavior": "OUTPUT_ONLY",
            "examples": [
              "\"3f8a1b2c4d5e\""
            ]
          },
          "email": {
            "type": "string",
            "description": "The address the new account signs in with, and where the platform writes to it. Must not already be registered \u2014 `users:check_email_availability` says so before the form is submitted.",
            "x-field-behavior": "REQUIRED",
            "examples": [
              "ada@example.com"
            ]
          },
          "is_active": {
            "type": "boolean",
            "description": "Whether the new account is usable. It is true on a successful signup.",
            "readOnly": true,
            "x-field-behavior": "OUTPUT_ONLY"
          },
          "account_id": {
            "type": "string",
            "description": "The account created alongside the user \u2014 the one the new website hangs off on the Tenant API.",
            "readOnly": true,
            "x-field-behavior": "OUTPUT_ONLY"
          },
          "signup_token": {
            "type": "string",
            "description": "A one-time credential to trade for a bearer JWT at `POST /tenant-api/v1/users:exchange_handoff`, so a freshly signed-up user does not have to type the password they just chose.",
            "readOnly": true,
            "x-field-behavior": "OUTPUT_ONLY"
          },
          "operation": {
            "type": "string",
            "description": "The name of the provisioning Operation to poll (\u00a717). The account exists as soon as this call returns; the website is not servable until that Operation is `done`.",
            "readOnly": true,
            "x-field-behavior": "OUTPUT_ONLY",
            "examples": [
              "operations/8f2c1ab4d09e"
            ]
          }
        },
        "required": [
          "name",
          "id"
        ],
        "examples": [
          {
            "name": "users/6f3b1c8e2a4d",
            "id": "6f3b1c8e2a4d",
            "create_time": "2026-01-15T09:30:00Z",
            "update_time": "2026-01-15T09:30:00Z",
            "etag": "\"3f8a1b2c4d5e\"",
            "email": "ada@example.com",
            "is_active": true,
            "account_id": "string",
            "signup_token": "string",
            "operation": "operations/8f2c1ab4d09e"
          }
        ]
      },
      "SignupUserCreate": {
        "type": "object",
        "description": "The `users` fields a client may set on create.",
        "properties": {
          "email": {
            "type": "string",
            "description": "The address the new account signs in with, and where the platform writes to it. Must not already be registered \u2014 `users:check_email_availability` says so before the form is submitted.",
            "x-field-behavior": "REQUIRED",
            "examples": [
              "ada@example.com"
            ]
          },
          "password": {
            "type": "string",
            "description": "The password for the new account. Write-only: it is applied and never echoed back.",
            "writeOnly": true,
            "x-field-behavior": "INPUT_ONLY"
          },
          "website_name": {
            "type": "string",
            "description": "What to call the first website. It names the account, seeds the site's own name and is the slug the platform hostname is built from. Not a field of the user, so it never comes back \u2014 read it from the website on the Tenant API.",
            "writeOnly": true,
            "x-field-behavior": "INPUT_ONLY",
            "examples": [
              "Northwind Store"
            ]
          },
          "website_type": {
            "type": "string",
            "description": "Which kind of site to provision, which decides the content it is seeded with. Optional; the platform picks a default.",
            "writeOnly": true,
            "x-field-behavior": "INPUT_ONLY"
          },
          "default_language": {
            "type": "string",
            "description": "The language the first view is created in. It becomes the `general.locale.default_language` setting of the new website.",
            "writeOnly": true,
            "x-field-behavior": "INPUT_ONLY",
            "examples": [
              "en"
            ]
          },
          "recaptcha_token": {
            "type": "string",
            "description": "A CAPTCHA proof, when the deployment asks for one. This is the only unauthenticated write on the platform, so it is also the only one that needs it.",
            "writeOnly": true,
            "x-field-behavior": "INPUT_ONLY"
          },
          "request_id": {
            "type": "string",
            "description": "A client-chosen idempotency key (\u00a715). Retrying a signup that timed out with the same value returns the first attempt's result instead of provisioning a second site.",
            "writeOnly": true,
            "x-field-behavior": "INPUT_ONLY"
          }
        },
        "required": [
          "email"
        ],
        "examples": [
          {
            "email": "ada@example.com"
          }
        ]
      },
      "SignupUserUpdate": {
        "type": "object",
        "description": "The `users` fields to merge. Fields absent from the body are left untouched.",
        "properties": {
          "email": {
            "type": "string",
            "description": "The address the new account signs in with, and where the platform writes to it. Must not already be registered \u2014 `users:check_email_availability` says so before the form is submitted.",
            "x-field-behavior": "REQUIRED",
            "examples": [
              "ada@example.com"
            ]
          },
          "password": {
            "type": "string",
            "description": "The password for the new account. Write-only: it is applied and never echoed back.",
            "writeOnly": true,
            "x-field-behavior": "INPUT_ONLY"
          },
          "website_name": {
            "type": "string",
            "description": "What to call the first website. It names the account, seeds the site's own name and is the slug the platform hostname is built from. Not a field of the user, so it never comes back \u2014 read it from the website on the Tenant API.",
            "writeOnly": true,
            "x-field-behavior": "INPUT_ONLY",
            "examples": [
              "Northwind Store"
            ]
          },
          "website_type": {
            "type": "string",
            "description": "Which kind of site to provision, which decides the content it is seeded with. Optional; the platform picks a default.",
            "writeOnly": true,
            "x-field-behavior": "INPUT_ONLY"
          },
          "default_language": {
            "type": "string",
            "description": "The language the first view is created in. It becomes the `general.locale.default_language` setting of the new website.",
            "writeOnly": true,
            "x-field-behavior": "INPUT_ONLY",
            "examples": [
              "en"
            ]
          },
          "recaptcha_token": {
            "type": "string",
            "description": "A CAPTCHA proof, when the deployment asks for one. This is the only unauthenticated write on the platform, so it is also the only one that needs it.",
            "writeOnly": true,
            "x-field-behavior": "INPUT_ONLY"
          },
          "request_id": {
            "type": "string",
            "description": "A client-chosen idempotency key (\u00a715). Retrying a signup that timed out with the same value returns the first attempt's result instead of provisioning a second site.",
            "writeOnly": true,
            "x-field-behavior": "INPUT_ONLY"
          }
        },
        "examples": [
          {
            "email": "ada@example.com"
          }
        ]
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "\u00a719: `Authorization: Bearer <jwt>`. No API keys, no tokens in query strings, no session cookies."
      }
    }
  },
  "x-public-surface": "/site-api",
  "x-generator": {
    "source": "cmake/tools/extract_openapi_endpoints.py",
    "total_operations": 3,
    "total_paths": 3,
    "warnings": []
  }
}
