Events API

These endpoints read your events: the filtered list, one event's full detail, the runs that produced them, counts and statistics, exports, the es-python map, and your saved searches. All of them are reads unless marked otherwise.

Query events

POST /api/get_all_events_with_count/ returns one page of your events, newest first, filtered the same way the events page filters them. It is a POST because the filters travel in a JSON body, but it is a read: a read-only key may call it, and it keeps working on an account limited to reading.

POST /api/get_all_events_with_count/
curl -s https://eyalsec.com/api/get_all_events_with_count/ \
  -H "X-API-Key: es2_EXAMPLEKEYdoNotUse0000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{"from":"2026-06-01","severity":["critical","high"],"blacklist_repr":"^healthcheck$"}'

Request body

Every field is optional, and an empty body returns the first page with no filters. Each field matches a control on the events page.

Field Meaning
cursor The next_cursor of the previous page, to get the next one. See paging.
view Which product's events: python (es-python) or browser (es-chromium). Omitted or empty means every product on your account. A product your account does not have is a 403; an unknown value is a 400. See event lists.
bucket live (the default: only events not suppressed), suppressed (only suppressed ones) or all. This is the page's Show control.
whitelist_repr A regex, case-insensitive. Keep only events whose where (the sink label, index 1 of a row) matches. Despite the name it is not matched against the value. See filtering.
blacklist_repr A regex, case-insensitive. Drop events whose where matches.
from, to Dates, YYYY-MM-DD or RFC3339. to includes the whole of that day.
machine One machine, by its display name (exact match).
severity A list from critical, high, medium, low, info. See severity.
sources A list of taint source categories from socket, file, stdin, env, argv, manual, fuzzer, foreign.
tags A list of impact classes, such as xss or sqli. Keeps events carrying any of them; an unknown class is a 400.
labels A list of your label ids. Keeps events carrying any of them.
xss_only true keeps only the es-chromium events whose sink can run script. It is the Can cause XSS button, and it must be sent with "view": "browser" (any other view is a 400).
scope The scope bar, as {"mode": "ends", "value": ".example.com", "include_empty": false}. mode is is, ends or regex. See scope.
advanced The condition builder, as {"match": "all", "conditions": [{"field": "sink", "op": "contains", "value": "sql", "negate": false}]}. match is all or any; the fields depend on the view. See conditions.

Unknown values in severity and sources are ignored. An invalid scope or advanced is a 400 naming the problem.

xss_only keeps markup sinks, code sinks, script URLs and script bodies, attribute writes and navigations. It drops storage and cookie writes (the write itself runs nothing; a later read-back that reaches a script sink is its own event and is kept), style writes, outbound requests, messages, plain-text writes, and findings that report a fact about a page rather than a value reaching a sink.

Response

The answer is a list of events plus paging information. Each event is an array, not an object, to keep large pages small.

{
  "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"}],
     [{"id": 1, "name": "triage", "color": "amber"}], false, ""]
  ],
  "next_cursor": "",
  "scanned": 1,
  "exhausted": true,
  "oldest_examined": "2026-07-05T12:00:00Z",
  "event_limit": 25000,
  "event_limit_monthly": true
}

The event row

Each event is an array of 16 values in a fixed order. New values are only ever added at the end, so read by position and ignore any extra values you do not know.

Index Field Meaning
0 created_at When the event was first seen.
1 where The sink label: what happened, where.
2 str_repr The value that reached the sink, as text.
3 count How many times it happened. See count.
4 machine_name The machine that reported it.
5 severity critical, high, medium, low or info.
6 agent_kind The product that reported it (one of the view values).
7 id The event id, for one event's detail, labels and notes.
8 source_label A short name for where the data came in. Empty for es-python and for products that do not report one.
9 context The page the flow happened on (es-chromium) or the file written to (an es-python write). Empty otherwise.
10 suppressed true when the sanitizer decided this value cannot reach its sink dangerously. See suppressed events.
11 sanitize_reason One sentence explaining that decision.
12 impact Up to three impact classes as {label, sev}, worst first. sev is the class's own severity, which can differ from the event's severity at index 5.
13 labels Your triage labels on this event, as {id, name, color}.
14 has_comment true when you have written a note on this event.
15 extension The browser extension an es-chromium event came from, by name (or id when the name is unknown). Empty otherwise.

The large fields (stack trace, origin, where the value was created, source code listings, your note's text) are not in the row, because shipping them on every row would make a page roughly a megabyte. Fetch them for one event with GET /api/event/{id}.

Paging

A page holds up to 100 events. To get the next page, send the next_cursor from the previous answer as cursor, with the same filters; when next_cursor comes back empty, you have everything.

A short page is not the last page. Each request looks at a bounded slice of your history so that no call runs for seconds, so a selective filter can return fewer than 100 events, or none, and still hand back a next_cursor. Keep following the cursor until it is empty, even after an empty page. A client that stops at the first empty page will report "no matches" for a search that has matches further back.

Three fields report progress: scanned is how many stored rows this request examined, exhausted is true when the scan reached the end of the range, and oldest_examined is the time of the oldest row it looked at. The events page uses them to show how far back a search has got.

When one of your enabled Hide rules matches every event (an empty pattern, or .*), nothing in the view can be shown, so the response comes back at once with no events, exhausted: true, and a hidden_by object holding that rule's id and label. Turn the rule off or narrow its pattern to see events again.

Your event quota

The API shows exactly what the dashboard shows, so your event quota applies. event_limit is the quota (-1 means none) and event_limit_monthly says whether it resets each calendar month.

The quota applies to each product separately, at its full value: an event_limit of 5000 means up to 5000 visible es-python events and up to 5000 es-chromium events, not 5000 across the account. The quota counts event rows by when they were first seen: each product's window shows the earliest events of the month and hides the rest, and those stay hidden unless the quota is raised. A new month starts with fresh room. See event quota.

One event's detail

GET /api/event/{id} returns the large fields the events list leaves out, for one of your events. id is index 7 of an event row.

GET /api/event/{id}
curl -s https://eyalsec.com/api/event/90210 \
  -H "X-API-Key: es2_EXAMPLEKEYdoNotUse0000000000000000000000"
{
  "trace": "Traceback (most recent call last)...",
  "source": "",
  "origin": "{...origin...}",
  "repr_created": "<str 'id'>",
  "source_code": "/srv/app/handler.py:12\n  11    q = request.args['q']\n> 12    run(q)",
  "sink_code": "/srv/app/db.py:40\n> 40    cur.execute(sql)",
  "detail": "/var/www/app/handler.py",
  "comment": "",
  "extension": {"id": "", "name": "", "version": "", "label": ""},
  "runs": [
    {"run_uid": "9f2c1d4ab8e0472c93ad5f6178be20c4",
     "cmdline": "es-python app.py --port 8080 --token [masked]", "env": "",
     "count": 12, "first_seen": "2026-09-17T08:12:03Z", "last_seen": "2026-09-17T09:40:51Z"}
  ]
}
Field Meaning
trace The stack trace at the moment the value reached the sink. See stack trace.
origin Where the data came in. See origin.
repr_created Where the tainted value was created. See repr.
detail A secondary locator: the file a write went to, or the page an es-chromium event happened on.
rule_source The name to use as a rule's source for this event's taint source (for example socket, weak-random, db, a number for a make_vuln() value, or any when the source is unknown), so a rule you create through the rules API matches it. This is what the Events page's Rule row menu uses.
source_code, sink_code A few lines of your program's source around the line that created the value and the line that used it, each starting with a path:line header. Empty unless the machine is set to send source code.
comment Your own note on the event, empty when you have none.
extension The browser extension an es-chromium event came from: its 32-character id, and the name and version from its manifest when known. Show label, which is the name and version when known and the id otherwise. All empty when no extension was involved.
runs The runs that produced the event, newest first, at most 50. See below.
source Reserved. Always empty on your account.

Each entry in runs is one program invocation: its command line, its environment (empty unless the machine is set to send environment variables), how many of the event's occurrences it produced, and when it first and last did. Values that look like passwords, tokens or keys are masked. run_uid names that one invocation; pass it to the events query to see only what it produced (see runs). It is empty for events from an older agent, which groups invocations by command line and environment instead. The list is empty for an event reported without run information.

The event must be yours. An id that belongs to another account answers 404, exactly like one that does not exist. A non-numeric id is a 400.

List runs

GET /api/runs/ lists the runs behind your events, newest first. A run is one invocation of one monitored program: run es-python app.py and that is a run; run it again and that is another. See Runs.

GET /api/runs/
Parameter Meaning
machine One machine, by display name, the same name the events query's machine filter takes. Omitted covers every machine on your account.
secret One machine, by its internal machine secret. The API never hands out machine secrets, so use machine instead.
q A case-insensitive piece of the command line.
before The next_before of the previous page. An unreadable value is treated as the first page.
limit Rows per page, 1 to 200. The default is 200.
curl -s "https://eyalsec.com/api/runs/?machine=web-01&limit=50" \
  -H "X-API-Key: es2_EXAMPLEKEYdoNotUse0000000000000000000000"
{
  "runs": [
    {"run_uid": "9f2c1d4ab8e0472c93ad5f6178be20c4",
     "cmdline": "es-python app.py --port 8080 --token [masked]",
     "first_seen": "2026-09-20T08:12:03Z", "last_seen": "2026-09-20T08:14:55Z",
     "events": 7, "occurrences": 31}
  ],
  "next_before": ""
}

events is how many distinct events that invocation produced and occurrences how many times it produced them. Follow next_before until it comes back empty. A machine that is not yours gives an empty list.

To see the events of one run, add a run condition to the events query, on the es-python view. Under that filter each event's count is what that run contributed, not the event's all-time total. The same condition works on the export and the map. To see every run of one command instead, use the command field.

curl -s https://eyalsec.com/api/get_all_events_with_count/ \
  -H "X-API-Key: es2_EXAMPLEKEYdoNotUse0000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{"view":"python","advanced":{"match":"all","conditions":[
       {"field":"run","op":"is","value":"9f2c1d4ab8e0472c93ad5f6178be20c4"}]}}'

Event statistics

GET /api/get_event_stats/ returns the summary the dashboard shows: total hits, distinct event types, counts for the last hour, day and week, the top event types, recent events and a 14-day timeline.

GET /api/get_event_stats/

The answer is shown in the worked example. first_detection and latest_detection are absent when you have no events yet. The numbers are cached per account for a short time.

Event count for your quota

GET /api/event_count/ returns how many of your stored events count toward your current quota period, together with your limit. The period is the current calendar month when event_limit_monthly is true.

GET /api/event_count/?view=python
{ "count": 1234, "view": "python", "event_limit": 5000, "event_limit_monthly": true }

view is optional and takes the same values as the events query. Because the quota is per product, pass a view to get the number that product's list is capped by. Without it you get the total across every product you have, which, with more than one product, no single list is capped by. A product your account does not have is a 403; an unknown value is a 400. The answer echoes the view it counted.

The count is cached per account and view and refreshed in the background, so repeat calls answer at once and may be a few minutes behind.

Event count per product

GET /api/event_view_counts/ returns how many events each of your products' lists would show. The events page uses it for the count beside each list in the sidebar.

GET /api/event_view_counts/
{ "python": 1204, "browser": 38 }

Every product on your account is always present, at zero when it has no events. An account with no products gets {}. Each number is narrowed exactly as the list is, by your event quota and by bucket, so a count and the list under it always agree.

Parameter Meaning
bucket live (default), suppressed or all, as on the events query.
scope Count only events whose scope key matches: the page host for es-chromium, and the first part of the origin (such as a file path) for es-python.
scope_mode is, ends or regex. Required with scope; a scope without it is ignored.
scope_empty true also counts events with no scope key.

The counts are cached and may be up to a minute behind. For the quota tally, use /api/event_count/.

Live and suppressed totals

GET /api/event_visibility_counts/ returns how many of your stored events are shown and how many the sanitizer currently hides. The events page uses it for the numbers beside its Show control.

GET /api/event_visibility_counts/
{ "live": 1200, "suppressed": 34 }

Suppression never deletes anything, so the two numbers always add up to every event you have stored. Nothing narrows them: no quota, product, scope, filter or bucket. The answer is cached per account and may be up to a minute behind.

Export events

POST /api/events_export downloads every event matching a filter as a CSV or JSON file, instead of one page. The body is exactly the body of the events query, so the quota, the bucket and every filter select the same events the events page shows. See Export.

POST /api/events_export?format=csv

format is csv (the default) or json; anything else is a 400. A cursor in the body is ignored, because an export is the whole set.

curl -s "https://eyalsec.com/api/events_export?format=csv" \
  -H "X-API-Key: es2_EXAMPLEKEYdoNotUse0000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{"view":"browser","severity":["critical","high"]}' \
  -o findings.csv

The CSV columns are:

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

Every row carries the large fields the list leaves out (page and call site, origin, where the value was created, stack trace), so a finding in the file can be reproduced. The taint_chain column is always present and always empty on your account.

An export stops at 50,000 rows and always says so: the CSV ends with a row beginning TRUNCATED, and the JSON sets "truncated": true. Narrow the dates or the filter to get the rest.

Map events

POST /api/event_map/ returns the matching es-python events as a graph: the sources the data came from, the files it was read from, and the sinks it reached. It is what the Map layout on the es-python events page draws (see Events map).

POST /api/event_map/

The body is the body of the events query, and view must be python (any other view is a 400). The quota, bucket, tags and every filter select the same events the list shows.

curl -s "https://eyalsec.com/api/event_map/?sink=os%20system" \
  -H "X-API-Key: es2_EXAMPLEKEYdoNotUse0000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{"view":"python","from":"2026-07-01"}'
{
  "events": 3,
  "hits": 6,
  "truncated": false,
  "max_events": 0,
  "sources": [{"kind": "file", "events": 2, "hits": 3, "severity": "critical"},
              {"kind": "socket", "events": 1, "hits": 3, "severity": "high"}],
  "files": [{"path": "/srv/app/settings.py", "events": 2, "hits": 3, "severity": "critical"}],
  "sinks": [{"sink": "os system", "events": 3, "hits": 6, "severity": "critical"}],
  "links": [{"source": "file", "file": "/srv/app/settings.py", "sink": "os system", "events": 2, "hits": 3, "severity": "critical"},
            {"source": "socket", "file": "", "sink": "os system", "events": 1, "hits": 3, "severity": "high"}],
  "focus": {"events": [["2026-07-05T12:00:00Z", "os system", "'...'", 2, "web-01", "critical", "python", 12345,
                        "", "", false, "This value was not sanitized.", [], [], false, ""]],
            "truncated": false}
}

Each event is counted under its source (the same categories as the sources filter, plus other for data none of them describe), under its file when the source is file, and under its sink (its where). links lists every source, file and sink path; file is empty on a link whose source is not file, and a file path is empty for a file read without a known path. Every entry has events (stored events), hits (the occurrences those stand for) and severity (the worst among them), worst first. Only what your events contain appears.

Parameter Meaning
source Focus on one source: socket, file, stdin, env, argv, manual, fuzzer, foreign or other.
file Focus on one file path, exactly as it appears in files. Empty selects the file with no known path.
sink Focus on one sink, exactly as it appears in sinks.
only focus returns only the focus object, when you already have the graph. Needs one of the three above.

With a focus, the answer also carries focus.events: the newest 200 matching events in the same row shape as the events query, with focus.truncated set when there were more. The graph itself is not narrowed. A focus request reuses the map most recently built for the same body, which makes it fast, but reads the events fresh.

A map covers every event the filter selects, so truncated is false. A wide filter over a large account is a lot of work, so prefer a from and to window when you have one.

Event names

GET /api/event_names/ lists the distinct sink labels (the where field) across your events. Use it to build a filter or rule pattern.

GET /api/event_names/
{ "names": ["socket-recv", "file-open", "sql-execute"] }

Saved searches

A saved search is a named events-page query string: the product, scope, toolbar filters and conditions, exactly as they appear in the page's address. Choosing one puts the page back where it was. Unlike a filter, it changes nothing until you choose it. See Saved searches.

Searches are stored per product, because the searchable fields differ per product, so every call names one view.

List saved searches

GET /api/event_searches/ lists your saved searches for one product.

GET /api/event_searches/?view=browser
{ "searches": [ { "id": 3, "name": "my domain", "view": "browser",
                  "query": "view=browser&scope=shop.test" } ] }

view takes the same values as the events query and defaults to python. An unknown value is a 400; a product your account does not have is a 403.

POST /api/event_searches/ stores one named search. This is a write.

POST /api/event_searches/

JSON body: name (1 to 80 characters, trimmed), view, and query (the query string without the leading ?, at most 4096 characters). Saving the same name again in the same product overwrites it, which is how you edit a search. The answer is 201 with that product's whole list, so the new id is there without a second call.

curl -s https://eyalsec.com/api/event_searches/ \
  -H "X-API-Key: es2_EXAMPLEKEYdoNotUse0000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{"name":"my domain","view":"browser","query":"view=browser&scope=shop.test"}'

DELETE /api/event_searches/{id} removes one saved search and answers 204. This is a write.

DELETE /api/event_searches/{id}

An id that belongs to another account deletes nothing and still answers 204, so the answer never confirms that someone else's search exists.

Something unclear or missing on this page? Email support@eyalsec.com.

EyalSec Pricing Docs Security Contact Login Book a live demo