{
    "openapi": "3.0.3",
    "info": {
        "title": "CAT",
        "version": "1",
        "description": "IronWiFi customer administration API. No API key. Sign in with Google at /tajne_tools_auth, then send the session cookie. Read POST /login_check {\"action\":\"check\"} before mutating. See /llms.txt for the call guide."
    },
    "servers": [
        {
            "url": "https://cat.ironwifi.io"
        }
    ],
    "paths": {
        "/login_check": {
            "post": {
                "operationId": "loginCheck",
                "summary": "Session flags, or sign out.",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "oneOf": [
                                    {
                                        "type": "object",
                                        "required": [
                                            "action"
                                        ],
                                        "properties": {
                                            "action": {
                                                "type": "string",
                                                "enum": [
                                                    "check"
                                                ]
                                            }
                                        }
                                    },
                                    {
                                        "type": "object",
                                        "required": [
                                            "action"
                                        ],
                                        "properties": {
                                            "action": {
                                                "type": "string",
                                                "enum": [
                                                    "signout"
                                                ]
                                            }
                                        }
                                    }
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "check returns user_id, email, name, identity_label, can_mutate, can_mint_activation, can_extend_trial, readonly, env, mutating_actions. user_id is null without a valid cookie. Ignore company-id lists. signout redirects to /tajne_tools."
                    }
                }
            }
        },
        "/tools_action": {
            "post": {
                "operationId": "toolsAction",
                "summary": "One customer-admin action. Requires an allowlisted session cookie.",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "oneOf": [
                                    {
                                        "$ref": "#/components/schemas/Action_search"
                                    },
                                    {
                                        "$ref": "#/components/schemas/Action_fetch_quotes"
                                    },
                                    {
                                        "$ref": "#/components/schemas/Action_fetch_account"
                                    },
                                    {
                                        "$ref": "#/components/schemas/Action_fetch_activity"
                                    },
                                    {
                                        "$ref": "#/components/schemas/Action_fetch_tasks"
                                    },
                                    {
                                        "$ref": "#/components/schemas/Action_fetch_person"
                                    },
                                    {
                                        "$ref": "#/components/schemas/Action_fetch_signals"
                                    },
                                    {
                                        "$ref": "#/components/schemas/Action_preview_activation_link"
                                    },
                                    {
                                        "$ref": "#/components/schemas/Action_activation_quote_pdf"
                                    },
                                    {
                                        "$ref": "#/components/schemas/Action_extend_trial"
                                    },
                                    {
                                        "$ref": "#/components/schemas/Action_save_quote"
                                    },
                                    {
                                        "$ref": "#/components/schemas/Action_save_task"
                                    },
                                    {
                                        "$ref": "#/components/schemas/Action_activate_sub"
                                    },
                                    {
                                        "$ref": "#/components/schemas/Action_mint_activation_link"
                                    }
                                ],
                                "discriminator": {
                                    "propertyName": "action",
                                    "mapping": {
                                        "search": "#/components/schemas/Action_search",
                                        "fetch-quotes": "#/components/schemas/Action_fetch_quotes",
                                        "fetch-account": "#/components/schemas/Action_fetch_account",
                                        "fetch-activity": "#/components/schemas/Action_fetch_activity",
                                        "fetch-tasks": "#/components/schemas/Action_fetch_tasks",
                                        "fetch-person": "#/components/schemas/Action_fetch_person",
                                        "fetch-signals": "#/components/schemas/Action_fetch_signals",
                                        "preview-activation-link": "#/components/schemas/Action_preview_activation_link",
                                        "activation-quote-pdf": "#/components/schemas/Action_activation_quote_pdf",
                                        "extend-trial": "#/components/schemas/Action_extend_trial",
                                        "save-quote": "#/components/schemas/Action_save_quote",
                                        "save-task": "#/components/schemas/Action_save_task",
                                        "activate-sub": "#/components/schemas/Action_activate_sub",
                                        "mint-activation-link": "#/components/schemas/Action_mint_activation_link"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Action result. Shape depends on action. See the schema description."
                    },
                    "403": {
                        "description": "Body Unauthorised when the cookie is not an allowlisted admin. JSON {\"error\":\"Forbidden\",\"detail\":\"Readonly admin: mutate actions denied\"} when a readonly session calls a mutate action it may not call."
                    },
                    "422": {
                        "description": "JSON {\"error\":\"...\"} validation failure."
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "Action_search": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "action",
                    "searchBy",
                    "search_val"
                ],
                "properties": {
                    "action": {
                        "description": "search",
                        "type": "string",
                        "enum": [
                            "search"
                        ]
                    },
                    "searchBy": {
                        "description": "email, ID, auth_port, or company_name. Any other value returns an empty body with HTTP 200.",
                        "type": "string",
                        "example": "email"
                    },
                    "search_val": {
                        "description": "Lookup text. email and company_name are case-insensitive substrings. ID is a prefix. auth_port is an integer.",
                        "type": "string",
                        "example": "ada@example.com"
                    }
                },
                "description": "Find up to 25 customers. Who: Any allowlisted admin, including a readonly session. Response: JSON {\"_embedded\":{\"customers\":[...]}}. email, ID, and company_name rows include username, owner_id, plan, region, company_name, plan_limit_status, plan_expiration, console_url. auth_port rows include username, id, owner_id."
            },
            "Action_fetch_quotes": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "action"
                ],
                "properties": {
                    "action": {
                        "description": "fetch-quotes",
                        "type": "string",
                        "enum": [
                            "fetch-quotes"
                        ]
                    }
                },
                "description": "The 25 newest saved quotes. monthly_pdf and yearly_pdf are large. Skip them unless you need the file. Who: Any allowlisted admin, including a readonly session. Response: JSON {\"_embedded\":{\"quotes\":[...]}} with id, email, name, date, expiration_date, service_type, discount, billing, monthly_pdf, yearly_pdf, units."
            },
            "Action_fetch_account": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "action",
                    "owner_id"
                ],
                "properties": {
                    "action": {
                        "description": "fetch-account",
                        "type": "string",
                        "enum": [
                            "fetch-account"
                        ]
                    },
                    "owner_id": {
                        "description": "Company id from search.",
                        "type": "string",
                        "example": "12345"
                    }
                },
                "description": "One account: company, lifecycle, logins, adopted node count, plan cap, and the latest support note. Who: Any allowlisted admin, including a readonly session. Response: JSON {\"account\":{company, summary, contacts, contacts_more, node_count, node_count_source, max_routers, note}}. summary.lifecycle is trial, paid, expired, or unknown. summary also has deleted, days_left, stale_expiry, paying, over_limit, over_what (nodes, users, or null), over_limit_stale. On failure {\"account\":null,\"error\":\"missing_id\"|\"not_found\"|\"db_error\"}."
            },
            "Action_fetch_activity": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "action",
                    "owner_id"
                ],
                "properties": {
                    "action": {
                        "description": "fetch-activity",
                        "type": "string",
                        "enum": [
                            "fetch-activity"
                        ]
                    },
                    "owner_id": {
                        "description": "Company id.",
                        "type": "string",
                        "example": "12345"
                    },
                    "window": {
                        "description": "1d, 7d, or 30d. Default and any other value is 7d.",
                        "type": "string",
                        "example": "7d"
                    }
                },
                "description": "Recent console audit events for one account. Consecutive identical actions are collapsed. Capped at 300. Who: Any allowlisted admin, including a readonly session. Response: JSON {events, raw_count, truncated, window, owner_id, error}. Each event has cmd, username, resource, count, last_time, first_time, ip, mixed_ip. Missing owner_id returns events [] and error missing_id."
            },
            "Action_fetch_tasks": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "action"
                ],
                "properties": {
                    "action": {
                        "description": "fetch-tasks",
                        "type": "string",
                        "enum": [
                            "fetch-tasks"
                        ]
                    },
                    "account_id": {
                        "description": "Optional company id.",
                        "type": "string",
                        "example": ""
                    }
                },
                "description": "Task board. Omit account_id for live tasks (status not done, limit 500). Pass account_id for that account, including done tasks (limit 200). Who: Any allowlisted admin, including a readonly session. Response: JSON {board, reminders, team, error}. board is groups of {assignee, tasks, open_count, overdue_count}. Each task is a cat_tasks row plus overdue (bool). reminders is {overdue, today, soon, count, soon_days}. team is the assignable emails. error is null, or db_error."
            },
            "Action_fetch_person": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "action",
                    "assignee"
                ],
                "properties": {
                    "action": {
                        "description": "fetch-person",
                        "type": "string",
                        "enum": [
                            "fetch-person"
                        ]
                    },
                    "assignee": {
                        "description": "Email on the team list returned by fetch-tasks. Anyone else returns error unknown_person.",
                        "type": "string",
                        "example": "person@ironwifi.com"
                    }
                },
                "description": "One assignee's workload, including done tasks from the last 28 days. Who: Any allowlisted admin, including a readonly session. Response: JSON {assignee, stats, error}. stats includes open_count, doing_count, doing, committed_hours, unestimated_count, capacity_known, has_live_work, free_hours (null when any live task is unestimated), hours_per_week, cycle_median_hours, cycle_sample, done_in_window, window_days. error is null, unknown_person, or db_error."
            },
            "Action_fetch_signals": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "action"
                ],
                "properties": {
                    "action": {
                        "description": "fetch-signals",
                        "type": "string",
                        "enum": [
                            "fetch-signals"
                        ]
                    }
                },
                "description": "Accounts currently over their user or node limit, hottest first. Limit 500. Deleted accounts are excluded. Who: Any allowlisted admin, including a readonly session. Response: JSON {\"_embedded\":{\"signals\":[...]},\"row_count\":N}. Each row has id, owner_id, company_name, plan, region, plan_limit_status (over_user_limit or over_node_limit), lastlogin, stripe_subscription_id, billing_email, console_url, signal_score, signal_tier (hot, warm, or cold)."
            },
            "Action_preview_activation_link": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "action",
                    "services"
                ],
                "properties": {
                    "action": {
                        "description": "preview-activation-link",
                        "type": "string",
                        "enum": [
                            "preview-activation-link"
                        ]
                    },
                    "services": {
                        "description": "Object of positive counts. Keys: guest_wifi, employee_wifi, iot, coworking, students, campus_users. Education keys (students, campus_users) cannot be mixed with the commercial keys.",
                        "type": "object",
                        "example": {
                            "employee_wifi": 100
                        }
                    },
                    "billing": {
                        "description": "annual (default) or monthly. Anything else is treated as annual.",
                        "type": "string",
                        "example": "annual"
                    },
                    "currency": {
                        "description": "USD, EUR, or GBP. Default USD.",
                        "type": "string",
                        "example": "USD"
                    },
                    "collection_method": {
                        "description": "charge_automatically (default) or send_invoice.",
                        "type": "string",
                        "example": "charge_automatically"
                    },
                    "override_monthly": {
                        "description": "Optional positive number. The quote service clamps it to list price.",
                        "type": "string"
                    },
                    "discount_pct": {
                        "description": "Optional percent off list, from 0 to 100. Above 100 is rejected.",
                        "type": "string"
                    },
                    "venues": {
                        "description": "Optional venue count. Only values above 1 change the price.",
                        "type": "string"
                    },
                    "is_test": {
                        "description": "Optional true. A test link needs no customer.",
                        "type": "string"
                    },
                    "contact_name": {
                        "description": "Optional, max 200 characters.",
                        "type": "string"
                    },
                    "contact_company": {
                        "description": "Optional, max 200 characters.",
                        "type": "string"
                    },
                    "contact_email": {
                        "description": "Optional. Rejected when it is not an email.",
                        "type": "string"
                    },
                    "contact_phone": {
                        "description": "Optional, max 200 characters.",
                        "type": "string"
                    }
                },
                "description": "Price an activation-link quote. Same body as mint-activation-link. Does not create a link. Who: Any allowlisted admin, including a readonly session. Response: JSON from the quote service. A success includes monthly_usd (number) and no registration_url. HTTP 422 {\"error\":\"...\"} when the body is rejected."
            },
            "Action_activation_quote_pdf": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "action",
                    "id"
                ],
                "properties": {
                    "action": {
                        "description": "activation-quote-pdf",
                        "type": "string",
                        "enum": [
                            "activation-quote-pdf"
                        ]
                    },
                    "id": {
                        "description": "Quote id matching q_ and 8 hex characters, for example q_1a2b3c4d.",
                        "type": "string",
                        "example": "q_1a2b3c4d"
                    },
                    "registration_url": {
                        "description": "URL returned by mint-activation-link. Printed on the PDF.",
                        "type": "string",
                        "example": "https://example.invalid/register"
                    },
                    "itdr_tier": {
                        "description": "Optional. essential, plus, or enterprise. Any other value is left off the PDF.",
                        "type": "string",
                        "example": ""
                    }
                },
                "description": "PDF for an activation quote already stored by mint-activation-link. Numbers come from the stored quote, not from this request. Who: Any allowlisted admin, including a readonly session. Response: JSON {filename, pdf}. pdf is base64. HTTP 422 {\"error\":\"...\"} for a bad id or a quote the service cannot read."
            },
            "Action_extend_trial": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "action",
                    "customer_owner_id",
                    "days"
                ],
                "properties": {
                    "action": {
                        "description": "extend-trial",
                        "type": "string",
                        "enum": [
                            "extend-trial"
                        ]
                    },
                    "customer_owner_id": {
                        "description": "Company id.",
                        "type": "string",
                        "example": "12345"
                    },
                    "days": {
                        "description": "Integer from 1 to 90. Anything else returns message \"Incomplete payload\" and writes nothing.",
                        "type": "integer",
                        "example": 14
                    }
                },
                "description": "Move a company's plan_expiration forward by days and set plan_limit_status to ok. This writes the live customer record. Who: Full admin only (can_mutate true). A readonly session gets HTTP 403. Response: JSON {\"message\":true} on success, or {\"message\":\"Incomplete payload\"}."
            },
            "Action_save_quote": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "action",
                    "customer_name",
                    "customer_email",
                    "billing",
                    "currency"
                ],
                "properties": {
                    "action": {
                        "description": "save-quote",
                        "type": "string",
                        "enum": [
                            "save-quote"
                        ]
                    },
                    "customer_name": {
                        "description": "Person the quote is addressed to.",
                        "type": "string",
                        "example": "Ada Lovelace"
                    },
                    "customer_email": {
                        "description": "Where the quote is stored, and where it is emailed if send_email is set.",
                        "type": "string",
                        "example": "ada@example.com"
                    },
                    "company_name": {
                        "description": "Optional company on the PDF.",
                        "type": "string"
                    },
                    "service_type": {
                        "description": "Product label. Ignored when calc_lines is set (the server joins the line labels).",
                        "type": "string",
                        "example": "WPA Enterprise"
                    },
                    "items_num": {
                        "description": "Unit count for the classic single-product path. Default 0.",
                        "type": "integer",
                        "example": 10
                    },
                    "discount": {
                        "description": "Percent. Default 0.",
                        "type": "integer",
                        "example": 0
                    },
                    "billing": {
                        "description": "Monthly / Yearly, Monthly, or Yearly. Anything else saves both monthly and yearly.",
                        "type": "string",
                        "example": "Yearly"
                    },
                    "currency": {
                        "description": "USD, GBP, or EUR.",
                        "type": "string",
                        "example": "USD"
                    },
                    "prepaid_option": {
                        "description": "Default, 3 Years, or 5 Years.",
                        "type": "string"
                    },
                    "premium_support": {
                        "description": "Optional true adds 20 percent.",
                        "type": "string"
                    },
                    "owner_id": {
                        "description": "Optional company id.",
                        "type": "string"
                    },
                    "calc_lines": {
                        "description": "Optional pre-priced lines: {label, qty, unit, rate, total, discPct}. When present, this path is used instead of service_type.",
                        "type": "string"
                    },
                    "calc_billing_label": {
                        "description": "Label stored with calc_lines. Default Annually.",
                        "type": "string"
                    },
                    "calc_currency": {
                        "description": "Currency symbol source for calc_lines. Falls back to currency, then USD.",
                        "type": "string"
                    },
                    "send_email": {
                        "description": "Omit unless a person asked to email the customer. Any present value sends the email.",
                        "type": "string"
                    }
                },
                "description": "Save a classic quote and build its PDF. Include send_email only when a person has asked to email that customer. When send_email is present the server emails them and the response is {\"message\":\"sent\"} instead of the PDFs. Who: Full admin only (can_mutate true). A readonly session gets HTTP 403. Response: JSON {pdf_monthly, pdf_yearly, sql_result}. A request that set send_email returns {\"message\":\"sent\"} and does not include the PDFs."
            },
            "Action_save_task": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "action",
                    "title"
                ],
                "properties": {
                    "action": {
                        "description": "save-task",
                        "type": "string",
                        "enum": [
                            "save-task"
                        ]
                    },
                    "id": {
                        "description": "Omit to create. Set to update that task.",
                        "type": "string"
                    },
                    "title": {
                        "description": "Required. Max 255 characters.",
                        "type": "string",
                        "example": "Confirm the RADIUS cert"
                    },
                    "detail": {
                        "description": "Optional text.",
                        "type": "string"
                    },
                    "assignee": {
                        "description": "Optional email from the fetch-tasks team list. Any other email is rejected.",
                        "type": "string",
                        "example": "person@ironwifi.com"
                    },
                    "status": {
                        "description": "open (default), doing, blocked, or done.",
                        "type": "string",
                        "example": "open"
                    },
                    "priority": {
                        "description": "low, normal (default), or high.",
                        "type": "string",
                        "example": "normal"
                    },
                    "due_date": {
                        "description": "YYYY-MM-DD, or omit. Required when recurrence is set.",
                        "type": "string"
                    },
                    "account_id": {
                        "description": "Optional company id.",
                        "type": "string",
                        "example": "12345"
                    },
                    "estimate_hours": {
                        "description": "Optional number from 0 to 1000.",
                        "type": "string"
                    },
                    "recurrence": {
                        "description": "daily, weekly, monthly, yearly, or omit.",
                        "type": "string"
                    },
                    "remind_at": {
                        "description": "YYYY-MM-DD, or omit.",
                        "type": "string"
                    }
                },
                "description": "Create a task, or update one when id is set. created_by is the signed-in subject, not a field you send. Marking a recurring task done spawns the next one. Who: Full admin only (can_mutate true). A readonly session gets HTTP 403. Response: JSON {id, error:null} on create. JSON {id, spawned, error:null} on update. spawned is the new task id when a done recurring task spawned the next one, otherwise null. HTTP 422 {\"error\":\"...\"} when validation fails or the team list is empty."
            },
            "Action_activate_sub": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "action",
                    "company_id",
                    "name",
                    "max_users",
                    "max_routers",
                    "billing",
                    "sub_id",
                    "saml",
                    "scep"
                ],
                "properties": {
                    "action": {
                        "description": "activate-sub",
                        "type": "string",
                        "enum": [
                            "activate-sub"
                        ]
                    },
                    "company_id": {
                        "description": "Company id.",
                        "type": "string",
                        "example": "12345"
                    },
                    "name": {
                        "description": "Plan name.",
                        "type": "string",
                        "example": "WPA Enterprise"
                    },
                    "max_users": {
                        "description": "Integer cap.",
                        "type": "integer",
                        "example": 100
                    },
                    "max_routers": {
                        "description": "Integer AP cap.",
                        "type": "integer",
                        "example": 10
                    },
                    "billing": {
                        "description": "charge_automatically or send_invoice.",
                        "type": "string",
                        "example": "charge_automatically"
                    },
                    "sub_id": {
                        "description": "Stripe subscription id.",
                        "type": "string",
                        "example": "sub_example"
                    },
                    "saml": {
                        "description": "1 or 0.",
                        "type": "integer",
                        "example": 0
                    },
                    "scep": {
                        "description": "1 or 0.",
                        "type": "integer",
                        "example": 0
                    }
                },
                "description": "Insert a plan and point the company at it. Clears plan_expiration. This writes the live customer record. Who: Full admin only (can_mutate true). A readonly session gets HTTP 403. Response: JSON {plans, companies} with the two write results."
            },
            "Action_mint_activation_link": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "action",
                    "services"
                ],
                "properties": {
                    "action": {
                        "description": "mint-activation-link",
                        "type": "string",
                        "enum": [
                            "mint-activation-link"
                        ]
                    },
                    "services": {
                        "description": "Object of positive counts. Keys: guest_wifi, employee_wifi, iot, coworking, students, campus_users. Education keys (students, campus_users) cannot be mixed with the commercial keys.",
                        "type": "object",
                        "example": {
                            "guest_wifi": 20
                        }
                    },
                    "billing": {
                        "description": "annual (default) or monthly. Anything else is treated as annual.",
                        "type": "string",
                        "example": "annual"
                    },
                    "currency": {
                        "description": "USD, EUR, or GBP. Default USD.",
                        "type": "string",
                        "example": "USD"
                    },
                    "collection_method": {
                        "description": "charge_automatically (default) or send_invoice.",
                        "type": "string",
                        "example": "send_invoice"
                    },
                    "override_monthly": {
                        "description": "Optional positive number. The quote service clamps it to list price.",
                        "type": "string"
                    },
                    "discount_pct": {
                        "description": "Optional percent off list, from 0 to 100. Above 100 is rejected.",
                        "type": "string"
                    },
                    "venues": {
                        "description": "Optional venue count. Only values above 1 change the price.",
                        "type": "string"
                    },
                    "is_test": {
                        "description": "Optional true. A test link needs no customer.",
                        "type": "string"
                    },
                    "contact_name": {
                        "description": "Optional, max 200 characters.",
                        "type": "string"
                    },
                    "contact_company": {
                        "description": "Optional, max 200 characters.",
                        "type": "string"
                    },
                    "contact_email": {
                        "description": "Optional. Rejected when it is not an email.",
                        "type": "string",
                        "example": "ada@example.com"
                    },
                    "contact_phone": {
                        "description": "Optional, max 200 characters.",
                        "type": "string"
                    }
                },
                "description": "Create an activation link. Same body as preview-activation-link. A readonly session may call this only when /login_check says can_mint_activation. Other mutate actions stay denied for that session. Who: Full admin, or a readonly admin whose /login_check can_mint_activation is true. Response: JSON from the quote service. A success includes registration_url and id (q_ plus 8 hex). HTTP 422 {\"error\":\"...\"} when the body or the quote service rejects it. Pass that id to activation-quote-pdf."
            }
        }
    }
}
