Current scope: Monitors evaluate a rolling hourly or daily window and trigger on
new_matches. They do not calculate material change versus a prior period, escalation rules, or
analytic baselines. Build those comparisons downstream from retained runs or replay requests;
do not interpret a new_matches notification as a period-over-period analytic judgment.enabled: false. Pause a running Monitor and then PATCH the
saved one with { "enabled": true } to move the slot. Concurrent activations are enforced atomically.
Hello World
The smallest Monitor that works watches one country, once a day, by email. Five fields, and nothing to resolve first:Body
historical_example using the same filters over an expanded 30-day period. That example is
labelled as historical and never changes sample_count, evaluation, trigger state, or delivery.
Preview response
Shortened, not edited:
sample_rows came back with ten rows carrying the full canonical card, and
one row is shown here with the fields worth reading at a glance. explanation is omitted for length,
and normalized_spec is shown just below. Your counts will differ — the window is the trailing 24
hours over a live corpus.preview.normalized_spec: the question as the service resolved it, which
is the field to read when a Monitor matches something you did not expect.
preview.normalized_spec
selected_families is the one to check first — it names the lanes this question actually opens, and
a taxonomy filter can close one (see Criteria).
Read the rows, not the count. A number cannot tell you whether a geographic, semantic, entity or
taxonomy criterion means what you intended; three titles can.
Now save the same body — POST /api/v2/monitors accepts exactly what Preview accepted:
201 Created
criteriafilled itself in. You sent none, so the Monitor watches Events and Stories. Every supported default is visible in the response rather than implied.schedulegainedtimezoneanddaily_hour. A daily Monitor fires atdaily_hourin its IANAtimezone, defaulting to 08:00 UTC.delivery.webhookis a status block, not an echo. It reports what this Monitor has, including its failure counters.webhook_signing_secretisnullbecause no webhook was configured; see Monitor webhooks.last_checked_atandlast_triggered_atarenull. Nothing has run yet. Runs come from the scheduler, and Preview is not one.
Know your limits before you build the next one
GET /api/v2/monitors returns a capabilities block alongside your Monitors. Read it first — it
answers every gating question in one call, and it is the only way to find out what your plan allows
without failing. This is the real block for a Builder key:
evaluation_active means the owner of a Free workspace is currently inside the temporary Explore
evaluation window that enables its trial Monitor allowance. It does not report scheduler health
and is normally false on paid plans whose scheduled Monitors are fully active.
Each of those refusals carries the same capability fields in details, so a client that hit the wall
without reading ahead still learns the shape of the wall:
403 Forbidden
code, never on the sentence. Every code the Monitor surface can raise is published in
Errors.
delivery.webhook.configured on a Monitor response answers a different question — whether that
Monitor has a webhook — not whether your plan permits one. Use capabilities.can_use_monitor_webhook
for that.
Three Monitors, end to end
Same contract, three different questions. Each request below is the exact body that produced the response beside it.- Entity
- Country
- Category-narrowed
- Worldwide by category
Watch everything connected to a resolved company. Entity subjects take ids, never bare names — see
Subjects for how to resolve one and why it matters.The server added
201 Created
"match": "coverage" — the one matching mode a Monitor accepts, spelled out rather
than assumed. Previewed against the trailing 24 hours, this body returned 5 matches: 2 Events and 3
Stories.The four
201 bodies above are abbreviated to the fields that differ: organization_id,
created_by, created_at, updated_at and the webhook failure counters are omitted. The Hello
World response earlier on this page shows the full shape.Subjects
Every Monitor has exactly one subject, and the subject is what anchors it. These six are the whole list:
Two of the six —
category and topic — carry no fields at all. Their scope lives in criteria,
which is what keeps the taxonomy selection and the semantic phrase in one place instead of two.
Facility and place subjects, end to end
Use the exact field names below. A facility subject resolves one canonical directory id; a place subject uses an explicit latitude/longitude point. Both apply a true great-circle radius.Resolve a name to an id first
Monitors take resolved identifiers, never bare names. A name is ambiguous, and a Monitor that guessed wrong would quietly watch the wrong company for months.Response
limit=5, and only the first is shown, without its score,
country_iso3 and sources fields. The second was Petronas — a fuzzy name match to a different
oil company — which is the whole argument for showing candidates to a human before saving anything.
Two fields on each candidate matter here. monitorable says whether that exact id is accepted as a
Monitor subject — check it instead of discovering the answer from a 400. coverage_30d is a
measured story count, not a popularity estimate, so a registry entity with no news identity honestly
reports 0.
Pass whatever identifier the resolver hands back — Monitors accept
every entity id-space /api/v2/search returns, so you never have to convert one. Facility subjects
take the canonical_site_id from GET /api/v2/facilities?search=…; a legacy f_… unit id also
works and is normalized to its parent site on create.
An unknown id fails loudly rather than matching everything:
400 Bad Request
subject.match — not entity_match, which is the Core API’s spelling
on /api/v2/events and /api/v2/stories. A Monitor payload validates strictly, so
criteria.entity_match is refused as an invented field rather than silently ignored.
subject.match accepts coverage and nothing else. The narrower material and actor modes the
Core API offers are rejected here rather than silently widened, so a Monitor never quietly answers a
broader question than the one you asked:
"match": "material" returns MATERIAL_MATCH_NOT_SUPPORTED, and "actor" returns
ACTOR_MATCH_NOT_SUPPORTED — both naming coverage as what to use instead.
Criteria
criteria.data selects events, stories or events_and_stories, and the taxonomy filters are
scoped to match:
Event-family filters need Event data and Story filters need Story data. A mismatch is a
400, not a
Monitor that quietly matches nothing. criteria also takes the same geographic scopes as a
geography subject — use one or the other, not both, and the service refuses a body that does both.
Directional actor filters require criteria.data: "events"; Stories do not carry actor roles. They
are also distinct from geographic scope: criteria.countries: ["CHN"] means the Event happened in
China, while source_actor_countries: ["CHN"] means a Chinese actor performed the coded action.
Combine source and target values when actor order is part of the question:
category subject; the fourth tab in
Three Monitors, end to end is that Monitor, running against the world.
An unanchored Monitor is refused, not silently accepted
A subject anchors,criteria narrows, and every Monitor needs both ends. Two subjects hold their
scope in criteria rather than in themselves, so for those two the refusal lands on the criteria
field they left empty. A category subject with no taxonomy selection would be the whole corpus,
every run:
400 Bad Request — subject.type category, no family_filters
topic subject with no criteria.search is refused the same way, naming that field instead:
400 Bad Request — subject.type topic, no search
Each entry names the field, the machine-readable
issue, and the message — the rule you broke, in
one sentence. Branch on code and field; show message to a human. issue: "custom" means a
cross-field rule refused the body rather than a single field failing its own type or bound, so the
message is the only place that rule is stated.candidate_limit: 100 — the ceiling a semantic question is retrieved against, and the reason
its count can be a floor rather than a total.
Confirmed duplicate Events are always folded to their canonical incident identity. This is part of
the Event contract, not a Monitor option. fatalities_only restricts to records carrying fatalities.
Preview and scheduled runs cover different windows
This is by design, and it is the most common surprise: Preview said 3 and the first run said 5. Neither is wrong.
Preview answers “does this question match the kind of thing I meant?” against whatever is on the
wire right now, and says so in
window_label and the window_start/window_end pair it returns. A
run answers “what is new in the period that just ended?” on a fixed boundary. The two windows
overlap partially and almost never contain the same rows, so treat Preview as a check on the
specification, never as a forecast of the count.
Run windows are half-open — window_start is included, window_end is excluded — so consecutive
runs never double-count and never leave a gap. A daily window spans a local day, which is 23 or 25
hours across a daylight-saving transition.
Run a saved Monitor immediately
POST /api/v2/monitors/{id}/run-now executes the saved specification over the current rolling
hour or day and persists the result for seven days. It costs 1 QU and is intentionally safe for
testing: it sends no email or webhook, does not mark matches as seen, and does not move the next
scheduled run. A human-paused Monitor can be tested; a Monitor paused because its plan no longer
permits the configuration must be corrected first.
cURL
execution_origin: "manual_test"; ordinary retained runs carry "scheduled".
Reading a run
A triggered run stays listed for seven days. List them withGET /api/v2/monitors/{id}/runs, then fetch
one with GET /api/v2/monitors/{id}/runs/{runId}.
Runs response, before anything has triggered
monitor is the full Monitor record, abbreviated here to two fields. total counts triggered runs,
which is what limit and offset page through.
A quiet check creates no run — rather than fabricate a zero-result row, a Monitor advances
last_checked_at and leaves last_triggered_at alone. checks is how you tell “checked, found
nothing” apart from “never ran”: one row per execution whether or not it triggered.
A run reports a count, retains rows, and includes a few canonical cards inline. They are deliberately
different numbers.
Those four live on
run.summary in a run response and on trigger in a webhook envelope. The run
row itself also carries run.result_count (the same reported count) and run.retained_row_count
(execution rows stored with the run).
truncated is an inline flag: it says the reported count exceeds the cards you were handed —
not that the rest are unreachable. They are: see
Paging every match. It says nothing about whether semantic
retrieval searched the whole corpus — that is what
total_matches_is_lower_bound is for, and confusing the two is the easiest way to under-report.
When you need a semantic count to be exhaustive rather than a floor, narrow the question with a
subject, a geography and family-scoped taxonomy filters.
run.summary, run.matches and run.replay_requests are present for Monitors created through this
contract. A read-only legacy Alert surfaced in the Monitor list returns the bare run row without
them.
Paging every match
A run counts every match and retains a few. Those are different numbers and they are usually far apart: measured across production on 2026-08-26, 354 of 467 runs — 76% — matched more than the run kept, the largest by 250 to 10. The count was never wrong. What was missing was a way to reach the rest, so a Nigeria daily Monitor could report 139 matches and hand you ten rows.GET /api/v2/monitors/{id}/runs/{runId}/matches pages the full matched list, and it is a cursor walk
rather than an offset one — pass no cursor for the first page, then send back the next_cursor you
were given until it comes back null.
GET /api/v2/monitors/{id}/runs/{runId}/matches.csv. The UTF-8 CSV includes both the frozen
as-triggered fields and the current hydrated card. It contains a header-only file for a zero-match
manual run. Check X-GDELT-Result-Count, X-GDELT-Retained-Match-Count, and
X-GDELT-Matched-Items-Truncated: retention can cap the export while the exact result count remains
larger.
Response
current is the full Event or Story card, and only its id and geo are kept here.
That middle row is what the two tellings are for. Nothing about the story’s headline changed, but the
country we resolved it to did — United States when the Monitor fired, Russia now that the
Moscow visit is the resolved location. Re-running the question would have shown you only the second
answer, with no sign the first had ever been sent.
result_count is the run’s own count, and it does not change as you page — comparing it against the
rows you have collected is how you know you reached the end. matched_items_truncated is the one
case where they will not meet: a window enormous enough to exceed the retention cap freezes a prefix,
and the flag says so rather than letting the totals quietly disagree.
Two tellings per row, and the difference between them
A run is a point-in-time answer, and the warehouse keeps moving under it — the settle continues, clusters get merged, events get re-coded. Re-running the run’s query an hour later is therefore not the same question, and it can legitimately return a different set than the notification claimed. So each row carries both tellings.as_triggered is exactly what the email or webhook asserted,
frozen and never re-derived. current is the live record, fetched fresh. status is the
relationship, and it is the answer to “has this changed since you told me?”:
A superseded Story usually has no successor to give you
This is the honest limit of the endpoint, and it comes up the moment anyone sees the status. When the same-day merge collapses two Stories it re-inserts the loser marked superseded — there is nomerged_into column on the Story record, so in the general case the surviving Story is not
recoverable from the superseded one. Reporting the merge with a null successor is the true answer;
treat a tool or a script that produces a successor anyway as having guessed.
What it means in practice is milder than it sounds: the Story was a duplicate, its coverage lives on
under the survivor, and as_triggered still holds exactly what you were told. If you need the
surviving narrative, search the window for the same subject rather than asking this row for a
forwarding address.
Replaying a run
A run carriesrun.replay_requests: ready-to-send Core API requests that reproduce it. Replaying
takes two stages, and the request alone is not the replay.
- Call each request and follow
pagination.next_cursoruntil it is null. These requests are day-bounded, so they deliberately return more than the run window. - Apply
exact_window_filteras a half-open condition — keep rows whose named timestamp is>= start_inclusiveand< end_exclusive. Skipping this widens the replay.
Replay requests are ordinary Core API calls, and the Core API spells its parameters its own way — a
hand-edited replay is where that bites. An unknown parameter is refused with the name it expected
rather than ignored:
400 Bad Request — GET /api/v2/events?family=conflict
accepted_params is abbreviated here; the real response lists all 57.Managing Monitors
PATCH /api/v2/monitors/{id}changes config, schedule, delivery orenabledstate. A body naming no field is refused rather than answered200with an unchanged Monitor.POST /api/v2/monitors/batchresumes, pauses or deletes many at once. The response separatesrequested,unique_ids,affected_idsandnot_found;affectedcounts only rows that actually changed, so pausing an already-paused Monitor is a successful no-op rather than anot_found.DELETE /api/v2/monitors/{id}permanently removes the Monitor and its saved runs.
/runs accepts query
parameters — limit and offset — and an unrecognized one is a 400 UNKNOWN_PARAM rather than a
silently ignored filter.
Only organization owners and admins manage Monitors. Other members share the organization view and
can inspect what their role permits.
From MCP
Monitors live in thegdelt_cloud category behind the progressive dispatcher — discover a schema
with gdelt_cloud_tool_get, then call it. get_monitor_run returns a run and the rows it retained;
get_monitor_run_matches pages the rest, taking the same cursor and returning the same status
per row as the REST endpoint above.
Monitors are written through
gdelt_cloud_tool_write, not gdelt_cloud_tool_call.
gdelt_cloud_tool_call is the READ-ONLY dispatcher and refuses every state-changing tool by name.
The seven that create, update, pause, delete or send run through gdelt_cloud_tool_write:
create_monitor, update_monitor, delete_monitor, set_monitor_enabled,
configure_monitor_delivery, test_monitor_delivery, batch_monitors.
preview_monitor is a read and uses gdelt_cloud_tool_call.preview_monitor and create_monitor compile the same body as REST. Use update_monitor for a
partial update, set_monitor_enabled for pause/resume, and batch_monitors to act on many ids.
Inspect runs with list_monitor_runs and get_monitor_run, then execute each replay_request
through search_events or search_stories — applying exact_window_filter yourself, exactly as
above.
Use run_monitor_now through gdelt_cloud_tool_write to execute an existing Monitor immediately.
Like REST POST /api/v2/monitors/{id}/run-now, it costs 1 QU, saves a manual-test run for seven
days, sends no delivery, does not advance new_matches state, and does not change the schedule.
Preview remains a separate unsaved check for a Monitor definition you have not created yet.
