{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "urn:cede:schema:account:v0",
  "title": "CEDE account, v0",
  "description": "A self-service account on the Cede platform, and — in the one response that creates it — the API key that comes with it. Returned by POST /signup (which needs no credential, because it hands out the first one) and by GET /account (which reads the account the presented key belongs to). An account is an identity for usage metering and for ownership of the objects and jobs its keys create. It is descriptive only: holding an account makes its holder party to nothing, and Cede is party to nothing on its behalf (SPEC.md section 4).",
  "type": "object",
  "additionalProperties": false,
  "required": ["schema_version", "id", "created_at", "plan"],
  "properties": {
    "schema_version": {
      "type": "string",
      "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
      "description": "Version of this account schema the payload conforms to."
    },
    "id": {
      "type": "string",
      "pattern": "^acct_[0-9a-f]{24}$",
      "description": "Server-assigned identity of the account. Random, not derived from anything the holder supplied, so it can appear in logs and support threads without revealing a credential."
    },
    "created_at": {
      "type": "string",
      "format": "date-time",
      "description": "When the account was created, RFC 3339 in UTC."
    },
    "plan": {
      "enum": ["sandbox"],
      "description": "The commercial plan (SPEC.md section 6). Every self-service account starts on the free sandbox, which is sufficient to integrate against without payment. Further plans arrive by schema version bump, never by silent widening."
    },
    "billing": {
      "type": "object",
      "additionalProperties": false,
      "required": ["plan", "status", "mode", "free_units_remaining"],
      "description": "The account's commercial state (SPEC.md section 6). Cede holds no money and no card: what is here is the account's counterpart at the payment processor and how much of the free sandbox is left. Usage itself is at GET /usage.",
      "properties": {
        "plan": {
          "enum": ["sandbox"],
          "description": "The plan this account is billed under. The same value as 'plan' above, repeated here so a client reading the billing block reads a complete statement."
        },
        "status": {
          "enum": ["active", "cancelled"],
          "description": "Whether the account is still metered. 'cancelled' after DELETE /subscription: no further usage is recorded and the billable verbs refuse with 402 account_cancelled."
        },
        "mode": {
          "enum": ["test", "dark"],
          "description": "'test' when this deployment is configured against Stripe test mode, 'dark' when it holds no billing credential at all — in which case usage is still metered and readable, and nothing leaves the deployment. There is no third value: Cede is in test mode until the operator flips the real-money switch (SPEC.md section 6), and a live credential is refused at start-up."
        },
        "customer_id": {
          "type": "string",
          "minLength": 1,
          "description": "The account's customer identity at the payment processor. Absent when billing is dark, or when the processor could not be reached — the account exists either way, and this is not invented."
        },
        "free_units_remaining": {
          "type": "integer",
          "minimum": 0,
          "description": "Units left in the free sandbox allowance (SPEC.md section 6)."
        }
      }
    },
    "label": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200,
      "description": "Optional free text the holder chose to recognise this account by. Never interpreted by Cede."
    },
    "api_key": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "secret", "created_at", "scheme", "expires_at"],
      "description": "Present in exactly one response: the signup call that created this key. The secret is returned here and nowhere else — Cede keeps only its digest, so no later call and no operator can recover it. Store it when you receive it; if it is lost, sign up again. The key does not expire: 'expires_at' is null and this build issues no other value.",
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^key_[0-9a-f]{16}$",
          "description": "Public identity of the key: a digest prefix, one-way, safe to log. This is what owns the objects and jobs the key creates."
        },
        "secret": {
          "type": "string",
          "pattern": "^cede_sk_[A-Za-z0-9_-]{40,}$",
          "description": "The credential itself. Send it as 'Authorization: Bearer <secret>' on every request outside GET /health and POST /signup. Treat it as a password: anything holding it is this account."
        },
        "created_at": {
          "type": "string",
          "format": "date-time",
          "description": "When the key was issued, RFC 3339 in UTC."
        },
        "scheme": {
          "const": "Bearer",
          "description": "The HTTP authorization scheme this credential is sent under, stated so an integrator does not have to infer it."
        },
        "expires_at": {
          "type": ["string", "null"],
          "format": "date-time",
          "description": "When this key stops authenticating, RFC 3339 in UTC — or null, meaning never, which is what Cede issues. Keys are not aged out: there is no time-to-live on the authentication path, and with a database configured a key survives a restart of the deployment that issued it. Stated as a field rather than left to prose because an integration script that started answering 401 has to be able to rule expiry out without asking anyone. Required and explicitly null rather than omitted: a build that ever did issue a short-lived key could then not do it silently."
        }
      }
    }
  }
}
