# CAT > IronWiFi customer administration API at https://cat.ironwifi.io. Staff only. No API key. Machine-readable contract: https://cat.ironwifi.io/openapi.json. Sign in with Google as an @ironwifi.com or @ironwifi.io account that the server allowlist accepts. Sign-in posts to /tajne_tools_auth and sets an HttpOnly session cookie. Send that cookie on every later call (a browser fetch with credentials include, or the Cookie header from that session). Both calls below are POST, body JSON, header Content-Type application/json. ## Session POST /login_check {"action":"check"} Read user_id (null when the cookie is missing or bad), can_mutate, can_mint_activation, can_extend_trial, readonly, mutating_actions, and env (production or staging). Ignore any company-id lists in this response. They are internal. A readonly session may call every action marked read. It may call mint-activation-link only when can_mint_activation is true, and extend-trial (1-90 days) only when can_extend_trial is true. preview-activation-link and activation-quote-pdf are read. save-quote, save-task, and activate-sub stay denied. POST /login_check {"action":"signout"} clears the session and redirects to /tajne_tools. ## Calling an action POST /tools_action with the JSON body shown under the action. HTTP 403 with body Unauthorised means the cookie is not an allowlisted admin. HTTP 403 JSON {"error":"Forbidden","detail":"Readonly admin: mutate actions denied"} means a readonly session called a mutate action it may not call. HTTP 422 JSON {"error":"..."} means the body failed validation. {"message":"Action not recognized"} means the action name is not in this list. Unknown names are treated as mutate, so a readonly session is denied before that message. ## Actions ### search (read) Find up to 25 customers. Who: Any allowlisted admin, including a readonly session. Body: { "action": "search", "searchBy": "email", "search_val": "ada@example.com" } - action: search - searchBy: email, ID, auth_port, or company_name. Any other value returns an empty body with HTTP 200. - search_val: Lookup text. email and company_name are case-insensitive substrings. ID is a prefix. auth_port is an integer. 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. ### fetch-quotes (read) 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. Body: { "action": "fetch-quotes" } - action: fetch-quotes Response: JSON {"_embedded":{"quotes":[...]}} with id, email, name, date, expiration_date, service_type, discount, billing, monthly_pdf, yearly_pdf, units. ### fetch-account (read) One account: company, lifecycle, logins, adopted node count, plan cap, and the latest support note. Who: Any allowlisted admin, including a readonly session. Body: { "action": "fetch-account", "owner_id": "12345" } - action: fetch-account - owner_id: Company id from search. 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"}. ### fetch-activity (read) Recent console audit events for one account. Consecutive identical actions are collapsed. Capped at 300. Who: Any allowlisted admin, including a readonly session. Body: { "action": "fetch-activity", "owner_id": "12345", "window": "7d" } - action: fetch-activity - owner_id: Company id. - window: 1d, 7d, or 30d. Default and any other value is 7d. 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. ### fetch-tasks (read) 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. Body: { "action": "fetch-tasks", "account_id": "" } - action: fetch-tasks - account_id: Optional company id. 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. ### fetch-person (read) One assignee's workload, including done tasks from the last 28 days. Who: Any allowlisted admin, including a readonly session. Body: { "action": "fetch-person", "assignee": "person@ironwifi.com" } - action: fetch-person - assignee: Email on the team list returned by fetch-tasks. Anyone else returns error unknown_person. 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. ### fetch-signals (read) Accounts currently over their user or node limit, hottest first. Limit 500. Deleted accounts are excluded. Who: Any allowlisted admin, including a readonly session. Body: { "action": "fetch-signals" } - action: fetch-signals 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). ### preview-activation-link (read) Price an activation-link quote. Same body as mint-activation-link. Does not create a link. Who: Any allowlisted admin, including a readonly session. Body: { "action": "preview-activation-link", "services": { "employee_wifi": 100 }, "billing": "annual", "currency": "USD", "collection_method": "charge_automatically" } - action: preview-activation-link - services: 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. - billing: annual (default) or monthly. Anything else is treated as annual. - currency: USD, EUR, or GBP. Default USD. - collection_method: charge_automatically (default) or send_invoice. - override_monthly: Optional positive number. The quote service clamps it to list price. - discount_pct: Optional percent off list, from 0 to 100. Above 100 is rejected. - venues: Optional venue count. Only values above 1 change the price. - is_test: Optional true. A test link needs no customer. - contact_name: Optional, max 200 characters. - contact_company: Optional, max 200 characters. - contact_email: Optional. Rejected when it is not an email. - contact_phone: Optional, max 200 characters. Response: JSON from the quote service. A success includes monthly_usd (number) and no registration_url. HTTP 422 {"error":"..."} when the body is rejected. ### activation-quote-pdf (read) 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. Body: { "action": "activation-quote-pdf", "id": "q_1a2b3c4d", "registration_url": "https://example.invalid/register", "itdr_tier": "" } - action: activation-quote-pdf - id: Quote id matching q_ and 8 hex characters, for example q_1a2b3c4d. - registration_url: URL returned by mint-activation-link. Printed on the PDF. - itdr_tier: Optional. essential, plus, or enterprise. Any other value is left off the PDF. Response: JSON {filename, pdf}. pdf is base64. HTTP 422 {"error":"..."} for a bad id or a quote the service cannot read. ### extend-trial (mutate) 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. Body: { "action": "extend-trial", "customer_owner_id": "12345", "days": 14 } - action: extend-trial - customer_owner_id: Company id. - days: Integer from 1 to 90. Anything else returns message "Incomplete payload" and writes nothing. Response: JSON {"message":true} on success, or {"message":"Incomplete payload"}. ### save-quote (mutate) 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. Body: { "action": "save-quote", "customer_name": "Ada Lovelace", "customer_email": "ada@example.com", "service_type": "WPA Enterprise", "items_num": 10, "discount": 0, "billing": "Yearly", "currency": "USD" } - action: save-quote - customer_name: Person the quote is addressed to. - customer_email: Where the quote is stored, and where it is emailed if send_email is set. - company_name: Optional company on the PDF. - service_type: Product label. Ignored when calc_lines is set (the server joins the line labels). - items_num: Unit count for the classic single-product path. Default 0. - discount: Percent. Default 0. - billing: Monthly / Yearly, Monthly, or Yearly. Anything else saves both monthly and yearly. - currency: USD, GBP, or EUR. - prepaid_option: Default, 3 Years, or 5 Years. - premium_support: Optional true adds 20 percent. - owner_id: Optional company id. - calc_lines: Optional pre-priced lines: {label, qty, unit, rate, total, discPct}. When present, this path is used instead of service_type. - calc_billing_label: Label stored with calc_lines. Default Annually. - calc_currency: Currency symbol source for calc_lines. Falls back to currency, then USD. - send_email: Omit unless a person asked to email the customer. Any present value sends the email. Response: JSON {pdf_monthly, pdf_yearly, sql_result}. A request that set send_email returns {"message":"sent"} and does not include the PDFs. ### save-task (mutate) 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. Body: { "action": "save-task", "title": "Confirm the RADIUS cert", "assignee": "person@ironwifi.com", "status": "open", "priority": "normal", "account_id": "12345" } - action: save-task - id: Omit to create. Set to update that task. - title: Required. Max 255 characters. - detail: Optional text. - assignee: Optional email from the fetch-tasks team list. Any other email is rejected. - status: open (default), doing, blocked, or done. - priority: low, normal (default), or high. - due_date: YYYY-MM-DD, or omit. Required when recurrence is set. - account_id: Optional company id. - estimate_hours: Optional number from 0 to 1000. - recurrence: daily, weekly, monthly, yearly, or omit. - remind_at: YYYY-MM-DD, or omit. 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. ### activate-sub (mutate) 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. Body: { "action": "activate-sub", "company_id": "12345", "name": "WPA Enterprise", "max_users": 100, "max_routers": 10, "billing": "charge_automatically", "sub_id": "sub_example", "saml": 0, "scep": 0 } - action: activate-sub - company_id: Company id. - name: Plan name. - max_users: Integer cap. - max_routers: Integer AP cap. - billing: charge_automatically or send_invoice. - sub_id: Stripe subscription id. - saml: 1 or 0. - scep: 1 or 0. Response: JSON {plans, companies} with the two write results. ### mint-activation-link (mutate) 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. Body: { "action": "mint-activation-link", "services": { "guest_wifi": 20 }, "billing": "annual", "currency": "USD", "collection_method": "send_invoice", "contact_email": "ada@example.com" } - action: mint-activation-link - services: 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. - billing: annual (default) or monthly. Anything else is treated as annual. - currency: USD, EUR, or GBP. Default USD. - collection_method: charge_automatically (default) or send_invoice. - override_monthly: Optional positive number. The quote service clamps it to list price. - discount_pct: Optional percent off list, from 0 to 100. Above 100 is rejected. - venues: Optional venue count. Only values above 1 change the price. - is_test: Optional true. A test link needs no customer. - contact_name: Optional, max 200 characters. - contact_company: Optional, max 200 characters. - contact_email: Optional. Rejected when it is not an email. - contact_phone: Optional, max 200 characters. 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.