{
  "openapi": "3.1.0",
  "info": {
    "title": "EyalSec API",
    "version": "1.0.0",
    "summary": "Read your EyalSec events, machines and activity, and change your machines, rules and settings.",
    "description": "The EyalSec API lets a script do what you do in the EyalSec dashboard: read your events, machines and activity, and change your machines, rules and settings. It is the same backend the dashboard talks to, and it answers in JSON unless an endpoint says otherwise.\n\n**Authentication.** Send an API key in the `X-API-Key` header. Create it on **Settings > API access** in the dashboard; it starts with `es2_` and is shown once. A read-only key can call every endpoint marked read and is refused every endpoint marked write (403). A page already open in a signed-in browser can use its session cookie instead, and then sends the CSRF token in `X-CSRFToken` on every write.\n\n**Limits.** 300 requests per minute per account for key-signed requests, 600 per minute per IP address, 120 per minute per IP address across the events calls, and 10 per minute per account for the calls that ask for your password. A request over a limit is answered with 429 and a `Retry-After` header.\n\n**Paging.** The events query returns at most 100 events per page and a `next_cursor`. A short or empty page is not the last page: keep following the cursor until it comes back empty.\n\nThe guide, with a worked example for every endpoint, is at https://eyalsec.com/docs/api. To set up an account, book a live demo at https://eyalsec.com/contact.",
    "termsOfService": "https://eyalsec.com/legal/terms",
    "contact": {
      "name": "EyalSec support",
      "url": "https://eyalsec.com/contact",
      "email": "support@eyalsec.com"
    },
    "x-logo": {
      "url": "https://eyalsec.com/static/icons/eyalsec-shield.png",
      "altText": "EyalSec"
    }
  },
  "externalDocs": {
    "description": "The API guide",
    "url": "https://eyalsec.com/docs/api"
  },
  "servers": [
    {
      "url": "https://eyalsec.com"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    },
    {
      "SessionCookie": []
    }
  ],
  "tags": [
    {
      "name": "Events",
      "description": "Query, export and map your events; one event's detail; runs, counts, statistics and saved searches.",
      "externalDocs": {
        "url": "https://eyalsec.com/docs/api-events"
      }
    },
    {
      "name": "Machines",
      "description": "List, add, rename and delete machines; install and uninstall commands; the targets and Python versions you can install on.",
      "externalDocs": {
        "url": "https://eyalsec.com/docs/api-machines"
      }
    },
    {
      "name": "CI",
      "description": "Install es-python on a CI runner by machine name, and check what a run reported.",
      "externalDocs": {
        "url": "https://eyalsec.com/docs/api-ci"
      }
    },
    {
      "name": "Filters, rules and taint",
      "description": "Filters, machine rules, taint sources, web protection, outbound connections and the unique-event definition.",
      "externalDocs": {
        "url": "https://eyalsec.com/docs/api-rules"
      }
    },
    {
      "name": "Rule templates",
      "description": "List, create, apply and snapshot rule templates.",
      "externalDocs": {
        "url": "https://eyalsec.com/docs/api-templates"
      }
    },
    {
      "name": "Labels and notes",
      "description": "Your triage labels, and your note on each event.",
      "externalDocs": {
        "url": "https://eyalsec.com/docs/api-labels"
      }
    },
    {
      "name": "Activity and account",
      "description": "Your activity log, profile, password, two-factor settings and API key.",
      "externalDocs": {
        "url": "https://eyalsec.com/docs/api-account"
      }
    },
    {
      "name": "Agent endpoints",
      "description": "The one agent-side check you can also call yourself: whether a self-test event reached the dashboard.",
      "externalDocs": {
        "url": "https://eyalsec.com/docs/api-agents"
      }
    }
  ],
  "paths": {
    "/api/get_all_events_with_count/": {
      "post": {
        "tags": [
          "Events"
        ],
        "operationId": "postGetAllEventsWithCount",
        "summary": "Query events (filter body). A POST, but a read: a read-only API key may call it",
        "description": "Returns one page (up to 100 rows) of the caller's coalesced events, newest first (or most severe first with sort=severity), with an opaque next_cursor for pagination. Filters arrive as an optional JSON body (an empty POST = first page, no filters). Cookie callers must send a valid CSRF token (X-CSRFToken); API-key callers are exempt. Each event is a positional array of 17 elements: [created_at, where, str_repr, count, machine_name, severity, agent_kind, id, source_label, context, suppressed, sanitize_reason, impact, labels, has_comment, extension, taint_source]. taint_source is an es-python event's taint source by the name a rule's source takes (socket, file, env, ...); it is empty for every other product and when no source was recorded. The array is appended to, never reordered, so index by position and ignore trailing elements you do not know. impact is the event's vulnerability classes as [{label, sev}]: label is a short class name (\"xss\", \"sqli\", \"command injection\") and sev is that CLASS's own severity, which is not the event's severity in element 5 - an rce-class sink stays critical on an event graded low because its source is trusted. A row can carry more than one (a sink class plus a source class), worst first, at most three. labels is your own triage labels on the event as [{id, name, color}]; has_comment is true when you have written a note on it; extension names the browser extension an es-chromium event is attributed to (its name and version, or its id when no name is known) and is empty for every other event. The heavy per-event fields (trace, source, origin, repr_created, detail) and your note's text are NOT in this array; fetch them for one event with GET /api/event/{id}. A SHORT PAGE IS NOT THE LAST PAGE: every request examines a bounded slice of your history so no single call runs for seconds, so a selective filter can return fewer than 100 events, or zero, and still hand back a next_cursor. Follow the cursor until it comes back empty, even after an empty page; the scanned and exhausted fields report how far the current request got, and oldest_examined is the timestamp of the oldest row it looked at. When an enabled Hide rule matches every event (an empty pattern or .*), the response returns at once with no events, exhausted true, and hidden_by {id, label} naming that rule. This applies to every filter, including whitelist_repr, blacklist_repr, machine and sources.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Events",
          "url": "https://eyalsec.com/docs/api-events"
        },
        "requestBody": {
          "description": "Every field is optional; an empty body returns the first page with no filters.",
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EventQuery"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventPage"
                },
                "example": {
                  "event_limit": 25000,
                  "event_limit_monthly": true,
                  "events": [
                    [
                      "2026-07-05T12:00:00Z",
                      "socket-recv",
                      "'…'",
                      3,
                      "web-01",
                      "high",
                      "python",
                      12345,
                      "",
                      "",
                      false,
                      "This value was not sanitized.",
                      [
                        {
                          "label": "data exfiltration",
                          "sev": "medium"
                        }
                      ],
                      [
                        {
                          "color": "amber",
                          "id": 1,
                          "name": "triage"
                        }
                      ],
                      false,
                      "",
                      "socket"
                    ]
                  ],
                  "exhausted": true,
                  "next_cursor": "",
                  "oldest_examined": "2026-07-05T12:00:00Z",
                  "scanned": 1
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X POST https://eyalsec.com/api/get_all_events_with_count/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"machine\":\"web-01\",\"from\":\"2026-07-01\",\"bucket\":\"all\"}'"
          }
        ]
      }
    },
    "/api/events_export": {
      "post": {
        "tags": [
          "Events"
        ],
        "operationId": "postEventsExport",
        "summary": "Export the whole filtered event set as CSV or JSON",
        "description": "Takes the SAME body as /api/get_all_events_with_count/ and returns every matching event as a downloadable file rather than one page as JSON. It shares that endpoint's request parsing, so the filters, the monthly per-view quota, the suppression bucket and the impact-tag predicate all mean exactly what they mean on the events page: the file covers the rows the screen was showing, never a different set. Any cursor in the body is ignored, because an export is the whole set rather than the page you had scrolled to. Unlike the list, every row carries the heavy fields (page URL and call site, origin, repr_created and trace), since a finding you cannot reproduce is not worth exporting. taint_chain is the forensic chain and is blank for non-admin accounts, matching GET /api/event/{id}; the column is present either way so a non-admin file and an admin file stay diffable. The export is capped at 50000 rows: CSV announces the cap as a final row beginning TRUNCATED, and JSON sets truncated to true, because a silent stop at the cap is indistinguishable from having found exactly that many events.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Events",
          "url": "https://eyalsec.com/docs/api-events"
        },
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "description": "csv (the default) or json. Any other value is a 400 naming the two, never a silent fallback.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "Identical to /api/get_all_events_with_count/: whitelist_repr, blacklist_repr, from, to, machine, severity, sources, tags, view, bucket, labels, xss_only, scope, advanced and sort (the order of the rows in the file). An empty POST exports everything you can see.",
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EventQuery"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "id,created_at,severity,agent_kind,machine,sink,source,value,count,page_or_context,origin,repr_created,trace,suppressed,sanitize_reason,impact_tags,taint_chain\n12345,2026-07-05T12:00:00Z,critical,browser,laptop-01,Element.innerHTML,location.hash,\"<img src=x>\",3,https://app.example.com/p,…,,…,false,This value was not sanitized.,xss:critical,"
              },
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X POST 'https://eyalsec.com/api/events_export?format=csv' \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"view\":\"browser\",\"severity\":[\"critical\",\"high\"]}' -o findings.csv"
          }
        ]
      }
    },
    "/api/event_map/": {
      "post": {
        "tags": [
          "Events"
        ],
        "operationId": "postEventMap",
        "summary": "Map the filtered es-python events as sources, files and the sinks they reached",
        "description": "Takes the SAME body as /api/get_all_events_with_count/, with view python, and returns the whole filtered set aggregated into a graph instead of a page of rows. This is what the events page's Map layout draws. Every event is counted under the source category the sources filter would select it with (socket, file, stdin, env, argv, manual, fuzzer, foreign, or other for an origin none of those claim), under the file it was read from when that source is file, and under its sink (the where field). sources, files and sinks list each of those, and links lists every source, file and sink path; each entry carries events (stored events), hits (the occurrences those events coalesce) and severity (the worst among them), worst first. file is empty on a link whose source is not file, and a file path is empty for a file read through a descriptor that named no path. Only what the filtered events contain appears: with no socket events there is no socket source. The filters, the monthly per-view quota, the suppression bucket and the impact-tag predicate mean exactly what they mean on the events page. Add any of source, file and sink as query parameters to also receive focus.events, the newest 200 matching events as list rows in the same 17-element shape as /api/get_all_events_with_count/, with focus.truncated set when there were more; the aggregate is never narrowed by them. A present but empty file selects the descriptor with no path. Add only=focus with a focus to receive just {\"focus\": ...} when you already hold the graph; a focus request is answered from the map most recently built for the same body, and its events are read fresh, so one that no longer matches the filter is left out. The map covers every event the filter selects, so truncated is false. Any view other than python is a 400.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Events",
          "url": "https://eyalsec.com/docs/api-events"
        },
        "parameters": [
          {
            "name": "source",
            "in": "query",
            "description": "Focus: one of socket, file, stdin, env, argv, manual, fuzzer, foreign, other. Anything else is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "file",
            "in": "query",
            "description": "Focus: a file path exactly as it appears in files; empty selects the descriptor with no path. Only file-source events can match.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sink",
            "in": "query",
            "description": "Focus: a sink label exactly as it appears in sinks.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "only",
            "in": "query",
            "description": "focus: return only the focus object, not the graph. Needs source, file or sink; any other value is a 400.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "Identical to /api/get_all_events_with_count/: whitelist_repr, blacklist_repr, from, to, machine, severity, sources, tags, view, bucket, labels, scope and advanced. view must resolve to python.",
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EventQuery"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "events": 3,
                  "files": [
                    {
                      "events": 2,
                      "hits": 3,
                      "path": "/srv/app/settings.py",
                      "severity": "critical"
                    }
                  ],
                  "focus": {
                    "events": [
                      [
                        "2026-07-05T12:00:00Z",
                        "os system",
                        "'…'",
                        2,
                        "web-01",
                        "critical",
                        "python",
                        12345,
                        "",
                        "",
                        false,
                        "This value was not sanitized.",
                        [],
                        [],
                        false,
                        "",
                        "file"
                      ]
                    ],
                    "truncated": false
                  },
                  "hits": 6,
                  "links": [
                    {
                      "events": 2,
                      "file": "/srv/app/settings.py",
                      "hits": 3,
                      "severity": "critical",
                      "sink": "os system",
                      "source": "file"
                    },
                    {
                      "events": 1,
                      "file": "",
                      "hits": 3,
                      "severity": "high",
                      "sink": "os system",
                      "source": "socket"
                    }
                  ],
                  "max_events": 0,
                  "sinks": [
                    {
                      "events": 3,
                      "hits": 6,
                      "severity": "critical",
                      "sink": "os system"
                    }
                  ],
                  "sources": [
                    {
                      "events": 2,
                      "hits": 3,
                      "kind": "file",
                      "severity": "critical"
                    },
                    {
                      "events": 1,
                      "hits": 3,
                      "kind": "socket",
                      "severity": "high"
                    }
                  ],
                  "truncated": false
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X POST 'https://eyalsec.com/api/event_map/?sink=os%20system' \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"view\":\"python\",\"from\":\"2026-07-01\"}'"
          }
        ]
      }
    },
    "/api/event/{id}": {
      "get": {
        "tags": [
          "Events"
        ],
        "operationId": "getEventById",
        "summary": "Heavy fields of one event (trace, origin, detail, your note; source admin-only)",
        "description": "Returns the large per-event fields that /api/get_all_events_with_count/ deliberately omits: trace, source, origin, repr_created, detail, source_code and sink_code, plus comment (the text of your note on the event, empty when there is none), extension ({id, name, version, label} of the browser extension an es-chromium event is attributed to, all empty otherwise) and runs (the programs that produced the event, newest first, at most 50: {run_uid, cmdline, env, count, first_seen, last_seen}, with values that look like secrets masked; env is empty unless the machine sends it, and the list is empty for an event reported without run context), and requests (the web requests an es-python event happened in, newest first, at most 50: {ip, peer, method, path, host, count, first_seen, last_seen}; ip is the client address web protection attributed the request to, peer the connection address when it differs, and query values whose names look like secrets are masked; empty outside a web request). source_code and sink_code are listings of the program's own source around the line that created the tainted value and the line that consumed it, each beginning with a \"<path>:<line>\" header line; both are empty unless that machine is set to send them. The id is element 7 of an events-list row. source is the forensic taint chain and is blank for non-admin accounts. Ownership is a predicate in the lookup, not a check after it, so an event belonging to another account 404s exactly like a missing one, and so does an event of a product your account no longer holds. rule_source is the value a rule's source field takes to match this event's taint source: a built-in source name, a number for one of your own sources, or any when the source is unknown.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Events",
          "url": "https://eyalsec.com/docs/api-events"
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Event id, taken from element 7 of an events-list row.",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "comment": "",
                  "detail": "/var/www/app/handler.py",
                  "extension": {
                    "id": "",
                    "label": "",
                    "name": "",
                    "version": ""
                  },
                  "origin": "{...origin...}",
                  "repr_created": "<str 'id'>",
                  "rule_source": "socket",
                  "runs": [
                    {
                      "cmdline": "es-python app.py --port 8080",
                      "count": 12,
                      "env": "",
                      "first_seen": "2026-09-17T08:12:03Z",
                      "last_seen": "2026-09-17T09:40:51Z",
                      "run_uid": "9f2c1d4ab8e0472c93ad5f6178be20c4"
                    }
                  ],
                  "sink_code": "/srv/app/db.py:40\n> 40    cur.execute(sql)",
                  "source": "",
                  "source_code": "/srv/app/handler.py:12\n  11    q = request.args['q']\n> 12    run(q)",
                  "trace": "Traceback (most recent call last)…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl https://eyalsec.com/api/event/90210 -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/runs/": {
      "get": {
        "tags": [
          "Events"
        ],
        "operationId": "getRuns",
        "summary": "The runs (individual program invocations) behind a machine's events",
        "description": "Returns a machine's runs, newest first: one entry per invocation of an instrumented program, with what was run, when it was first and last seen reporting, how many distinct events it produced and how many occurrences those add up to. Values that look like passwords, tokens or keys are masked in the command line. Use run_uid with the events query's advanced builder - {\"field\":\"run\",\"op\":\"is\",\"value\":\"<run_uid>\"} on the python view - to narrow the events list, an export or the map to that one run; under that filter the count on each event is that run's own occurrences rather than the event's all-time total. A run_uid is empty on runs reported by agents that predate run ids, which are grouped by command line and environment instead: several invocations then share one entry. Ownership is a predicate in the query, so a machine secret belonging to another account returns an empty list, never that account's runs. Page with next_before: pass it back as before until it comes back empty.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Events",
          "url": "https://eyalsec.com/docs/api-events"
        },
        "parameters": [
          {
            "name": "secret",
            "in": "query",
            "description": "Restrict to one machine by its secret (from GET /api/get_all_machines/). Omitted = every machine on the account.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "machine",
            "in": "query",
            "description": "Restrict to one machine by display name (exact match), the same name the events query's machine filter takes.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Case-insensitive substring of the command line.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "before",
            "in": "query",
            "description": "Pagination cursor: the next_before of the previous page, passed back unchanged (it is opaque, not a bare timestamp). An unparseable value is treated as the first page rather than a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Rows per page, 1-200 (default and maximum 200).",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "next_before": "",
                  "runs": [
                    {
                      "cmdline": "es-python app.py --port 8080",
                      "events": 7,
                      "first_seen": "2026-09-20T08:12:03Z",
                      "last_seen": "2026-09-20T08:14:55Z",
                      "occurrences": 31,
                      "run_uid": "9f2c1d4ab8e0472c93ad5f6178be20c4"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl 'https://eyalsec.com/api/runs/?secret=<machine-secret>&limit=2' -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/event_view_counts/": {
      "get": {
        "tags": [
          "Events"
        ],
        "operationId": "getEventViewCounts",
        "summary": "Per-view event tallies for the events page tabs",
        "description": "Returns how many of your stored events came from each agent kind you have access to. Every view you can open is always present, at zero when you have none, so a caller never has to guess whether a missing key means zero or an error. Which views you have is set per account: every product is one an administrator enables for you individually. An account with no products enabled receives {}. Each number is what the events list for that view would actually show, not a raw row count: your monthly event quota and the bucket parameter narrow it exactly as they narrow the list, so a tab and the rows under it cannot disagree. Admins have no quota. /api/event_count/ remains the separate quota-period tally. Counting is O(rows) on your whole partition, so the result is cached per (account, scope, bucket, quota) and refreshed in the background: a repeat call is served instantly and may be up to a minute stale.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Events",
          "url": "https://eyalsec.com/docs/api-events"
        },
        "parameters": [
          {
            "name": "bucket",
            "in": "query",
            "description": "Which suppression bucket to count, matching the events page's Show control: live (default, unsuppressed only), suppressed (suppressed only) or all. Any other value is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "scope",
            "in": "query",
            "description": "Restrict the tally to events whose scope key matches this value. The scope key is the page host for es-chromium, the sink's package for es-go, and the first token of the event's origin (for example a file path) for every other agent. Omit to count everything.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "scope_mode",
            "in": "query",
            "description": "How scope is matched: is (exact), ends (suffix) or regex. There is no default: a scope sent without scope_mode is ignored. An unknown mode or an uncompilable regex is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "scope_empty",
            "in": "query",
            "description": "Also count events with no extractable scope key. Default false.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "browser": 38,
                  "c": 0,
                  "go": 0,
                  "python": 1204
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl 'https://eyalsec.com/api/event_view_counts/?scope_mode=ends&scope=.mydomain.com' -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/event_visibility_counts/": {
      "get": {
        "tags": [
          "Events"
        ],
        "operationId": "getEventVisibilityCounts",
        "summary": "How many of your events are live and how many the sanitizer hides",
        "description": "Returns the two-number split behind the events page's suppressed badge: live is how many of your stored events the sanitization policy shows, suppressed is how many it currently hides. Suppression is presentation-only, so nothing is deleted and the two numbers always add up to every row you have stored; no quota, filter, view or bucket narrows them. It is a separate endpoint rather than a field of /api/get_all_events_with_count/ because it depends on none of that call's inputs, and counting a whole account inside every page fetch made that request many times slower on a large account. The result is cached per account and refreshed in the background, so a repeat call is served instantly and may be up to a minute stale.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Events",
          "url": "https://eyalsec.com/docs/api-events"
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "live": 1200,
                  "suppressed": 34
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl https://eyalsec.com/api/event_visibility_counts/ -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/event_searches/": {
      "get": {
        "tags": [
          "Events"
        ],
        "operationId": "getEventSearches",
        "summary": "List your saved events-page searches for one view",
        "description": "Saved searches are named events-page query strings: the view, the scope, the toolbar filters and the advanced conditions, exactly as they appear in the page URL. They are stored per view, because the searchable fields differ per agent, so this lists one view at a time. Unlike /api/filters/, a saved search changes nothing until it is chosen.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Events",
          "url": "https://eyalsec.com/docs/api-events"
        },
        "parameters": [
          {
            "name": "view",
            "in": "query",
            "description": "One of python, browser, go, c, cpp, rust, node, solidity, java, bash, ruby or php; omitted means python. Anything else is a 400, and a view your account does not hold is a 403.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "searches": [
                    {
                      "id": 3,
                      "name": "my domain",
                      "query": "view=browser&scope=shop.test",
                      "view": "browser"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl 'https://eyalsec.com/api/event_searches/?view=browser' -H 'X-API-Key: <key>'"
          }
        ]
      },
      "post": {
        "tags": [
          "Events"
        ],
        "operationId": "postEventSearches",
        "summary": "Save (or overwrite) an events-page search",
        "description": "Stores one named search. Re-saving the same name in the same view overwrites it, because that is how a search is edited. name is 1 to 80 characters and is trimmed; query is at most 4096 characters and is a URL query string with no leading '?'; view must be python, browser, go, c, cpp, rust, node, solidity, java, bash, ruby or php. The reply is the view's whole list after the save, so the new row's id is available without a second call. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Events",
          "url": "https://eyalsec.com/docs/api-events"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Label shown in the saved-search picker. 1 to 80 characters."
                  },
                  "view": {
                    "type": "string",
                    "description": "python, browser, go, c, cpp, rust, node, solidity, java, bash, ruby or php."
                  },
                  "query": {
                    "type": "string",
                    "description": "The events-page query string, without the leading '?'. At most 4096 characters. Empty saves a search with no filters."
                  }
                },
                "required": [
                  "name",
                  "view"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "searches": [
                    {
                      "id": 3,
                      "name": "my domain",
                      "query": "view=browser&scope=shop.test",
                      "view": "browser"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X POST https://eyalsec.com/api/event_searches/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"name\":\"my domain\",\"view\":\"browser\",\"query\":\"view=browser&scope=shop.test\"}'"
          }
        ]
      }
    },
    "/api/event_searches/{id}": {
      "delete": {
        "tags": [
          "Events"
        ],
        "operationId": "deleteEventSearchesById",
        "summary": "Delete a saved events-page search",
        "description": "Deletes one saved search by id. Ownership is part of the delete itself, so an id belonging to another account removes nothing and still replies 204: distinguishing 'not yours' from 'not there' would confirm that another account's row exists. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Events",
          "url": "https://eyalsec.com/docs/api-events"
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Saved-search id, from GET /api/event_searches/.",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Done. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X DELETE https://eyalsec.com/api/event_searches/3 -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/event_names/": {
      "get": {
        "tags": [
          "Events"
        ],
        "operationId": "getEventNames",
        "summary": "List distinct event names (for filter building)",
        "description": "Returns the distinct event names (the where field) seen across the caller's events, for populating filter/rule pattern pickers.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Events",
          "url": "https://eyalsec.com/docs/api-events"
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "names": [
                    "socket-recv",
                    "file-open",
                    "sql-execute"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl https://eyalsec.com/api/event_names/ -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/get_event_stats/": {
      "get": {
        "tags": [
          "Events"
        ],
        "operationId": "getGetEventStats",
        "summary": "Aggregate event statistics",
        "description": "Returns cached aggregate statistics over the caller's events: total hits, unique event types, rolling 1h/24h/7d counts, the top event types, recent events, a 14-day daily timeline, and (when present) first/latest detection timestamps. The result is cached per account, so concurrent callers share one computation.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Events",
          "url": "https://eyalsec.com/docs/api-events"
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "first_detection": "2026-06-01T00:00:00Z",
                  "last_1h": 3,
                  "last_24h": 40,
                  "last_7d": 300,
                  "latest_detection": "2026-07-05T12:00:00Z",
                  "recent": [
                    {
                      "count": 3,
                      "time": "2026-07-05T12:00:00Z",
                      "where": "socket-recv"
                    }
                  ],
                  "timeline_14d": [
                    {
                      "count": 40,
                      "date": "2026-07-05"
                    }
                  ],
                  "top_events": [
                    {
                      "count": 900,
                      "where": "socket-recv"
                    }
                  ],
                  "total_hits": 1234,
                  "unique_types": 12
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl https://eyalsec.com/api/get_event_stats/ -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/event_count/": {
      "get": {
        "tags": [
          "Events"
        ],
        "operationId": "getEventCount",
        "summary": "How many events the caller holds this quota period",
        "description": "Returns the number of the caller's stored event rows that count toward their quota period (the current calendar month for a regular user, all time for an admin), plus the caller's event limit. The event limit applies to each view separately at its full value, so pass view to get the tally the matching events list is actually capped by; omit it for the total across every view you can open, which for an account with more than one view is a number no single list is capped by. Counting is O(rows) on the caller's whole partition, so the value is cached per (user, view) and refreshed in the background: a repeat call is served instantly from cache and may be up to a few minutes stale. The dashboard events page calls this separately from the events query so a slow count never delays the page, and refetches it on a tab switch.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Events",
          "url": "https://eyalsec.com/docs/api-events"
        },
        "parameters": [
          {
            "name": "view",
            "in": "query",
            "description": "Count only one agent kind: python, browser, go, c, cpp, rust, node, solidity, java, bash, ruby or php. Omit for every view you can open; if that is exactly one, the unscoped tally is that view's. Naming a view your account does not hold is a 403, and an unrecognised value is a 400, never a silent whole-account count. The response echoes the view it counted.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "count": 1234,
                  "event_limit": 5000,
                  "event_limit_monthly": true,
                  "view": "python"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl 'https://eyalsec.com/api/event_count/?view=python' -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/supported_os_types/": {
      "get": {
        "tags": [
          "Machines"
        ],
        "operationId": "getSupportedOsTypes",
        "summary": "List the distro + architecture targets you can install on",
        "description": "Needs no authentication. These are the values you combine into a machine's os_type when calling POST /api/add_new_machine/.\n\nNeeds no authentication.",
        "externalDocs": {
          "description": "Guide: Machines",
          "url": "https://eyalsec.com/docs/api-machines"
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "distros": [
                    {
                      "archs": [
                        "x86_64",
                        "aarch64"
                      ],
                      "id": "ubuntu_24_04",
                      "label": "Ubuntu 24.04 LTS"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [],
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s https://eyalsec.com/api/supported_os_types/"
          }
        ]
      }
    },
    "/api/supported_python_versions/": {
      "get": {
        "tags": [
          "Machines"
        ],
        "operationId": "getSupportedPythonVersions",
        "summary": "List the Python versions you can pick, and the default",
        "description": "Needs no authentication. Use one of these as python_version when calling POST /api/add_new_machine/. Without os it lists every version es-python supports; with os it lists only the versions available for that target, which is what add_new_machine accepts for it, so pass os when you are about to create a machine. When the usual default is not available for the target, default is the newest version that is.\n\nNeeds no authentication.",
        "externalDocs": {
          "description": "Guide: Machines",
          "url": "https://eyalsec.com/docs/api-machines"
        },
        "parameters": [
          {
            "name": "os",
            "in": "query",
            "description": "An os_type (distro_arch, e.g. ubuntu_24_04_x86_64). An unknown target is a 422.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "default": "3.13",
                  "versions": [
                    "3.9",
                    "3.10",
                    "3.11",
                    "3.12",
                    "3.13",
                    "3.14"
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [],
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s 'https://eyalsec.com/api/supported_python_versions/?os=ubuntu_24_04_x86_64'"
          }
        ]
      }
    },
    "/api/get_all_machines/": {
      "get": {
        "tags": [
          "Machines"
        ],
        "operationId": "getGetAllMachines",
        "summary": "List this account's machines",
        "description": "Lists every machine owned by the caller. Each machine is a positional array of 13 elements: [created_at, machine_name, status, public_id, os_type, installed_build, current_build, python_version, socket_only, clone_locked, stalled, stale_agent, agent_kind]. The array is appended to, never reordered. socket_only is a retired flag: machines added now always carry false. agent_kind is the product the machine runs (python, browser, go, c, cpp, rust, node, solidity, java, bash, ruby or php).\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Machines",
          "url": "https://eyalsec.com/docs/api-machines"
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "machines": [
                    [
                      "2026-07-01T00:00:00Z",
                      "web-01",
                      "installed",
                      "<public_id>",
                      "ubuntu_24_04_x86_64",
                      "<build>",
                      "<build>",
                      "3.13",
                      false,
                      false,
                      false,
                      false,
                      "python"
                    ]
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl https://eyalsec.com/api/get_all_machines/ -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/add_new_machine/": {
      "post": {
        "tags": [
          "Machines"
        ],
        "operationId": "postAddNewMachine",
        "summary": "Register a new machine (returns a single-use install secret)",
        "description": "Creates a machine row owned by the caller. It serves es-python and es-chromium; every other product is registered at POST /scanner/<product>/generate, which returns the machine's token and its install command in one response, and naming one here is a 422 that says where to go. Which fields apply depends on product: es-python (the default) takes either nothing (the build is detected on the host when its install runs: os_type and python_version are stored as auto and resolved by /install.sh from what the install command reports) or a distro+arch pair (os_type = distro_arch, validated against the supported targets) with a python_version that must be available for that target (422 otherwise; see GET /api/supported_python_versions/?os=); python_version=auto picks the host's own version at install. es-chromium takes the name alone. A new es-python machine gets machine-scope socket=on and file=on taint settings for each of the two the account has not set account-wide. You must hold the product you name (403 otherwise). Enforced atomically against the caller's machine limit, which is counted PER PRODUCT, so a full es-python allowance does not consume the es-chromium one (409 when at cap). Cookie callers must pass CSRF; API-key callers are exempt.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Machines",
          "url": "https://eyalsec.com/docs/api-machines"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "machine_name": {
                    "type": "string",
                    "description": "Display name for the machine."
                  },
                  "product": {
                    "type": "string",
                    "description": "Which product this machine runs: python (default) or browser. Machines for every other product are registered at POST /scanner/<product>/generate instead, and naming one here is a 422 naming that route."
                  },
                  "distro": {
                    "type": "string",
                    "description": "Distro id, e.g. ubuntu_24_04, or auto. product=python only. Omit it together with arch (or send auto) to have the build detected at install; give both to choose it."
                  },
                  "arch": {
                    "type": "string",
                    "description": "Architecture, e.g. x86_64; combined into os_type = distro_arch. product=python only; given with distro or not at all (one without the other is a 422)."
                  },
                  "python_version": {
                    "type": "string",
                    "description": "Python version, or auto (the host's own version when a build exists for it, else 3.13). Defaults to auto when the build is detected, and to the server default (3.13) when distro and arch are given. product=python only."
                  },
                  "socket_only": {
                    "type": "boolean",
                    "description": "Retired and ignored: every machine is created with socket_only false, whatever is sent. Still accepted so older clients do not fail."
                  }
                },
                "required": [
                  "machine_name"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK. For example:\n\n```\n{\n  \"Name\": \"web-01\",\n  \"OsType\": \"auto\",\n  \"Status\": \"created\",\n  \"PublicID\": \"<public_id>\",\n  \"PythonVersion\": \"auto\",\n  \"SocketOnly\": \"false\",\n  \"AgentKind\": \"python\"\n}\n\n// product=browser omits the es-python-only fields:\n{\n  \"Name\": \"test-browser\",\n  \"OsType\": \"\",\n  \"Status\": \"created\",\n  \"PublicID\": \"<public_id>\",\n  \"AgentKind\": \"browser\"\n}\n```",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "AgentKind": "python",
                  "Name": "web-01",
                  "OsType": "auto",
                  "PublicID": "<public_id>",
                  "PythonVersion": "auto",
                  "SocketOnly": "false",
                  "Status": "created"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X POST https://eyalsec.com/api/add_new_machine/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/x-www-form-urlencoded' \\\n  --data 'machine_name=web-01'\n\n# es-python with the build chosen\ncurl -X POST https://eyalsec.com/api/add_new_machine/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/x-www-form-urlencoded' \\\n  --data 'machine_name=web-02&distro=ubuntu_24_04&arch=x86_64'\n\n# es-chromium: a name and nothing else\ncurl -X POST https://eyalsec.com/api/add_new_machine/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/x-www-form-urlencoded' \\\n  --data 'machine_name=test-browser&product=browser'"
          }
        ]
      }
    },
    "/api/set_machine_os/": {
      "post": {
        "tags": [
          "Machines"
        ],
        "operationId": "postSetMachineOs",
        "summary": "Set a machine's OS target",
        "description": "Sets a machine's OS target (os_type = distro_arch, validated against the supported targets), keyed by the machine's display name. 404 if no machine of that name is owned by the caller. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Machines",
          "url": "https://eyalsec.com/docs/api-machines"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "machine_name": {
                    "type": "string",
                    "description": "Display name of the target machine."
                  },
                  "distro": {
                    "type": "string",
                    "description": "Distro id, e.g. ubuntu_24_04."
                  },
                  "arch": {
                    "type": "string",
                    "description": "Architecture, e.g. x86_64; combined into os_type = distro_arch."
                  }
                },
                "required": [
                  "machine_name",
                  "distro",
                  "arch"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "machine_name": "web-01",
                  "os_type": "ubuntu_24_04_x86_64"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X POST https://eyalsec.com/api/set_machine_os/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/x-www-form-urlencoded' \\\n  --data 'machine_name=web-01&distro=ubuntu_24_04&arch=x86_64'"
          }
        ]
      }
    },
    "/api/rename_machine/": {
      "post": {
        "tags": [
          "Machines"
        ],
        "operationId": "postRenameMachine",
        "summary": "Rename a machine",
        "description": "Renames a machine identified by its public_id (JSON body). The new name is trimmed and must then be 1 to 128 characters (422 otherwise). 404 if the machine is not owned by the caller. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Machines",
          "url": "https://eyalsec.com/docs/api-machines"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "public_id": {
                    "type": "string",
                    "description": "The machine's public id."
                  },
                  "machine_name": {
                    "type": "string",
                    "description": "New display name (max 128 chars)."
                  }
                },
                "required": [
                  "public_id",
                  "machine_name"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "machine_name": "db-primary",
                  "public_id": "<public_id>"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X POST https://eyalsec.com/api/rename_machine/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"public_id\":\"<public_id>\",\"machine_name\":\"db-primary\"}'"
          }
        ]
      }
    },
    "/api/delete_machine/": {
      "post": {
        "tags": [
          "Machines"
        ],
        "operationId": "postDeleteMachine",
        "summary": "Delete a machine",
        "description": "Deletes a machine identified by its public_id (JSON body). 404 if the machine is not owned by the caller; on success replies 204 No Content with no body. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Machines",
          "url": "https://eyalsec.com/docs/api-machines"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "public_id": {
                    "type": "string",
                    "description": "The machine's public id."
                  }
                },
                "required": [
                  "public_id"
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Done. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X POST https://eyalsec.com/api/delete_machine/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"public_id\":\"<public_id>\"}'"
          }
        ]
      }
    },
    "/api/get_eyalsec_install_script_data/": {
      "post": {
        "tags": [
          "Machines"
        ],
        "operationId": "postGetEyalsecInstallScriptData",
        "summary": "Return the data needed to render a machine's install command",
        "description": "Mints a fresh single-use install secret for an es-python machine (JSON body) and returns the /install.sh URL, the machine's os_type (auto when its build is detected at install) and expires_in, the seconds the secret has left (it lasts 10 minutes). Requires attested=true (authorized-use attestation; 422 otherwise). You must hold the machine's product (403 otherwise). Needs os_type set (else 422). If the machine is installed, mid-install or failed it is reset to 'created' first (a re-install); the live credential is not rotated until the new install actually starts. With renew=true that reset never happens: a machine that is not 'created' is a 409 and is left alone. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Machines",
          "url": "https://eyalsec.com/docs/api-machines"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "public_id": {
                    "type": "string",
                    "description": "The machine's public id."
                  },
                  "attested": {
                    "type": "boolean",
                    "description": "Must be true: attests authorized use before a token is issued."
                  },
                  "renew": {
                    "type": "boolean",
                    "description": "true: only replace an expiring command; 409 (and no reset) if the machine has started installing."
                  }
                },
                "required": [
                  "public_id",
                  "attested"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "expires_in": 600,
                  "os_type": "ubuntu_24_04_x86_64",
                  "script_path": "https://eyalsec.com/install.sh",
                  "secret": "<single-use-secret>"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X POST https://eyalsec.com/api/get_eyalsec_install_script_data/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"public_id\":\"<public_id>\",\"attested\":true}'"
          }
        ]
      }
    },
    "/api/get_uninstall_script_data/": {
      "post": {
        "tags": [
          "Machines"
        ],
        "operationId": "postGetUninstallScriptData",
        "summary": "Get the command that removes es-python from a machine",
        "description": "Returns a one-time secret and the uninstall script path for one of your machines, the same command the Machines page shows. The secret is single-use with a short life. es-python machines only: the command removes that product and nothing else, so asking for another product's machine is a 422 (delete the machine instead).\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Machines",
          "url": "https://eyalsec.com/docs/api-machines"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "public_id": {
                    "type": "string",
                    "description": "The machine's public_id, from GET /api/get_all_machines/."
                  }
                },
                "required": [
                  "public_id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "script_path": "https://eyalsec.com/uninstall.sh",
                  "secret": "…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s https://eyalsec.com/api/get_uninstall_script_data/ -H \"X-API-Key: es2_3f9a…\" -H \"Content-Type: application/json\" -d '{\"public_id\":\"m_8f3c2a1b\"}'"
          }
        ]
      }
    },
    "/ci.sh": {
      "get": {
        "tags": [
          "CI"
        ],
        "operationId": "getCiSh",
        "summary": "The es-python CI driver script",
        "description": "A POSIX sh script that installs es-python on a CI runner with your API key (POST /api/ci/install/), runs your test command under it, then asks GET /api/ci/findings/ what the run reported and exits non-zero when anything is at or above your threshold. It is static text with no secret in it; the API key is read from EYALSEC_API_KEY at run time. Run it with sh, never source it. See the Run es-python in CI page of the guide.\n\nNeeds no authentication.",
        "externalDocs": {
          "description": "Guide: CI",
          "url": "https://eyalsec.com/docs/api-ci"
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "#!/bin/sh\n# es-python CI driver ..."
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [],
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -fsSL https://eyalsec.com/ci.sh -o es-python-ci.sh\nsh es-python-ci.sh run --machine ci-myrepo --distro ubuntu_24_04 --arch x86_64 -- es-python -m pytest"
          }
        ]
      }
    },
    "/api/ci/findings/": {
      "get": {
        "tags": [
          "CI"
        ],
        "operationId": "getCiFindings",
        "summary": "What one es-python machine's runs reported since a moment, for failing a CI build",
        "description": "Lists the es-python events that programs on one machine reported at or after since, and counts those at or above min_severity. An event counts when one of the machine's runs reached it inside the window, even if an earlier run had reached it too; new marks the ones no run had reached before the window. Only events the es-python events page would show are counted: your product access, monthly event quota, saved filters, your global and this machine's Hide rules and the live (not suppressed) bucket all apply, so a finding you hid or that was suppressed never fails a build. The value that reached the sink is not returned, because CI logs are often public; open the event on the dashboard (url) to see it. Add format=text for key=value lines that a shell script can read without a JSON parser. 404 when no es-python machine has that name, 409 when more than one does, 403 without es-python on your account.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: CI",
          "url": "https://eyalsec.com/docs/api-ci"
        },
        "parameters": [
          {
            "name": "machine",
            "in": "query",
            "description": "The machine's display name (exact match, es-python machines only).",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "Window start: RFC3339, Unix seconds, or an HTTP date such as a Date response header. Use server_time from POST /api/ci/install/ so the runner's clock does not matter.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "min_severity",
            "in": "query",
            "description": "critical, high (the default), medium, low or info. findings counts events at or above it.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "How many of those events to list, worst first, 0 to 100 (default 20). The counts always cover all of them.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "text for key=value lines instead of JSON.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "by_severity": {
                    "critical": 1,
                    "high": 1,
                    "info": 0,
                    "low": 0,
                    "medium": 3
                  },
                  "events": [
                    {
                      "count": 3,
                      "id": 90210,
                      "last_seen": "2026-10-07T12:03:10Z",
                      "new": true,
                      "severity": "critical",
                      "where": "os system"
                    }
                  ],
                  "findings": 2,
                  "hidden_by": "",
                  "machine": "ci-myrepo",
                  "min_severity": "high",
                  "new_findings": 1,
                  "since": "2026-10-07T12:00:00Z",
                  "total": 5,
                  "truncated": false,
                  "url": "https://eyalsec.com/scanner/events?machine=ci-myrepo&sev=critical%2Chigh"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s 'https://eyalsec.com/api/ci/findings/?machine=ci-myrepo&since=2026-10-07T12:00:00Z&min_severity=high' \\\n  -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/ci/install/": {
      "post": {
        "tags": [
          "CI"
        ],
        "operationId": "postCiInstall",
        "summary": "Get a one-time install secret for a CI machine, by name, creating the machine if needed",
        "description": "For a pipeline that holds only an API key. Finds your es-python machine with this machine_name, or creates it when there is none (distro and arch are then required, and your machine limit applies exactly as for POST /api/add_new_machine/). An existing machine is reset for a fresh install, as POST /api/get_eyalsec_install_script_data/ does, and keeps its name, configuration, rules and events, so one machine serves every run of a pipeline. distro and arch sent for an existing machine update its OS. Returns a single-use install secret that expires after 10 minutes, the install script URL, and server_time, the moment to pass as since to GET /api/ci/findings/ after the run. Requires attested=true. Add format=text for key=value lines. 409 when more than one es-python machine has the name, or when python_version differs from the existing machine's; 403 without es-python on your account.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: CI",
          "url": "https://eyalsec.com/docs/api-ci"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "machine_name": {
                    "type": "string",
                    "description": "The machine's display name, 1 to 128 characters."
                  },
                  "attested": {
                    "type": "boolean",
                    "description": "Must be true: you are authorized to monitor the computer this installs on."
                  },
                  "distro": {
                    "type": "string",
                    "description": "Required when the machine does not exist yet, such as ubuntu_24_04. See GET /api/supported_os_types/."
                  },
                  "arch": {
                    "type": "string",
                    "description": "x86_64 or aarch64; sent together with distro."
                  },
                  "python_version": {
                    "type": "string",
                    "description": "For a new machine; defaults to the usual default. For an existing one it must match (409 otherwise)."
                  },
                  "format": {
                    "type": "string",
                    "description": "text for key=value lines instead of JSON."
                  }
                },
                "required": [
                  "machine_name",
                  "attested"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "created": false,
                  "events_url": "https://eyalsec.com/scanner/events?machine=ci-myrepo",
                  "expires_in": 600,
                  "machine_name": "ci-myrepo",
                  "os_type": "ubuntu_24_04_x86_64",
                  "public_id": "<public_id>",
                  "python_version": "3.13",
                  "script_path": "https://eyalsec.com/install.sh",
                  "secret": "<single-use-secret>",
                  "server_time": "2026-10-07T12:00:00.123456Z"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s https://eyalsec.com/api/ci/install/ -H 'X-API-Key: <key>' \\\n  -d machine_name=ci-myrepo -d distro=ubuntu_24_04 -d arch=x86_64 -d attested=true"
          }
        ]
      }
    },
    "/api/filters/": {
      "get": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "getFilters",
        "summary": "List event filters",
        "description": "Lists the caller's saved event filters: whitelist/blacklist patterns matched against an event's where, origin, source or impact tag, applied to the events view. Each filter belongs to one filters page (engine) or to all of them (any).\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "parameters": [
          {
            "name": "engine",
            "in": "query",
            "description": "Narrow the list to one filters page: python, browser, go, c, cpp, rust, node, solidity, java, bash, ruby or php. Filters for any are always included. Omit for every filter. An unknown engine is a 400; one your account does not hold is a 403.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "filters": [
                    {
                      "enabled": true,
                      "engine": "any",
                      "id": 1,
                      "label": "Hide secrets",
                      "mode": "blacklist",
                      "pattern": "secret",
                      "target": "where"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl https://eyalsec.com/api/filters/ -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/filters/create/": {
      "post": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "postFiltersCreate",
        "summary": "Create an event filter",
        "description": "Creates an event filter (JSON body). mode must be blacklist or whitelist; target defaults to where and may be where, origin, source or tag. A source target is available to admin accounts only (403 for every other account); where, origin and tag are open to everyone. engine defaults to any. enabled defaults to true. pattern is at most 512 characters and label at most 128. Returns 201 with the created filter. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "pattern": {
                    "type": "string",
                    "description": "Match pattern, at most 512 characters."
                  },
                  "label": {
                    "type": "string",
                    "description": "Human label for the filter, at most 128 characters."
                  },
                  "mode": {
                    "type": "string",
                    "description": "blacklist (hide matches) or whitelist (show only matches)."
                  },
                  "target": {
                    "type": "string",
                    "description": "where (default), origin, source or tag. source is admin-only; tag takes an impact class label as its pattern, and an unknown label is a 400."
                  },
                  "engine": {
                    "type": "string",
                    "description": "The filters page this filter belongs to: any (default, every page) or one of python, browser, go, c, cpp, rust, node, solidity, java, bash, ruby or php. An engine your account does not hold is a 403."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "Whether the filter is active (default true)."
                  }
                },
                "required": [
                  "pattern",
                  "mode"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "enabled": true,
                  "engine": "any",
                  "id": 7,
                  "label": "",
                  "mode": "blacklist",
                  "pattern": "secret",
                  "target": "where"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X POST https://eyalsec.com/api/filters/create/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"pattern\":\"secret\",\"mode\":\"blacklist\",\"target\":\"where\"}'"
          }
        ]
      }
    },
    "/api/filters/{id}/update/": {
      "patch": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "patchFiltersByIdUpdate",
        "summary": "Update an event filter",
        "description": "Partially updates an event filter by id (path). Only the keys present in the JSON body are changed; pattern cannot be blanked, mode must stay blacklist/whitelist, and switching target to source is admin-only (403 for every other account). A tag target must end up with a known impact class label as its pattern (400 otherwise). The response does not echo engine. 404 if the filter is not owned by the caller. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Filter id.",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "pattern": {
                    "type": "string",
                    "description": "New pattern (cannot be empty, at most 512 characters)."
                  },
                  "label": {
                    "type": "string",
                    "description": "New label, at most 128 characters."
                  },
                  "mode": {
                    "type": "string",
                    "description": "blacklist or whitelist."
                  },
                  "target": {
                    "type": "string",
                    "description": "where, origin, source or tag. source is admin-only; tag takes an impact class label as its pattern."
                  },
                  "engine": {
                    "type": "string",
                    "description": "any or one of python, browser, go, c, cpp, rust, node, solidity, java, bash, ruby or php. An engine your account does not hold is a 403."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "Enable/disable the filter."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "enabled": false,
                  "id": 7,
                  "label": "",
                  "mode": "blacklist",
                  "pattern": "secret",
                  "target": "where"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X PATCH https://eyalsec.com/api/filters/7/update/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"enabled\":false}'"
          }
        ]
      }
    },
    "/api/filters/{id}/delete/": {
      "delete": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "deleteFiltersByIdDelete",
        "summary": "Delete an event filter",
        "description": "Deletes an event filter by id (path). 404 if the filter is not owned by the caller; on success replies 204 No Content. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Filter id.",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Done. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X DELETE https://eyalsec.com/api/filters/7/delete/ -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/machine_rules/": {
      "get": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "getMachineRules",
        "summary": "List machine rules",
        "description": "Lists display/raise/drop rules for the given scope: the optional machine query param is a machine public_id ('' or absent = global scope). The first global load seeds the default rule set. Each rule reports its id, source scope, event pattern, mode (1=hide,2=show,3=raise,4=drop), action (vestigial, always raise), polarity, label, enabled flag, sanitize_scope and match_on (the extra event fields a rule made from an event must also equal; empty for every other rule). 404 if the machine is not owned by the caller.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "parameters": [
          {
            "name": "machine",
            "in": "query",
            "description": "Machine public_id to scope to; omit for global rules.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "rules": [
                    {
                      "Action": "raise",
                      "Enabled": true,
                      "EventPattern": "",
                      "ID": 1,
                      "Label": "",
                      "Mode": 2,
                      "Polarity": "whitelist",
                      "Source": "any",
                      "match_on": [],
                      "sanitize_scope": "any"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl 'https://eyalsec.com/api/machine_rules/?machine=<public_id>' -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/machine_rules/create/": {
      "post": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "postMachineRulesCreate",
        "summary": "Create a machine rule",
        "description": "Creates a rule (JSON body) in the given scope (machine = a machine public_id, '' = global). A rule only ever affects the events its event_pattern matches: 1 hides them from the list, 2 shows only them, 3 raises on them, 4 stops them being sent. mode defaults to 2 (Show) and must be 1-4 (mode 3/Raise requires the raise grant, admins always allowed; else 403); event_pattern (empty = all events) must be a valid regex; source defaults to any, and scoping a rule to a source is open to every user. polarity is vestigial: it is validated if supplied but the stored value always follows the mode (4 = blacklist, else whitelist). A Raise rule (mode 3) takes neither a sanitize_scope other than any nor a match_on: each is a 400. from_event makes the rule from one of your events; match_on or event_machine without it is a 400, and an event that is not yours is a 404. Returns 201 with the created rule. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "machine": {
                    "type": "string",
                    "description": "Machine public_id to scope to; omit/empty for global."
                  },
                  "polarity": {
                    "type": "string",
                    "description": "whitelist or blacklist. Validated, then replaced by the value the mode implies."
                  },
                  "event_pattern": {
                    "type": "string",
                    "format": "regex",
                    "description": "Event-name regex; empty means all events (.*)."
                  },
                  "mode": {
                    "type": "integer",
                    "description": "1=hide, 2=show (default), 3=raise (requires the raise grant), 4=drop."
                  },
                  "label": {
                    "type": "string",
                    "description": "Human label for the rule."
                  },
                  "source": {
                    "type": "string",
                    "description": "Taint-source scope: any (default), a built-in source name, or a non-negative number (your own source n). Anything else is a 400."
                  },
                  "sanitize_scope": {
                    "type": "string",
                    "description": "Which events by sanitizer verdict the rule applies to: any (default), unsanitized, sanitized, conditional, or sanitized:<class>. Anything else is a 400."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "Whether the rule is active (default true)."
                  },
                  "from_event": {
                    "type": "integer",
                    "description": "An event id. Makes the rule from that event: an empty event_pattern becomes its sink, anchored."
                  },
                  "match_on": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "With from_event: any of source (the taint chain), trace, detail, origin. The rule then also requires those fields to equal the event's. An unknown name is a 400; not allowed with mode 3."
                  },
                  "event_machine": {
                    "type": "boolean",
                    "description": "With from_event: true scopes the rule to the machine that sent the event, in place of machine."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "Action": "raise",
                  "Enabled": true,
                  "EventPattern": "noise",
                  "ID": 12,
                  "Label": "",
                  "Mode": 1,
                  "Polarity": "whitelist",
                  "Source": "any",
                  "match_on": [],
                  "sanitize_scope": "any"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X POST https://eyalsec.com/api/machine_rules/create/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"polarity\":\"blacklist\",\"event_pattern\":\"noise\",\"mode\":1}'"
          }
        ]
      }
    },
    "/api/machine_rules/{id}/update/": {
      "patch": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "patchMachineRulesByIdUpdate",
        "summary": "Update a machine rule",
        "description": "Partially updates a rule by id (path); only keys present in the JSON body change. Same validation/gates as create: polarity in {whitelist,blacklist}, mode 1-4 (3/Raise requires the raise grant, admins always allowed), event_pattern a valid regex, source any, a built-in source name or a non-negative number (open to every user), sanitize_scope one of the values create accepts, and a rule that would end up as mode 3 with a sanitize_scope other than any, or with a match key, is a 400. A rule's match_on cannot be changed here. Changing mode re-normalizes the stored polarity to match it. 404 if not owned by the caller. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Rule id.",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "polarity": {
                    "type": "string",
                    "description": "whitelist or blacklist."
                  },
                  "event_pattern": {
                    "type": "string",
                    "format": "regex",
                    "description": "Event-name regex; empty means all events."
                  },
                  "mode": {
                    "type": "integer",
                    "description": "1=hide, 2=show, 3=raise (requires the raise grant), 4=drop."
                  },
                  "label": {
                    "type": "string",
                    "description": "Human label."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "Enable/disable the rule."
                  },
                  "source": {
                    "type": "string",
                    "description": "Taint-source scope: any, a built-in source name, or a non-negative number."
                  },
                  "sanitize_scope": {
                    "type": "string",
                    "description": "any, unsanitized, sanitized, conditional, or sanitized:<class>."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "Action": "raise",
                  "Enabled": false,
                  "EventPattern": "noise",
                  "ID": 12,
                  "Label": "",
                  "Mode": 1,
                  "Polarity": "whitelist",
                  "Source": "any",
                  "match_on": [],
                  "sanitize_scope": "any"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X PATCH https://eyalsec.com/api/machine_rules/12/update/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"enabled\":false}'"
          }
        ]
      }
    },
    "/api/machine_rules/{id}/delete/": {
      "delete": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "deleteMachineRulesByIdDelete",
        "summary": "Delete a machine rule",
        "description": "Deletes a rule by id (path). 404 if not owned by the caller; on success replies 204 No Content. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Rule id.",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Done. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X DELETE https://eyalsec.com/api/machine_rules/12/delete/ -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/machine_taint/": {
      "get": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "getMachineTaint",
        "summary": "Get a machine's taint configuration",
        "description": "Returns the tri-state taint config for the given scope (machine query param = a machine public_id, '' or absent = global). Every gate key is reported as on, off, or unset: socket, file, stdin, foreign, env, make_vuln, argv, weak_random, hardcoded, build, posture, concurrency, resource, secret, memory, ffi, lifetime, arith, identity, subprocess, worker, dns, db_rows, calldata, caller, returndata, storage, chainenv, oracle, http, report_cmdline, report_env, report_code, drop_suppressed, handoff. drop_suppressed is not a taint source: switched on, the server discards that machine's suppressed events as they arrive instead of storing them, and a discarded event cannot be recovered. Each product's filters page offers the subset listed for that product. A key an agent has no counterpart for is ignored by that agent, never mapped onto a different source. es-chromium, es-c, es-cpp, es-node, es-ruby and es-php do not fetch this configuration at all, so nothing set here reaches them.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "parameters": [
          {
            "name": "machine",
            "in": "query",
            "description": "Machine public_id to scope to; omit for global.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK. For example:\n\n```\n{\n  \"taint\": {\n    \"socket\": \"on\",\n    \"file\": \"unset\",\n    \"stdin\": \"unset\",\n    \"foreign\": \"off\",\n    \"env\": \"unset\",\n    …one entry for every gate key\n  }\n}\n```",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl 'https://eyalsec.com/api/machine_taint/?machine=<public_id>' -H 'X-API-Key: <key>'"
          }
        ]
      },
      "put": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "putMachineTaint",
        "summary": "Set a machine's taint configuration",
        "description": "Sets one taint source's state for the given scope (JSON body). source must be one of the gate keys GET /api/machine_taint/ reports (anything else is a 400); state must be on, off, or unset; machine is a machine public_id ('' = global). Gate keys use underscores and are NOT always the name a rule's source field takes: gate weak_random is rule source weak-random, and gate db_rows is rule source db. On success replies 204 No Content. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "machine": {
                    "type": "string",
                    "description": "Machine public_id to scope to; omit/empty for global."
                  },
                  "source": {
                    "type": "string",
                    "description": "Gate key: socket, file, stdin, foreign, env, make_vuln, argv, weak_random, hardcoded, build, posture, concurrency, resource, secret, memory, ffi, lifetime, arith, identity, subprocess, worker, dns, db_rows, calldata, caller, returndata, storage, chainenv, oracle, http, report_cmdline, report_env, report_code, drop_suppressed, handoff."
                  },
                  "state": {
                    "type": "string",
                    "description": "on, off, or unset."
                  }
                },
                "required": [
                  "source",
                  "state"
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Done. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X PUT https://eyalsec.com/api/machine_taint/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"machine\":\"<public_id>\",\"source\":\"socket\",\"state\":\"on\"}'"
          }
        ]
      }
    },
    "/api/web_protection/": {
      "get": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "getWebProtection",
        "summary": "Get es-python's web protection settings",
        "description": "Returns the web protection settings stored in one scope (machine query param = a machine public_id, '' or absent = your account) and the values that apply where a setting is not stored there: the defaults for the account, the account's values for a machine. Keys: client (header or ip), client_header (a header name), client_proxies (1-16), request_context (on/off), rate (on/off), rate.limit (1-1000000 requests), rate.window (1-86400 seconds), rate.action (block or report), block (on/off: refuse the addresses on your blocked clients list), check.smuggling, check.reflect, check.redirect, check.host, check.cards, check.headers (on/off), hosts (your host names, comma separated, for check.host), routes (on/off: keep the route inventory), and, on the account only, autoblock (off, attacks or severe) and autoblock.hours (1-720). A stored value of \"\" means not set in this scope. can_block is false when your plan has no Raise: blocking settings then cannot be saved, and inherited values show them downgraded to report-only.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "parameters": [
          {
            "name": "machine",
            "in": "query",
            "description": "Machine public_id to scope to; omit for your account.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK. For example:\n\n```\n{\n  \"settings\": {\"rate\": \"on\", \"rate.limit\": \"\", \"client_header\": \"X-Real-IP\", …one entry for every key},\n  \"inherited\": {\"rate\": \"off\", \"rate.limit\": \"100\", \"client_header\": \"X-Forwarded-For\", …},\n  \"can_block\": true\n}\n```",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl 'https://eyalsec.com/api/web_protection/?machine=<public_id>' -H 'X-API-Key: <key>'"
          }
        ]
      },
      "put": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "putWebProtection",
        "summary": "Set one web protection setting",
        "description": "Stores one setting in one scope (JSON body: machine, key, value). value \"\" clears it, so the scope inherits again. An unknown key or a value out of range is a 400 with the reason. autoblock and autoblock.hours are account-wide: setting them on a machine is a 400. Settings that refuse requests (block on, rate.action block, autoblock on) need Raise on your plan, else 403. Machines pick a change up within about 30 seconds. On success replies 204 No Content. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "machine": {
                    "type": "string",
                    "description": "Machine public_id to scope to; omit/empty for your account."
                  },
                  "key": {
                    "type": "string",
                    "description": "A key GET /api/web_protection/ returns."
                  },
                  "value": {
                    "type": "string",
                    "description": "The new value, or \"\" to clear it."
                  }
                },
                "required": [
                  "key",
                  "value"
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Done. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X PUT https://eyalsec.com/api/web_protection/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"machine\":\"\",\"key\":\"rate\",\"value\":\"on\"}'"
          }
        ]
      }
    },
    "/api/web_protection/blocklist/": {
      "get": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "getWebProtectionBlocklist",
        "summary": "List your blocked clients",
        "description": "Returns your account's blocked clients, newest first: the addresses and ranges your es-python machines refuse with 403 while the block setting is on. source is manual for an entry you added and auto for one auto-block added; expires_at is null for an entry that never expires. Expired entries are left out. limit is the most entries one account can hold.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "entries": [
                    {
                      "address": "203.0.113.0/24",
                      "created_at": "2026-10-14T09:12:03Z",
                      "expires_at": null,
                      "id": 4,
                      "reason": "scanner",
                      "source": "manual"
                    }
                  ],
                  "limit": 5000
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl https://eyalsec.com/api/web_protection/blocklist/ -H 'X-API-Key: <key>'"
          }
        ]
      },
      "post": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "postWebProtectionBlocklist",
        "summary": "Block a client address or range",
        "description": "Adds an IP address or CIDR range to your blocked clients and answers 201 with the entry (JSON body: address, optional reason, optional hours until it expires, 0 or absent = never, at most a year). A range wider than /8 (IPv4) or /32 (IPv6) is a 400. Adding an address that is already listed replaces its reason and expiry and makes it a manual entry. A full list is a 409. Adding works without Raise; the list is only enforced when block is on, which needs it. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "address": {
                    "type": "string",
                    "description": "An IPv4 or IPv6 address, or a CIDR range."
                  },
                  "reason": {
                    "type": "string",
                    "description": "A note shown beside the entry, up to 200 characters."
                  },
                  "hours": {
                    "type": "integer",
                    "description": "Hours until the entry expires; 0 or absent for never."
                  }
                },
                "required": [
                  "address"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "address": "203.0.113.7",
                  "created_at": "2026-10-14T09:20:00Z",
                  "expires_at": "2026-10-15T09:20:00Z",
                  "id": 5,
                  "reason": "scanner",
                  "source": "manual"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X POST https://eyalsec.com/api/web_protection/blocklist/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"address\":\"203.0.113.7\",\"reason\":\"scanner\",\"hours\":24}'"
          }
        ]
      }
    },
    "/api/web_protection/blocklist/{id}/": {
      "delete": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "deleteWebProtectionBlocklistById",
        "summary": "Unblock a client",
        "description": "Removes one entry from your blocked clients and answers 204. An entry that is not yours is a 404. Machines stop refusing the address within about 30 seconds. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Entry id from GET /api/web_protection/blocklist/.",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Done. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X DELETE https://eyalsec.com/api/web_protection/blocklist/5/ -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/web_protection/routes/": {
      "get": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "getWebProtectionRoutes",
        "summary": "List the web routes your es-python applications served",
        "description": "Returns the route inventory web protection keeps while its routes setting is on: per machine, each (method, route) the application answered, with the number of requests and of 2xx, 3xx, 4xx and 5xx answers, when it was last seen, and events: how many of your events happened in a request on that route. Variable path parts are folded (/users/{n}, /files/{uuid}). Routes that produced events come first, then those with the most 5xx answers, then the busiest; at most 500.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "routes": [
                    {
                      "events": 3,
                      "hits": 5120,
                      "last_seen": "2026-10-15T08:10:00Z",
                      "machine": "shop-1",
                      "method": "GET",
                      "route": "/search",
                      "s2xx": 5100,
                      "s3xx": 0,
                      "s4xx": 18,
                      "s5xx": 2
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl https://eyalsec.com/api/web_protection/routes/ -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/event_uniqueness/": {
      "get": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "getEventUniqueness",
        "summary": "Get the unique-event definition",
        "description": "Returns which event fields make two detections DIFFERENT events for the given scope (machine query param = a machine public_id, '' or absent = the account default). `axes` is the definition in force after resolution (machine setting, else account default, else the built-in default of source+detail); `inherited` says whether this exact scope carries its own row or is inheriting one. `baseline` names the parts that are always keyed and cannot be switched off. Fields not listed in `axes` do not split events: two detections differing only in an unlisted field are one row whose count increments.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "parameters": [
          {
            "name": "agent_kind",
            "in": "query",
            "description": "Product to read the setting for, e.g. python.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "machine",
            "in": "query",
            "description": "Machine public_id to scope to; omit for the account default.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "event",
            "in": "query",
            "description": "An event id. Scopes to the machine that sent that event, in place of machine; 404 if the event is not yours.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "agent_kind": "python",
                  "available": [
                    "source",
                    "detail",
                    "trace",
                    "machine",
                    "origin"
                  ],
                  "axes": [
                    "source",
                    "detail"
                  ],
                  "baseline": [
                    "account",
                    "agent",
                    "sink"
                  ],
                  "inherited": true,
                  "machine": ""
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl 'https://eyalsec.com/api/event_uniqueness/?agent_kind=python' -H 'X-API-Key: <key>'"
          }
        ]
      },
      "put": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "putEventUniqueness",
        "summary": "Set the unique-event definition",
        "description": "Sets which fields split detections into separate events for the given scope. `axes` is any subset of source, detail, trace, machine, origin; an empty array keeps only the baseline (account, agent, sink); a name outside that list is ignored, not rejected. A product your account does not hold is a 400. OMITTING `axes` entirely clears this scope's row so it inherits the level below it; that is how you reset a machine to the account default. Applies to detections recorded from now on: it does not re-group what is already stored, and a field that was not keyed was never stored per-occurrence, so no later change can split history that has already merged. On success replies 204 No Content. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "machine": {
                    "type": "string",
                    "description": "Machine public_id to scope to; omit/empty for the account default."
                  },
                  "agent_kind": {
                    "type": "string",
                    "description": "Product the setting applies to, e.g. python."
                  },
                  "axes": {
                    "type": "array",
                    "description": "Subset of source, detail, trace, machine, origin. Omit to clear this scope."
                  }
                },
                "required": [
                  "agent_kind"
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Done. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X PUT https://eyalsec.com/api/event_uniqueness/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"agent_kind\":\"python\",\"axes\":[\"source\",\"detail\",\"trace\"]}'"
          }
        ]
      }
    },
    "/api/egress/": {
      "get": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "getEgress",
        "summary": "Get the outbound connection rules",
        "description": "Returns the es-python outbound connection policy for one scope (machine query param = a machine public_id; omit it for the account policy). `scope` is what this scope stores itself: a null mode, default or on_violation means it inherits. `effective` is what applies after resolving (machine, then account, then the built-in untrusted / allow / block) and lists the rules in the order they are checked: the machine's own first, then the account's. For a machine, `account` repeats the account policy so you can see what it inherits. Accounts without es-python get 403; a machine of another product gets 400.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "parameters": [
          {
            "name": "machine",
            "in": "query",
            "description": "An es-python machine public_id; omit for the account policy.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "defaults": {
                    "default": "allow",
                    "mode": "untrusted",
                    "on_violation": "block"
                  },
                  "effective": {
                    "default": "deny",
                    "mode": "untrusted",
                    "on_violation": "block",
                    "rules": [
                      {
                        "action": "allow",
                        "note": "payments",
                        "target": "*.stripe.com:443"
                      }
                    ]
                  },
                  "keywords": [
                    "@internal",
                    "@metadata"
                  ],
                  "machine": "",
                  "max_rules": 128,
                  "scope": {
                    "default": "deny",
                    "mode": null,
                    "on_violation": null,
                    "rules": [
                      {
                        "action": "allow",
                        "note": "payments",
                        "target": "*.stripe.com:443"
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl 'https://eyalsec.com/api/egress/' -H 'X-API-Key: <key>'"
          }
        ]
      },
      "put": {
        "tags": [
          "Filters, rules and taint"
        ],
        "operationId": "putEgress",
        "summary": "Set the outbound connection rules",
        "description": "Replaces one scope's outbound connection policy: its three settings and its whole ordered rule list, in one step. `mode` is untrusted (check only destinations that untrusted input chose) or all (check every connection); `default` is allow or deny (what happens when no rule matches); `on_violation` is block or report. Null or an empty string for any of the three means inherit. Each rule is {action: allow|deny, target, note}; a target is *, an IP address, a CIDR range, a host name, *.domain, @internal, @metadata, or an http(s) URL prefix, with an optional :port. Targets come back in canonical form. A bad row is a 400 naming the row number. At most 128 rules per scope. Machines pick the change up on their next config fetch, within about 30 seconds. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Filters, rules and taint",
          "url": "https://eyalsec.com/docs/api-rules"
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "machine": {
                    "type": "string",
                    "description": "An es-python machine public_id; omit/empty for the account policy."
                  },
                  "mode": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "untrusted, all, or null to inherit."
                  },
                  "default": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "allow, deny, or null to inherit."
                  },
                  "on_violation": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "block, report, or null to inherit."
                  },
                  "rules": {
                    "type": "array",
                    "description": "Ordered [{action, target, note}]. Replaces the stored list; [] or omitted clears it."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK. For example:\n\n```\n200 with the same body as GET, carrying the stored values.\n```",
            "content": {
              "application/json": {
                "schema": {
                  "type": "number"
                },
                "example": 200
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X PUT https://eyalsec.com/api/egress/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"default\":\"allow\",\"rules\":[{\"action\":\"deny\",\"target\":\"@internal\"},{\"action\":\"deny\",\"target\":\"@metadata\"}]}'"
          }
        ]
      }
    },
    "/api/rule_templates/": {
      "get": {
        "tags": [
          "Rule templates"
        ],
        "operationId": "getRuleTemplates",
        "summary": "List rule templates",
        "description": "Returns every template the caller may see: their own personal templates plus the EyalSec templates (the ready-made set EyalSec ships and any an administrator published). Each carries its items in authored order. owner is \"me\" or \"eyalsec\"; the ready-made templates also carry product, the agent kind their rules are written for (e.g. \"python\"), and category, the key of one of the groups listed in categories ({key, name, blurb}, in display order).\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Rule templates",
          "url": "https://eyalsec.com/docs/api-templates"
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "categories": [
                    {
                      "blurb": "Stop the attack, not only report it. Needs Raise.",
                      "key": "protect",
                      "name": "Block attacks"
                    }
                  ],
                  "templates": [
                    {
                      "description": "",
                      "id": 3,
                      "items": [
                        {
                          "event_pattern": "exec|eval",
                          "mode": 2,
                          "position": 0,
                          "sanitize_scope": "any",
                          "source": "socket"
                        }
                      ],
                      "name": "OWASP basics",
                      "owner": "eyalsec"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl 'https://eyalsec.com/api/rule_templates/' -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/rule_templates/create/": {
      "post": {
        "tags": [
          "Rule templates"
        ],
        "operationId": "postRuleTemplatesCreate",
        "summary": "Create a rule template",
        "description": "Creates a personal rule template owned by the caller. Items are validated exactly as machine rules are: source must be any/a built-in source name/a non-negative number, a non-empty event_pattern must be a valid regex (empty means all events), mode must be 1-4, and sanitize_scope must be any/unsanitized/sanitized/conditional/sanitized:<CLASS> (absent defaults to any); a mode 3 item with a sanitize_scope other than any is a 400. name is at most 128 bytes and description at most 512. Maximum 50 templates per user and 200 items per template. Names are unique per owner, case-insensitively. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Rule templates",
          "url": "https://eyalsec.com/docs/api-templates"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Template name (unique per owner)."
                  },
                  "description": {
                    "type": "string",
                    "description": "Free-text description."
                  },
                  "items": {
                    "type": "array",
                    "description": "Rules: source, event_pattern, mode, sanitize_scope. Omitted or empty makes a template with no rules."
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "description": "",
                  "id": 12,
                  "items": [
                    {
                      "event_pattern": "exec|eval",
                      "mode": 2,
                      "position": 0,
                      "sanitize_scope": "any",
                      "source": "socket"
                    }
                  ],
                  "name": "My web app",
                  "owner": "me"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X POST https://eyalsec.com/api/rule_templates/create/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"name\":\"My web app\",\"items\":[{\"source\":\"socket\",\"event_pattern\":\"exec|eval\",\"mode\":2}]}'"
          }
        ]
      }
    },
    "/api/rule_templates/{id}/update/": {
      "patch": {
        "tags": [
          "Rule templates"
        ],
        "operationId": "patchRuleTemplatesByIdUpdate",
        "summary": "Update a rule template",
        "description": "Renames a personal template and REPLACES its item list with the one supplied. 404 if the template is not owned by the caller, which includes every EyalSec template (those are writable only by an administrator). Replies 204 No Content. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Rule templates",
          "url": "https://eyalsec.com/docs/api-templates"
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Template id.",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Template name."
                  },
                  "description": {
                    "type": "string",
                    "description": "Free-text description."
                  },
                  "items": {
                    "type": "array",
                    "description": "The complete new item list (replaces the old one). Omitted or empty leaves the template with no rules."
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Done. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X PATCH https://eyalsec.com/api/rule_templates/12/update/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"name\":\"My web app\",\"items\":[]}'"
          }
        ]
      }
    },
    "/api/rule_templates/{id}/delete/": {
      "delete": {
        "tags": [
          "Rule templates"
        ],
        "operationId": "deleteRuleTemplatesByIdDelete",
        "summary": "Delete a rule template",
        "description": "Deletes a personal template and its items. 404 if the template is not owned by the caller. Rules already applied from it are unaffected: apply copies rules, it does not link them. Replies 204 No Content. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Rule templates",
          "url": "https://eyalsec.com/docs/api-templates"
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Template id.",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Done. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X DELETE https://eyalsec.com/api/rule_templates/12/delete/ -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/rule_templates/{id}/apply/": {
      "post": {
        "tags": [
          "Rule templates"
        ],
        "operationId": "postRuleTemplatesByIdApply",
        "summary": "Apply a rule template to a rule scope",
        "description": "Copies the template's items into a rule scope as ordinary rules: enabled, with the polarity the mode implies. machine is a machine public_id, or empty for the caller's global rules. An item is skipped when an identical rule (same source, event_pattern, mode and sanitize_scope) is already in the scope, when it repeats an earlier item of the same template, and when it is a Raise rule (mode 3) and the caller lacks the raise grant or the item carries a sanitize_scope other than any. Every skip is counted in the response. 404 if the template is not visible to the caller. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Rule templates",
          "url": "https://eyalsec.com/docs/api-templates"
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Template id (own or EyalSec).",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "machine": {
                    "type": "string",
                    "description": "Machine public_id, or empty for global rules."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "added": 5,
                  "skipped_duplicate": 1,
                  "skipped_raise": 2
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X POST https://eyalsec.com/api/rule_templates/3/apply/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' --data '{\"machine\":\"\"}'"
          }
        ]
      }
    },
    "/api/rule_templates/snapshot/": {
      "post": {
        "tags": [
          "Rule templates"
        ],
        "operationId": "postRuleTemplatesSnapshot",
        "summary": "Save a scope's rules as a new template",
        "description": "Captures the rules currently in a scope (machine public_id, or empty for global) as a new personal template. Disabled rules are captured too, since a template item has no enabled flag. A rule's match_on is not captured, so a rule made from an event becomes a broader item that matches on its pattern alone. Subject to the same 50-template and 200-item ceilings as create. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Rule templates",
          "url": "https://eyalsec.com/docs/api-templates"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Template name (unique per owner)."
                  },
                  "description": {
                    "type": "string",
                    "description": "Free-text description."
                  },
                  "machine": {
                    "type": "string",
                    "description": "Machine public_id, or empty for global rules."
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "description": "",
                  "id": 13,
                  "items": [],
                  "name": "prod baseline",
                  "owner": "me"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X POST https://eyalsec.com/api/rule_templates/snapshot/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"name\":\"prod baseline\",\"machine\":\"\"}'"
          }
        ]
      }
    },
    "/api/event_labels/": {
      "get": {
        "tags": [
          "Labels and notes"
        ],
        "operationId": "getEventLabels",
        "summary": "List your event labels",
        "description": "Lists the caller's own triage labels (colored tags for marking events), ordered by name.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Labels and notes",
          "url": "https://eyalsec.com/docs/api-labels"
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "labels": [
                    {
                      "color": "amber",
                      "id": 1,
                      "name": "triage"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl https://eyalsec.com/api/event_labels/ -H 'X-API-Key: <key>'"
          }
        ]
      },
      "post": {
        "tags": [
          "Labels and notes"
        ],
        "operationId": "postEventLabels",
        "summary": "Create an event label",
        "description": "Creates a triage label (JSON body). name is required, trimmed, and at most 48 characters; names are unique per user, case-insensitively, so a duplicate replies 409. color must be one of amber, cyan, red, green, blue, slate, violet, pink; an unrecognized value silently falls back to amber rather than failing the request, since the color is cosmetic. Returns 201 with the created label. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Labels and notes",
          "url": "https://eyalsec.com/docs/api-labels"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Label name, at most 48 characters, unique per user (case-insensitive)."
                  },
                  "color": {
                    "type": "string",
                    "description": "One of amber, cyan, red, green, blue, slate, violet, pink. Unknown values default to amber."
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "color": "amber",
                  "id": 1,
                  "name": "triage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X POST https://eyalsec.com/api/event_labels/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"name\":\"triage\",\"color\":\"amber\"}'"
          }
        ]
      }
    },
    "/api/event_labels/{id}/": {
      "patch": {
        "tags": [
          "Labels and notes"
        ],
        "operationId": "patchEventLabelsById",
        "summary": "Rename or recolor an event label",
        "description": "Replaces a label's name and color by id (path). Both are written on every call, so leaving color out resets it to amber. name follows the same rules as create, and renaming onto another of your labels' names replies 409. 404 if the label is not owned by the caller. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Labels and notes",
          "url": "https://eyalsec.com/docs/api-labels"
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Label id.",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "New name, at most 48 characters."
                  },
                  "color": {
                    "type": "string",
                    "description": "New color; unknown values default to amber."
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Done. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X PATCH https://eyalsec.com/api/event_labels/1/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"name\":\"reviewed\",\"color\":\"green\"}'"
          }
        ]
      },
      "delete": {
        "tags": [
          "Labels and notes"
        ],
        "operationId": "deleteEventLabelsById",
        "summary": "Delete an event label",
        "description": "Deletes a label by id (path); this also removes it from every event it was attached to. 404 if not owned by the caller. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Labels and notes",
          "url": "https://eyalsec.com/docs/api-labels"
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Label id.",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Done. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X DELETE https://eyalsec.com/api/event_labels/1/ -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/event/{id}/labels/": {
      "post": {
        "tags": [
          "Labels and notes"
        ],
        "operationId": "postEventByIdLabels",
        "summary": "Attach a label to an event",
        "description": "Attaches one of the caller's labels to one of the caller's events. Re-attaching an already-attached label is a silent no-op, not an error, since the events-page toggle must survive a double-click. Both ids must belong to the caller, and event ids are sequential and enumerable, so a foreign event id replies 404, the same as a nonexistent one, never 403. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Labels and notes",
          "url": "https://eyalsec.com/docs/api-labels"
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Event id.",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "label_id": {
                    "type": "integer",
                    "description": "Label id to attach."
                  }
                },
                "required": [
                  "label_id"
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Done. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X POST https://eyalsec.com/api/event/12345/labels/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"label_id\":1}'"
          }
        ]
      }
    },
    "/api/event/{id}/labels/{labelID}/": {
      "delete": {
        "tags": [
          "Labels and notes"
        ],
        "operationId": "deleteEventByIdLabelsByLabelID",
        "summary": "Detach a label from an event",
        "description": "Removes one label from one event, both scoped to the caller. Detaching a label that was not attached is a no-op. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Labels and notes",
          "url": "https://eyalsec.com/docs/api-labels"
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Event id.",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "labelID",
            "in": "path",
            "description": "Label id to detach.",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Done. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X DELETE https://eyalsec.com/api/event/12345/labels/1/ -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/event/{id}/comment/": {
      "put": {
        "tags": [
          "Labels and notes"
        ],
        "operationId": "putEventByIdComment",
        "summary": "Write your note on an event",
        "description": "Replaces the caller's single note on an event (there is one note per user per event; writing again overwrites the body). body is at most 4096 bytes. Event ids are sequential and enumerable, so a foreign event id replies 404, never 403. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Labels and notes",
          "url": "https://eyalsec.com/docs/api-labels"
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Event id.",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "body": {
                    "type": "string",
                    "description": "Note text, at most 4096 bytes. Omitted or empty stores an empty note."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Done. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X PUT https://eyalsec.com/api/event/12345/comment/ \\\n  -H 'X-API-Key: <key>' -H 'Content-Type: application/json' \\\n  --data '{\"body\":\"looks like a false positive\"}'"
          }
        ]
      },
      "delete": {
        "tags": [
          "Labels and notes"
        ],
        "operationId": "deleteEventByIdComment",
        "summary": "Delete your note on an event",
        "description": "Deletes the caller's note on an event, if any. Cookie callers must pass CSRF.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Labels and notes",
          "url": "https://eyalsec.com/docs/api-labels"
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Event id.",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Done. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X DELETE https://eyalsec.com/api/event/12345/comment/ -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/activity/": {
      "get": {
        "tags": [
          "Activity and account"
        ],
        "operationId": "getActivity",
        "summary": "Read your account's activity log",
        "description": "The same audit trail the Activity page shows: logins, machine changes, key generation and so on. Newest first, 50 per page. Page with before_ts and before_id from the last row you saw. Each entry carries time, id, action, description, target, source_ip, auth_method (session or apikey) and outcome. Entries are kept for 30 days.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Activity and account",
          "url": "https://eyalsec.com/docs/api-account"
        },
        "parameters": [
          {
            "name": "action",
            "in": "query",
            "description": "Filter by action, e.g. machine.create, auth.login, apikey.generate.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "outcome",
            "in": "query",
            "description": "success or failure.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "before_ts",
            "in": "query",
            "description": "Paging pointer: the time of the last row you saw (RFC3339). An unparseable value is ignored.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "before_id",
            "in": "query",
            "description": "Paging pointer: the id of the last row you saw. Only read together with before_ts.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "entries": [
                    {
                      "action": "machine.create",
                      "auth_method": "apikey",
                      "description": "Created machine web-prod-01",
                      "id": 4812,
                      "outcome": "success",
                      "source_ip": "203.0.113.4",
                      "target": "web-prod-01",
                      "time": "2026-07-16T06:20:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s \"https://eyalsec.com/api/activity/?action=machine.create&outcome=success\" -H \"X-API-Key: es2_3f9a…\""
          }
        ]
      }
    },
    "/api/onboarding_state/": {
      "get": {
        "tags": [
          "Activity and account"
        ],
        "operationId": "getOnboardingState",
        "summary": "Where the account is in setting up es-python",
        "description": "Returns the steps of the dashboard's setup checklist for es-python and the next one to do. has_plan is false until a plan is set up for the account. machines_total, machines_installed and machines_pending count es-python machines by status. sources_on is true when at least one es-python source of untrusted data is On for the account or for one of its es-python machines. ran is true once es-python has been started on a machine. events_total counts es-python events, leaving out self-test events, and stops counting at 100. selftest_seen is true when `eyalsec selftest` has reached the dashboard in the last 30 days. next_step is one of plan, add_machine, install, sources, run, first_event, done. empty_reason is one plain sentence saying why the es-python events list is empty, or empty when there is no setup reason. Cheap enough to poll every few seconds.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Activity and account",
          "url": "https://eyalsec.com/docs/api-account"
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "empty_reason": "There are no events yet because every source of untrusted data is off. Switch on network input and file reads to start watching.",
                  "events_total": 0,
                  "has_plan": true,
                  "has_python": true,
                  "machines_installed": 1,
                  "machines_pending": 0,
                  "machines_total": 1,
                  "next_step": "sources",
                  "ran": false,
                  "selftest_seen": false,
                  "sources_on": false
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl https://eyalsec.com/api/onboarding_state/ -H 'X-API-Key: <key>'"
          }
        ]
      }
    },
    "/api/account/": {
      "get": {
        "tags": [
          "Activity and account"
        ],
        "operationId": "getAccount",
        "summary": "Read your profile, limits and grants",
        "description": "Returns what the Settings page shows: your username, recovery email, timezone, which second factors are on, your machine and event limits, and whether raise rules are enabled for you. It never returns your password hash, your TOTP secret, an API key or a machine secret.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Activity and account",
          "url": "https://eyalsec.com/docs/api-account"
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "email": "alice@example.com",
                  "email_2fa_enabled": false,
                  "email_verified": true,
                  "event_limit": 5000,
                  "event_limit_monthly": true,
                  "grant_raise": false,
                  "machine_limit": 5,
                  "timezone": "Asia/Jerusalem",
                  "totp_enabled": true,
                  "username": "alice"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s https://eyalsec.com/api/account/ -H \"X-API-Key: es2_3f9a…\""
          }
        ]
      }
    },
    "/api/account/timezone": {
      "post": {
        "tags": [
          "Activity and account"
        ],
        "operationId": "postAccountTimezone",
        "summary": "Set the timezone your dates are displayed in",
        "description": "No password needed, the same as the Settings page. Send an IANA zone name. An empty value means auto (your browser's zone) and clears the stored one.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Activity and account",
          "url": "https://eyalsec.com/docs/api-account"
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "timezone": {
                    "type": "string",
                    "description": "IANA zone, e.g. Asia/Jerusalem. Empty or omitted = auto."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "timezone": "Asia/Jerusalem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s https://eyalsec.com/api/account/timezone -H \"X-API-Key: es2_3f9a…\" -d \"timezone=Asia/Jerusalem\""
          }
        ]
      }
    },
    "/api/account/email": {
      "post": {
        "tags": [
          "Activity and account"
        ],
        "operationId": "postAccountEmail",
        "summary": "Change or remove your recovery email. Needs your password and 2FA",
        "description": "Your recovery email is where password resets go, so changing it asks for your password AND, when an authenticator app is enrolled, a code from it, exactly like the dashboard (email 2FA alone asks for no code). A NEW address is not applied straight away: a single-use confirmation link is emailed to it and the change only lands when that link is clicked. Sending an empty address removes the recovery email immediately, which also disables email 2FA and signs out every session.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Activity and account",
          "url": "https://eyalsec.com/docs/api-account"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "The new address. Empty or omitted REMOVES the current one."
                  },
                  "password": {
                    "type": "string",
                    "description": "Your current password."
                  },
                  "totp": {
                    "type": "string",
                    "description": "A current TOTP code, or a one-time backup code. Required only when an authenticator app is enrolled."
                  }
                },
                "required": [
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK. For example:\n\n```\n{\n  \"status\": \"confirmation_sent\",\n  \"email\": \"new@example.com\",\n  \"note\": \"not applied yet: click the link sent to the new address to confirm\"\n}\n\nRemoving the address replies instead:\n{\n  \"status\": \"email_removed\",\n  \"note\": \"every session was revoked; email 2FA was disabled\"\n}\n```",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "email": "new@example.com",
                  "note": "not applied yet: click the link sent to the new address to confirm",
                  "status": "confirmation_sent"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s https://eyalsec.com/api/account/email -H \"X-API-Key: es2_3f9a…\" -d \"email=new@example.com\" -d \"password=…\" -d \"totp=123456\""
          }
        ]
      }
    },
    "/api/account/password": {
      "post": {
        "tags": [
          "Activity and account"
        ],
        "operationId": "postAccountPassword",
        "summary": "Change your password. REVOKES your API key",
        "description": "Asks for your current password, the same as the dashboard. A successful change revokes your API key and signs out every session, so the key making this call stops working the moment it succeeds. Generate a new one afterwards.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Activity and account",
          "url": "https://eyalsec.com/docs/api-account"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "current_password": {
                    "type": "string",
                    "description": "Your existing password."
                  },
                  "new_password": {
                    "type": "string",
                    "description": "The new password."
                  },
                  "confirm": {
                    "type": "string",
                    "description": "Repeat of new_password. Optional over the API; when sent it must match."
                  }
                },
                "required": [
                  "current_password",
                  "new_password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "note": "your API key and all sessions were revoked; generate a new key",
                  "status": "password_changed"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s https://eyalsec.com/api/account/password -H \"X-API-Key: es2_3f9a…\" -d \"current_password=…\" -d \"new_password=…\""
          }
        ]
      }
    },
    "/api/account/delete": {
      "post": {
        "tags": [
          "Activity and account"
        ],
        "operationId": "postAccountDelete",
        "summary": "Delete your account and everything in it. Permanent",
        "description": "Asks for your password. Removes the account and all its data. There is no undo.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Activity and account",
          "url": "https://eyalsec.com/docs/api-account"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "password": {
                    "type": "string",
                    "description": "Your current password."
                  }
                },
                "required": [
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Done. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s -X POST https://eyalsec.com/api/account/delete -H \"X-API-Key: es2_3f9a…\" -d \"password=…\""
          }
        ]
      }
    },
    "/api/account/2fa/setup": {
      "get": {
        "tags": [
          "Activity and account"
        ],
        "operationId": "getAccount2faSetup",
        "summary": "Start setting up an authenticator app",
        "description": "Returns a new secret and an otpauth:// URL you can turn into a QR code. The secret is held for you server-side for 10 minutes; finish with /api/account/2fa/enable. Calling this again starts over with a new secret, which is why a read-only key cannot call it.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Activity and account",
          "url": "https://eyalsec.com/docs/api-account"
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "expires_in_seconds": 600,
                  "note": "call /api/account/2fa/enable with a code from this secret to activate it",
                  "otpauth_url": "otpauth://totp/EyalSec:alice?secret=…",
                  "secret": "JBSWY3DPEHPK3PXP"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s https://eyalsec.com/api/account/2fa/setup -H \"X-API-Key: es2_3f9a…\""
          }
        ]
      }
    },
    "/api/account/2fa/enable": {
      "post": {
        "tags": [
          "Activity and account"
        ],
        "operationId": "postAccount2faEnable",
        "summary": "Turn on your authenticator app. Returns backup codes once",
        "description": "Asks for your password, and for a code from the secret you got from /api/account/2fa/setup. Note the code proves you hold the authenticator; it is not a second factor on this request. Returns your backup codes, shown once only. Signs out every session.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Activity and account",
          "url": "https://eyalsec.com/docs/api-account"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "password": {
                    "type": "string",
                    "description": "Your current password."
                  },
                  "totp": {
                    "type": "string",
                    "description": "A code generated from the pending secret."
                  }
                },
                "required": [
                  "password",
                  "totp"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "backup_codes": [
                    "…",
                    "…"
                  ],
                  "note": "backup codes are shown once; store them now. All sessions were revoked.",
                  "status": "2fa_enabled"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s https://eyalsec.com/api/account/2fa/enable -H \"X-API-Key: es2_3f9a…\" -d \"password=…\" -d \"totp=123456\""
          }
        ]
      }
    },
    "/api/account/2fa/disable": {
      "post": {
        "tags": [
          "Activity and account"
        ],
        "operationId": "postAccount2faDisable",
        "summary": "Turn off your authenticator app",
        "description": "Asks for your password. Turning off a second factor signs out every session, so other devices have to log in again.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Activity and account",
          "url": "https://eyalsec.com/docs/api-account"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "password": {
                    "type": "string",
                    "description": "Your current password."
                  }
                },
                "required": [
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "note": "all sessions were revoked",
                  "status": "2fa_disabled"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s https://eyalsec.com/api/account/2fa/disable -H \"X-API-Key: es2_3f9a…\" -d \"password=…\""
          }
        ]
      }
    },
    "/api/account/2fa/backup/regenerate": {
      "post": {
        "tags": [
          "Activity and account"
        ],
        "operationId": "postAccount2faBackupRegenerate",
        "summary": "Get a fresh set of backup codes",
        "description": "Asks for your password. Replaces any codes you already had, so the old ones stop working. Shown once only. Requires an authenticator app to be set up first.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Activity and account",
          "url": "https://eyalsec.com/docs/api-account"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "password": {
                    "type": "string",
                    "description": "Your current password."
                  }
                },
                "required": [
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "backup_codes": [
                    "…"
                  ],
                  "note": "shown once; these replace any previous codes",
                  "status": "backup_codes_regenerated"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s https://eyalsec.com/api/account/2fa/backup/regenerate -H \"X-API-Key: es2_3f9a…\" -d \"password=…\""
          }
        ]
      }
    },
    "/api/account/2fa/email/enable": {
      "post": {
        "tags": [
          "Activity and account"
        ],
        "operationId": "postAccount2faEmailEnable",
        "summary": "Turn on the emailed 6-digit second factor",
        "description": "Asks for your password. You need a recovery email on file first, since that is where the code is sent.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Activity and account",
          "url": "https://eyalsec.com/docs/api-account"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "password": {
                    "type": "string",
                    "description": "Your current password."
                  }
                },
                "required": [
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "email_2fa_enabled"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s https://eyalsec.com/api/account/2fa/email/enable -H \"X-API-Key: es2_3f9a…\" -d \"password=…\""
          }
        ]
      }
    },
    "/api/account/2fa/email/disable": {
      "post": {
        "tags": [
          "Activity and account"
        ],
        "operationId": "postAccount2faEmailDisable",
        "summary": "Turn off the emailed second factor",
        "description": "Asks for your password.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Activity and account",
          "url": "https://eyalsec.com/docs/api-account"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "password": {
                    "type": "string",
                    "description": "Your current password."
                  }
                },
                "required": [
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "email_2fa_disabled"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s https://eyalsec.com/api/account/2fa/email/disable -H \"X-API-Key: es2_3f9a…\" -d \"password=…\""
          }
        ]
      }
    },
    "/api/account/apikey": {
      "get": {
        "tags": [
          "Activity and account"
        ],
        "operationId": "getAccountApikey",
        "summary": "See details about your API key",
        "description": "Tells you which key is on the account, its scope, when it was made, when it expires and when it was last used. It never returns the key itself: only a hash is stored, and the key is shown once at generation.\n\nThis is a **read**: a read-only API key may call it.",
        "externalDocs": {
          "description": "Guide: Activity and account",
          "url": "https://eyalsec.com/docs/api-account"
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "created_at": "2026-07-01T09:00:00Z",
                  "exists": true,
                  "expires_at": "2027-01-01T00:00:00Z",
                  "key_prefix": "es2_3f9a…",
                  "last_ip": "203.0.113.4",
                  "last_used_at": "2026-07-16T06:20:00Z",
                  "scope": "full"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s https://eyalsec.com/api/account/apikey -H \"X-API-Key: es2_3f9a…\""
          }
        ]
      }
    },
    "/api/account/apikey/generate": {
      "post": {
        "tags": [
          "Activity and account"
        ],
        "operationId": "postAccountApikeyGenerate",
        "summary": "Create a new API key. REPLACES your current one",
        "description": "Asks for your password and, when an authenticator app is enrolled, a code from it, exactly like the dashboard. You get one key per account, so this replaces the key you are using right now: copy the new one from the response or you will lock yourself out. Shown once only.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Activity and account",
          "url": "https://eyalsec.com/docs/api-account"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "password": {
                    "type": "string",
                    "description": "Your current password."
                  },
                  "totp": {
                    "type": "string",
                    "description": "A current TOTP code, or a one-time backup code. Required only when an authenticator app is enrolled."
                  },
                  "expires": {
                    "type": "string",
                    "description": "Optional expiry as YYYY-MM-DD. Must be in the future. Omit for a key that never expires."
                  },
                  "read_only": {
                    "type": "string",
                    "description": "Any non-empty value makes the key read-only: it can read but not change anything."
                  }
                },
                "required": [
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "api_key": "es2_NEWKEY…",
                  "note": "shown once, and it replaces any previous key for this account"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s https://eyalsec.com/api/account/apikey/generate -H \"X-API-Key: es2_3f9a…\" -d \"password=…\" -d \"totp=123456\" -d \"read_only=1\""
          }
        ]
      }
    },
    "/api/account/apikey/revoke": {
      "post": {
        "tags": [
          "Activity and account"
        ],
        "operationId": "postAccountApikeyRevoke",
        "summary": "Delete your API key",
        "description": "Asks for your password. The key stops working immediately. If you revoke the key you are calling with, that call is the last one it makes.\n\nThis is a **write**: a read-only API key is refused with 403.",
        "externalDocs": {
          "description": "Guide: Activity and account",
          "url": "https://eyalsec.com/docs/api-account"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "password": {
                    "type": "string",
                    "description": "Your current password."
                  }
                },
                "required": [
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Done. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-read-only-key": false,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s -X POST https://eyalsec.com/api/account/apikey/revoke -H \"X-API-Key: es2_3f9a…\" -d \"password=…\""
          }
        ]
      }
    },
    "/api/selftest/{nonce}": {
      "get": {
        "tags": [
          "Agent endpoints"
        ],
        "operationId": "getSelftestByNonce",
        "summary": "Check whether a self-test event reached the dashboard",
        "description": "Called by `eyalsec selftest` on the monitored machine, which holds no dashboard credential, so it needs no authentication and answers one question only: did a self-test event carrying this nonce arrive in the last 30 minutes. nonce is the 32 lowercase hex characters the helper generated for that run; anything else is a 400. It says nothing about which account or machine sent the event. dashboard_url is the es-python events list on this server. Rate-limited per IP.\n\nNeeds no authentication.",
        "externalDocs": {
          "description": "Guide: Agent endpoints",
          "url": "https://eyalsec.com/docs/api-agents"
        },
        "parameters": [
          {
            "name": "nonce",
            "in": "path",
            "description": "32 lowercase hex characters, from the self-test script's name.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "dashboard_url": "https://eyalsec.com/scanner/events?view=python",
                  "received": true
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [],
        "x-read-only-key": true,
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -s https://eyalsec.com/api/selftest/5fdc376114ee72259c9249b36f64ceee"
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "EventQuery": {
        "type": "object",
        "properties": {
          "cursor": {
            "type": "string",
            "description": "Opaque pagination cursor from a prior next_cursor."
          },
          "sort": {
            "type": "string",
            "description": "List order: newest (the default) or severity (critical first, then high, medium, low and info, newest first within each; events with no recognised severity last). Both orders list the same events. A cursor only continues the order that produced it: a cursor from the other order is a 400, so start again without one when changing order. Any other value is a 400."
          },
          "whitelist_repr": {
            "type": "string",
            "format": "regex",
            "description": "Show only events whose sink label (the where field) matches this regex, case-insensitively. Despite the name, it is not matched against the repr. An invalid regex is a 400."
          },
          "blacklist_repr": {
            "type": "string",
            "format": "regex",
            "description": "Hide events whose sink label (the where field) matches this regex, case-insensitively. Despite the name, it is not matched against the repr. An invalid regex is a 400."
          },
          "from": {
            "type": "string",
            "description": "Lower time bound (RFC3339 or YYYY-MM-DD)."
          },
          "to": {
            "type": "string",
            "description": "Upper time bound (RFC3339 or YYYY-MM-DD, inclusive of the whole day)."
          },
          "machine": {
            "type": "string",
            "description": "Restrict to one machine by display name (exact match)."
          },
          "severity": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Severity levels to include: critical, high, medium, low, info. Unknown values are ignored."
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Taint-source categories to include, matched against the event's origin: socket, file, stdin, env, argv, manual, fuzzer, foreign. Unknown values are ignored."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Restrict to events carrying any of these impact class labels (e.g. sqli, xss). An unknown label is a 400."
          },
          "labels": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "Restrict to events carrying any of these triage label ids (your own, from GET /api/event_labels/)."
          },
          "xss_only": {
            "type": "boolean",
            "description": "es-chromium only: keep just the events whose sink can end in script execution. Must be sent with view=browser; with any other view it is a 400."
          },
          "scope": {
            "type": "object",
            "description": "The events page's scope bar, as {mode, value, include_empty}: mode is is, ends (suffix) or regex, matched against the event's scope key (see scope on GET /api/event_view_counts/). A scope with no mode or no value is ignored; an unknown mode or an invalid regex is a 400."
          },
          "advanced": {
            "type": "object",
            "description": "The condition builder, as {match, conditions}: match is all (default) or any, and each condition is {field, op, value, negate}. The fields available depend on the view; an unknown field or an invalid value is a 400."
          },
          "view": {
            "type": "string",
            "description": "Agent kind to include: python, browser, go, c, cpp, rust, node, solidity, java, bash, ruby or php. Omitted or empty = every view your account holds, which is what the pre-views clients this field exists for still get. Naming a view your account does not hold is a 403; if no products are enabled at all, even the omitted form is a 403. Any unrecognised value is a 400, never a silent empty page."
          },
          "bucket": {
            "type": "string",
            "description": "Sanitizer visibility bucket: live (the default, unsuppressed rows only), suppressed, or all. This was briefly named view during development; view now always means the agent kind above."
          }
        },
        "description": "The events filter. Every field is optional, and each matches a control on the events page."
      },
      "EventRow": {
        "type": "array",
        "description": "One event, as a positional array. New values are only ever added at the end, so read by position and ignore extra values you do not know.",
        "prefixItems": [
          {
            "title": "0: created_at",
            "type": "string",
            "description": "When the event was first seen (RFC3339)."
          },
          {
            "title": "1: where",
            "type": "string",
            "description": "The sink label: what happened, where."
          },
          {
            "title": "2: str_repr",
            "type": "string",
            "description": "The value that reached the sink, as text."
          },
          {
            "title": "3: count",
            "type": "integer",
            "description": "How many times it happened."
          },
          {
            "title": "4: machine_name",
            "type": "string",
            "description": "The machine that reported it."
          },
          {
            "title": "5: severity",
            "type": "string",
            "description": "critical, high, medium, low or info."
          },
          {
            "title": "6: agent_kind",
            "type": "string",
            "description": "The product that reported it (one of the view values)."
          },
          {
            "title": "7: id",
            "type": "integer",
            "description": "The event id, for GET /api/event/{id}, labels and notes."
          },
          {
            "title": "8: source_label",
            "type": "string",
            "description": "A short name for where the data came in. Empty for es-python and for products that do not report one."
          },
          {
            "title": "9: context",
            "type": "string",
            "description": "The page the flow happened on (es-chromium) or the file written to (an es-python write). Empty otherwise."
          },
          {
            "title": "10: suppressed",
            "type": "boolean",
            "description": "true when the sanitizer decided this value cannot reach its sink dangerously."
          },
          {
            "title": "11: sanitize_reason",
            "type": "string",
            "description": "One sentence explaining that decision."
          },
          {
            "title": "12: impact",
            "type": "array",
            "description": "Up to three impact classes as {label, sev}, worst first."
          },
          {
            "title": "13: labels",
            "type": "array",
            "description": "Your triage labels on this event, as {id, name, color}."
          },
          {
            "title": "14: has_comment",
            "type": "boolean",
            "description": "true when you have written a note on this event."
          },
          {
            "title": "15: extension",
            "type": "string",
            "description": "The browser extension an es-chromium event came from, by name (or id when the name is unknown). Empty otherwise."
          },
          {
            "title": "16: taint_source",
            "type": "string",
            "description": "The taint source of an es-python event, by the name a rule's source takes. Empty for other products and when no source was recorded."
          }
        ]
      },
      "EventPage": {
        "type": "object",
        "properties": {
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EventRow"
            }
          },
          "next_cursor": {
            "type": "string",
            "description": "Send as cursor to get the next page. Empty when you have everything."
          },
          "scanned": {
            "type": "integer",
            "description": "How many stored rows this request examined."
          },
          "exhausted": {
            "type": "boolean",
            "description": "true when the scan reached the end of the range."
          },
          "oldest_examined": {
            "type": "string",
            "description": "The time of the oldest row this request looked at."
          },
          "event_limit": {
            "type": "integer",
            "description": "Your event quota; -1 means none."
          },
          "event_limit_monthly": {
            "type": "boolean",
            "description": "Whether the quota resets each calendar month."
          },
          "hidden_by": {
            "type": "object",
            "description": "Present when an enabled Hide rule matches every event: that rule's id and label."
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "The API key is missing, wrong, revoked or expired.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "invalid or expired API key"
            }
          }
        }
      },
      "RateLimited": {
        "description": "A rate limit was hit, or the account is locked after wrong passwords. Retry-After gives the seconds to wait.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "text/plain": {
            "schema": {
              "type": "string"
            }
          }
        }
      },
      "Error": {
        "description": "An error. Check the status first; the body carries a short message, as JSON ({\"error\": \"...\"}) on most endpoints and as plain text on some. 400: malformed JSON, an invalid regex, an unreadable cursor or date, or an unknown value. 401: the API key is missing, wrong, revoked or expired. 403: a failed CSRF check, a read-only key trying to write, a product or feature not enabled on your account, a wrong password, or a write on an account limited to reading. 404: not found, including things that belong to another account. 409: conflict with the current state. 422: a field is missing or invalid. 429: a rate limit was hit, or the account is locked after wrong passwords. 500: something failed on our side.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "text/plain": {
            "schema": {
              "type": "string"
            }
          }
        }
      }
    },
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Your API key, from Settings > API access in the dashboard. It starts with es2_."
      },
      "SessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "__Host-es2session",
        "description": "A signed-in browser session. Every write must also carry the CSRF token, read from the __Host-csrftoken cookie, in the X-CSRFToken header."
      }
    }
  }
}
