{
  "openapi": "3.1.0",
  "info": {
    "title": "clx management API",
    "version": "0.8.0",
    "description": "The /v1 API of clx (docs/spec.md §15). The clx.cx page is a thin client over the same endpoints.\nAuthorization: `Bearer <access token>` from a signed-in page, or `Bearer clx_…` — an API key\n(one on the `free` plan, up to 5 on `api`). Every POST, PUT, PATCH and DELETE takes an `Idempotency-Key` header, required\nwith an API key; a repeat with the same body replays the first answer for 24 hours.\nErrors: `{ \"error\": { \"code\", \"message\", \"details\"? } }`. This file grows with each stage; a\ntest checks that every route in the code is here and back.\n"
  },
  "servers": [
    {
      "url": "https://clx.cx"
    }
  ],
  "security": [
    {
      "bearer": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer"
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 200
        }
      },
      "Id": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable codes: unauthorized, invalid_request, not_found, payload_too_large,\nlimit_reached, service_full, idempotency_conflict, operation_in_progress,\nscope_required, session_required, key_already_issued,\naccount_taken, already_connected, account_not_ready, invalid_bootstrap,\nbootstrap_permission_error, permission_catalog, orphan_not_deleted,\ntoken_rights_mismatch, token_check_failed, renew_lost, rate_limited, cloudflare_unavailable,\nnot_configured, internal. In an account's or operation's `error` (the install, §4):\nname_taken, resource_drift, cron_limit, permission_error, revoked, self_check_timeout,\ncredentials_missing, credentials_unreadable (clx cannot open the stored token),\ncloudflare_error. Sites: zone_not_found, site_exists, route_conflict, site_not_active,\nroute_not_ours (a route clx made was changed outside clx and left in place).\nSign-up: email_unconfirmed (POST /v1/accounts before the address is confirmed),\ninvalid_password (DELETE /v1/me).\nLinks: link_host_required (set the account's link host first), link_exists (the\ncode is taken in that account).\n"
              },
              "message": {
                "type": "string"
              },
              "details": {
                "type": "object"
              }
            }
          }
        }
      },
      "Key": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "prefix": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "accounts",
                "sites",
                "links",
                "reports"
              ]
            }
          },
          "allow_accounts": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            }
          },
          "allow_ips": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_used": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          }
        }
      },
      "Account": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "cf_account_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "pending",
              "bootstrap_lost",
              "connected",
              "installing",
              "ready",
              "permission_error",
              "revoked",
              "resource_drift",
              "no_connection"
            ],
            "description": "pending — the connect is running, or stopped on a passing failure (5xx other than token_rights_mismatch, token_check_failed, permission_catalog, which end it) and waits for the same request again (same Idempotency-Key); bootstrap_lost — a connect left pending for an hour, or one that died before clx stored its working token: revoke the token its error names and connect again; connected — token ready, clx-edge not installed; installing — the install runs; ready — installed and serving, go on (the worker confirms itself on its first cron: meanwhile `operation` shows the install at step selfcheck, for up to 15 minutes, and if no confirmation comes error.warnings says not_confirmed — nothing to wait for either way); no_connection — installed, but no push from the worker for 26 hours (sites can still be added; its next push makes it ready again)"
          },
          "token": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "name": {
                "type": "string",
                "description": "clx-<operation id>"
              },
              "expires_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "error": {
            "type": [
              "object",
              "null"
            ],
            "description": "code and details, or warnings such as bootstrap_not_deleted"
          },
          "edge": {
            "type": [
              "object",
              "null"
            ],
            "description": "The installed clx-edge, from its upload on (the worker's own confirmation is not needed)",
            "properties": {
              "bundle": {
                "type": "string",
                "description": "sha256 of the bundle"
              },
              "up_to_date": {
                "type": "boolean"
              },
              "schema": {
                "type": "integer"
              },
              "installed_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "push": {
            "type": [
              "object",
              "null"
            ],
            "description": "The worker's own report from its last push of totals (hourly)",
            "properties": {
              "at": {
                "type": "string",
                "format": "date-time"
              },
              "bundle": {
                "type": "string",
                "description": "sha256 of the bundle the worker runs"
              },
              "schema": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "queue": {
                "type": "integer",
                "description": "queue parts and final days waiting to be sent"
              },
              "error": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "the worker's last error"
              },
              "dropped": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                },
                "description": "items dropped unsent, by reason: budget, invalid, unknown_target, too_old, busy, expired"
              }
            }
          },
          "advice": {
            "type": [
              "object",
              "null"
            ],
            "description": "When to move this Cloudflare account to Workers Paid, from its measured use over the last 7 complete UTC days; recomputed daily, null until first computed",
            "properties": {
              "level": {
                "type": "string",
                "enum": [
                  "ok",
                  "watch",
                  "upgrade_soon",
                  "over"
                ],
                "description": "the worst metric: over — a limit was reached (or the database is at backpressure, 400 MB); upgrade_soon — busiest day ≥ 80% or the limit within 7 days; watch — ≥ 60% or within 30 days"
              },
              "metric": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "requests",
                  "writes",
                  "reads",
                  "size",
                  null
                ]
              },
              "metrics": {
                "type": "object",
                "description": "requests, writes, reads — per day for the whole account, against 100,000 / 100,000 / 5,000,000; size — clx-edge now, against 500 MB",
                "additionalProperties": {
                  "type": "object",
                  "properties": {
                    "level": {
                      "type": "string",
                      "enum": [
                        "ok",
                        "watch",
                        "upgrade_soon",
                        "over"
                      ]
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "busiest": {
                      "type": "object",
                      "properties": {
                        "day": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date"
                        },
                        "value": {
                          "type": "integer"
                        }
                      }
                    },
                    "forecast": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date",
                      "description": "the day the trend reaches the limit, when within 30 days"
                    }
                  }
                }
              },
              "unavailable": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "analytics",
                    "size"
                  ]
                },
                "description": "what could not be read — not guessed"
              },
              "suggest": {
                "type": "string",
                "enum": [
                  "shorter_hourly_retention"
                ],
                "description": "size alone is the problem"
              },
              "at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "operation": {
            "type": [
              "object",
              "null"
            ],
            "description": "The latest operation on the account (GET /v1/accounts/{id} only)",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "connect",
                  "renew",
                  "install",
                  "update",
                  "disconnect"
                ]
              },
              "state": {
                "type": "string",
                "enum": [
                  "running",
                  "done",
                  "failed"
                ]
              },
              "step": {
                "type": "string"
              },
              "error": {
                "type": [
                  "object",
                  "null"
                ]
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "link_host": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/LinkHost"
              },
              {
                "type": "null"
              }
            ],
            "description": "The account's link host (GET /v1/accounts/{id} only)"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "LinkHost": {
        "type": "object",
        "properties": {
          "host": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "route_pending",
              "active",
              "route_conflict"
            ],
            "description": "route_pending — the route `<host>/*` is still to be made (the cron retries); route_conflict — another worker holds it, see error"
          },
          "config": {
            "type": "string",
            "enum": [
              "pending",
              "synced"
            ]
          },
          "error": {
            "type": [
              "object",
              "null"
            ]
          }
        }
      },
      "Rule": {
        "type": "object",
        "required": [
          "url"
        ],
        "description": "matches when the visitor's country is in countries (if given) and their device in devices (if given)",
        "properties": {
          "countries": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^[A-Z]{2}$"
            }
          },
          "devices": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "mobile",
                "desktop"
              ]
            }
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "https:// only, up to 2048 characters, not on the link host"
          }
        }
      },
      "Link": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "account_id": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "short_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "https://<link host>/<code>"
          },
          "url": {
            "type": "string",
            "description": "where it leads when no rule matches"
          },
          "rules": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "$ref": "#/components/schemas/Rule"
            },
            "description": "the first match wins"
          },
          "state": {
            "type": "string",
            "enum": [
              "active",
              "deleted"
            ]
          },
          "config": {
            "type": "string",
            "enum": [
              "pending",
              "synced"
            ],
            "description": "whether the worker has this version of the link yet"
          },
          "clicks_7d": {
            "type": "integer",
            "description": "clicks of the last 7 closed UTC days (GET /v1/links only, and only with the reports scope)"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Site": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "account_id": {
            "type": "string"
          },
          "host": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "route_pending",
              "active",
              "route_conflict",
              "deleted"
            ],
            "description": "route_pending — the route is still to be made (the cron retries); route_conflict — another worker holds the pattern, see error"
          },
          "path": {
            "type": "string",
            "description": "the per-site path the counter answers under, e.g. /lumora or /kavi/tesomu"
          },
          "snippet": {
            "type": [
              "object",
              "null"
            ],
            "description": "Embed one of the two at build time; stable until rotate",
            "properties": {
              "inline": {
                "type": "string",
                "description": "<script>…</script>, under 600 bytes"
              },
              "script_tag": {
                "type": "string",
                "description": "<script src=\"/<path>/<s>.js\"> with defer (the order of the attributes varies by site)"
              }
            }
          },
          "excluded_paths": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "config": {
            "type": "string",
            "enum": [
              "pending",
              "synced"
            ],
            "description": "whether the worker has this version of the site yet"
          },
          "retiring": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "path": {
                  "type": "string"
                },
                "until": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            },
            "description": "old paths still counted after a rotation"
          },
          "error": {
            "type": [
              "object",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Top": {
        "type": "array",
        "maxItems": 10,
        "description": "the top 10 by count; \"(other)\" holds what went over the detail caps",
        "items": {
          "type": "object",
          "properties": {
            "key": {
              "type": "string"
            },
            "n": {
              "type": "integer"
            }
          }
        }
      },
      "Report": {
        "type": "object",
        "properties": {
          "period": {
            "type": "string",
            "enum": [
              "today",
              "7d",
              "30d"
            ]
          },
          "from": {
            "type": "string",
            "format": "date",
            "description": "first UTC day"
          },
          "to": {
            "type": "string",
            "format": "date",
            "description": "last UTC day — today"
          },
          "as_of": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "today is counted up to this moment. A site: the end of the latest closed hour the worker sent (hours arrive up to an hour late), null before its first push. A link: the moment of the live read of the account's database"
          },
          "totals": {
            "type": "object",
            "properties": {
              "views": {
                "type": "integer"
              },
              "bots": {
                "type": "integer"
              },
              "visitors": {
                "type": "integer",
                "description": "the sum of daily unique visitors"
              }
            }
          },
          "series": {
            "type": "array",
            "description": "today — one point per hour up to as_of; 7d/30d — one per day (with visitors)",
            "items": {
              "type": "object",
              "properties": {
                "at": {
                  "type": "string",
                  "format": "date-time"
                },
                "views": {
                  "type": "integer"
                },
                "bots": {
                  "type": "integer"
                },
                "visitors": {
                  "type": "integer"
                }
              }
            }
          },
          "incomplete": {
            "type": "boolean",
            "description": "today's hours were refused over the account's write budget; daily totals stay exact"
          },
          "breakdowns": {
            "type": [
              "object",
              "null"
            ],
            "description": "only with breakdowns=1; read from the account's own database, null when unavailable",
            "properties": {
              "pages": {
                "$ref": "#/components/schemas/Top"
              },
              "sources": {
                "$ref": "#/components/schemas/Top"
              },
              "countries": {
                "$ref": "#/components/schemas/Top"
              },
              "devices": {
                "$ref": "#/components/schemas/Top"
              },
              "browsers": {
                "$ref": "#/components/schemas/Top"
              },
              "os": {
                "$ref": "#/components/schemas/Top"
              },
              "bots": {
                "$ref": "#/components/schemas/Top"
              }
            }
          },
          "unavailable": {
            "type": "string",
            "enum": [
              "not_installed",
              "cloudflare",
              "rate_limited"
            ],
            "description": "why breakdowns (and a link's today) are not there: the worker is not installed or the token is gone; Cloudflare failed or took over 5 s; over 30 reads of the account databases a minute for this clx user (each cached 5 minutes)"
          }
        }
      }
    },
    "responses": {
      "Error": {
        "description": "An error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/v1/me": {
      "get": {
        "summary": "The caller — user, plan, limits, current use",
        "description": "Sign-up, e-mail confirmation and password reset are not part of /v1: they live under /auth\nwith the page session (docs/spec.md §9). Until `email_confirmed`, POST /v1/accounts answers\n`email_unconfirmed`.\n",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "email": {
                          "type": "string"
                        },
                        "email_confirmed": {
                          "type": "boolean"
                        }
                      }
                    },
                    "plan": {
                      "type": "string",
                      "enum": [
                        "free",
                        "api"
                      ]
                    },
                    "limits": {
                      "type": "object"
                    },
                    "use": {
                      "type": "object"
                    },
                    "via": {
                      "type": "string",
                      "enum": [
                        "session",
                        "key"
                      ]
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "summary": "Delete the account (page session only, with the password)",
        "description": "Every connected Cloudflare account is disconnected first (clx-edge, its database and routes\nremoved where the token still can — what could not be is listed in `left`, the working tokens\nto revoke in `revoke_tokens`); then the totals go at once, and the user with the keys, sites\nand links.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "password"
                ],
                "properties": {
                  "password": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "left": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "revoke_tokens": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/keys": {
      "get": {
        "summary": "API keys (page session only)",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "keys": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Key"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "summary": "Issue an API key (page session only; 1 on the free plan, 5 on api — `limit_reached` over it); the key is in this answer only",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "scopes"
                ],
                "properties": {
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "accounts",
                        "sites",
                        "links",
                        "reports"
                      ]
                    }
                  },
                  "allow_accounts": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "pattern": "^[0-9a-f]{32}$"
                    }
                  },
                  "allow_ips": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Issued — `key` is shown once"
          },
          "409": {
            "description": "key_already_issued — a replay of the same Idempotency-Key"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/keys/{id}": {
      "delete": {
        "summary": "Revoke an API key (page session only)",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/accounts": {
      "get": {
        "summary": "Connected Cloudflare accounts (scope accounts or sites)",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "accounts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Account"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "summary": "Connect a Cloudflare account with a bootstrap token (scope accounts)",
        "description": "The bootstrap token (Account Settings — Read + Account API Tokens — Edit, which the API names\nWrite; one day) is used\nwithin this request and deleted; it is never stored. The account then goes on to the\ninstall of clx-edge (§4) — poll GET for its state: `installing`, then `ready` once the script,\nits database and cron are in place (seconds; the worker's own confirmation may follow\nminutes later, or never — then `error.warnings` says `not_confirmed`), or `connected` with an `error`.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cf_account_id",
                  "bootstrap_token"
                ],
                "properties": {
                  "cf_account_id": {
                    "type": "string",
                    "pattern": "^[0-9a-f]{32}$"
                  },
                  "bootstrap_token": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Connected",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "account": {
                      "$ref": "#/components/schemas/Account"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/accounts/{id}": {
      "get": {
        "summary": "One connected account (scope accounts or sites), with its latest operation and link host",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "account": {
                      "$ref": "#/components/schemas/Account"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "summary": "Disconnect (scope accounts) — delete clx-edge and its database, then the record",
        "description": "The answer names the working token to revoke in Cloudflare (it cannot delete itself), and in\n`left` whatever clx could not delete — the token is revoked, a right is missing, or the\nworker was changed outside clx — for the user to remove by hand.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "Disconnected",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "revoke_token": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "left": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/accounts/{id}/zones": {
      "get": {
        "summary": "The account's zones (scope accounts or sites) — to pick a site's or link host's zone from",
        "description": "Read live from Cloudflare with the working token, sorted by name; at most 1,000, `truncated`\npast that. A host is still checked against its zone when it is added.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "zones": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "description": "Cloudflare's zone status: active, pending, …"
                          }
                        }
                      }
                    },
                    "truncated": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/accounts/{id}/install": {
      "post": {
        "summary": "Install clx-edge again (scope accounts) — after a failed install, or to reinstall",
        "description": "The recorded database is kept; the worker gets a new key (the previous one is accepted for a\nday). A clx-edge worker or database clx did not create is never touched (`name_taken`), nor\na worker changed outside clx (`resource_drift`). Poll GET for the state.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "202": {
            "description": "Started",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "account": {
                      "$ref": "#/components/schemas/Account"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/accounts/{id}/link-host": {
      "put": {
        "summary": "Set the account's link host (scope sites) — a host in one of its zones, routed whole to clx-edge",
        "description": "The account must be `ready`. The host needs a proxied DNS record — clx does not create it\n(§13 item 2): e.g. `AAAA <host> 100::`, proxied. Setting another host replaces this one: its\nroute is deleted and the links move to the new host with their codes.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "host"
                ],
                "properties": {
                  "host": {
                    "type": "string",
                    "description": "go.example.com — a host of a zone in that account"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Set",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "link_host": {
                      "$ref": "#/components/schemas/LinkHost"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/accounts/{id}/token": {
      "post": {
        "summary": "Renew the working token with a new bootstrap token (scope accounts)",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "bootstrap_token"
                ],
                "properties": {
                  "bootstrap_token": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Renewed"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/sites": {
      "get": {
        "summary": "Sites (scope sites); `account_id` narrows to one account",
        "parameters": [
          {
            "name": "account_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sites": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Site"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "summary": "Add a site (scope sites) — the zone is found in the account, the route made, the config synced",
        "description": "The account must be `ready`. The answer carries the snippet at once; `config` turns\n`synced` once the worker has the site (seconds), and the counter answers from then on.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "account_id",
                  "host"
                ],
                "properties": {
                  "account_id": {
                    "type": "string"
                  },
                  "host": {
                    "type": "string",
                    "description": "example.com or a subdomain of a zone in that account"
                  },
                  "excluded_paths": {
                    "type": "array",
                    "maxItems": 50,
                    "items": {
                      "type": "string",
                      "pattern": "^/"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Added",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "site": {
                      "$ref": "#/components/schemas/Site"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/sites/{id}": {
      "get": {
        "summary": "One site (scope sites)",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "site": {
                      "$ref": "#/components/schemas/Site"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "patch": {
        "summary": "Change the excluded paths (scope sites)",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "excluded_paths": {
                    "type": "array",
                    "maxItems": 50,
                    "items": {
                      "type": "string",
                      "pattern": "^/"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "site": {
                      "$ref": "#/components/schemas/Site"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "summary": "Remove the site's routes (scope sites); the site stays as `deleted`, its totals with it",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "site": {
                      "$ref": "#/components/schemas/Site"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/sites/{id}/report": {
      "get": {
        "summary": "Totals and series of the site (scope reports); with breakdowns=1 also the top pages, sources, countries, devices, browsers, OS and bots",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          },
          {
            "name": "period",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "today",
                "7d",
                "30d"
              ],
              "default": "today"
            }
          },
          {
            "name": "breakdowns",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            },
            "description": "read the breakdowns from the account's database (cached 5 minutes)"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "report": {
                      "$ref": "#/components/schemas/Report"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/links": {
      "get": {
        "summary": "Links (scope links); `account_id` narrows to one account",
        "parameters": [
          {
            "name": "account_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "links": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Link"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "summary": "Add a short link (scope links) on the account's link host",
        "description": "A click is a `302` to the first matching rule's URL, else `url`; it is counted as a view of\nthe link, with source `qr` when the address carries `?q` (the QR code's) — or as a bot when\nthe client is one (curl, a script, a crawler: it is redirected all the same). `config` turns\n`synced` once the worker has the link (seconds).\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "account_id",
                  "url"
                ],
                "properties": {
                  "account_id": {
                    "type": "string"
                  },
                  "code": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9_-]{3,32}$",
                    "description": "a random 6 characters when not given"
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "https:// only, up to 2048 characters, not on the link host"
                  },
                  "rules": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "$ref": "#/components/schemas/Rule"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Added",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "link": {
                      "$ref": "#/components/schemas/Link"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/links/{id}": {
      "get": {
        "summary": "One link (scope links)",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "link": {
                      "$ref": "#/components/schemas/Link"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "patch": {
        "summary": "Change the URL or the rules (scope links); the code stays",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "rules": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "$ref": "#/components/schemas/Rule"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "link": {
                      "$ref": "#/components/schemas/Link"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "summary": "Remove the link (scope links); its code is freed, the link stays as `deleted` with its totals",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "link": {
                      "$ref": "#/components/schemas/Link"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/links/{id}/qr.svg": {
      "get": {
        "summary": "The QR code of the link (scope links) — an SVG of `https://<link host>/<code>?q`",
        "description": "The `?q` makes a scan count with source `qr`; the worker does not pass it on. One `<rect>` and\none `<path>`, integer coordinates, a quiet zone of 4 modules, error correction M. Without\n`size` the SVG has no width and height and scales to its container. It follows the link host:\na replaced host makes earlier printed codes stop working.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 64,
              "maximum": 9999
            },
            "description": "pixels"
          }
        ],
        "responses": {
          "200": {
            "description": "The SVG",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/links/{id}/report": {
      "get": {
        "summary": "Clicks of the link (scope reports); with breakdowns=1 also sources, countries, devices, browsers, OS and bots (`pages` lists no pages for a link: at most one empty key)",
        "description": "Closed days come from clx.cx; today (and any day the worker has not sent yet) is read live\nfrom the account's own database in one read with the breakdowns — `as_of` is the moment of\nthat read (cached up to 5 minutes, so it can be that much older than the answer). When that database\ncannot be read, the totals hold clx.cx's days only, the series of `today` is empty and\n`unavailable` says why. `incomplete` is always false: a link's days are never refused.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          },
          {
            "name": "period",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "today",
                "7d",
                "30d"
              ],
              "default": "today"
            }
          },
          {
            "name": "breakdowns",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "report": {
                      "$ref": "#/components/schemas/Report"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/sites/{id}/rotate": {
      "post": {
        "summary": "A new path, names and snippet (scope sites); the old path is counted for 30 more days",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "Rotated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "site": {
                      "$ref": "#/components/schemas/Site"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    }
  }
}
