Oil & Gas Well Permits Search & Monitor API (ND/CO)
Pricing
from $4.40 / 1,000 results
Oil & Gas Well Permits Search & Monitor API (ND/CO)
Search and monitor US oil & gas well permits and wells from official state ArcGIS sources (North Dakota / Bakken, Colorado). Filter by state, operator, status, well type and county, and track newly issued drilling permits for lead gen. Keyless, official sources.
Pricing
from $4.40 / 1,000 results
Rating
0.0
(0)
Developer
Kyle Maloney
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
0
Monthly active users
7 days ago
Last modified
Categories
Share
Search and monitor US oil and gas wells and permitted drilling locations straight from the official state regulator ArcGIS services for North Dakota (Bakken) and Colorado. Filter by state, operator, status, well type, county, name or API number; get one normalised row per well with coordinates and PLSS location, exportable as JSON, CSV or Excel. Keyless, official sources, no scraped middleman.
Built for oilfield-service sales teams working fresh drilling activity by county, mineral-rights and royalty buyers tracking who is drilling on which section-township-range, midstream and siting analysts sizing gathering and water-disposal demand, ESG and academic researchers needing a citable well inventory, land brokers resolving an operator or API number to a location, and AI agents needing a strictly-typed permit-search tool.
Every count below was re-read from the two live services with returnCountOnly on
2026-08-25, against build 1.0.11. Every field name came out of the shipped
dataset schema.
Breaking changes in build 1.0.11 - read this if you already consume this Actor
Three changes alter values you may be parsing. Each replaces a confident wrong answer.
1. api_number is now null on every Colorado row. Colorado's locations layer
publishes fac_num, a pad or aliquot designator, not an API well number. Measured
over all 84,927 CO locations: 5,559 values contain a space, 1,589 contain "Pad",
765 (0.90%) are null and 2 are the literal string N/A. Values like 16 SWD Pad,
14NESW and S28-10D were being published in a field called api_number. The raw
value is not discarded - it now ships in the new facility_number field, and
api_number_status reads not_an_api_number, so "the state published nothing" and
"the state published something that is not an API number" stay distinguishable.
Validation is by shape, not by state, so ND's api_no is checked on every row too.
The apiNumber INPUT is unaffected. A Colorado lookup still queries fac_num, so
{"states":["CO"],"apiNumber":"4-8"} matches exactly what it always did. Rejecting a
value for output must not silently disable the filter.
2. Colorado record_id moved from fac_num to loc_id. fac_num is not a key:
220 Colorado locations share the literal value Pad, 8 share 1-28, and 765 are
null. As a delta identity that silently suppressed 219 of those 220 forever. loc_id
is non-null on all 84,927 rows. If you joined on Colorado record_id, rekey on
loc_id - which is also what api_id carries on Colorado rows.
3. A run whose date window no selected state can honour now FAILS. A Colorado-only
run with sinceModified previously returned the entire unfiltered Colorado
universe up to maxResults, labelled as filtered, and billed for it. It now stops
before any fetch and bills nothing.
Also: all delta baselines reset once. monitor mode moved to a named,
query-scoped key-value store, so the first scheduled run after this build has no prior
baseline for its scope, emits everything, and says so on every row via
delta_baseline_established_this_run: true.
Scope and cost - read this first
states is REQUIRED
{ "states": ["ND"], "maxResults": 500 }
A call that omits states is rejected before the run starts - HTTP 400, no run
object created, nothing billed, not even the Actor-start event. That is
deliberate: the field used to carry a schema default, and Apify injects defaults
server-side, so a caller who named no state was quietly answered about one arbitrary
state, told the run SUCCEEDED, and billed.
One caveat: an explicitly empty array ({"states": []}) is not the same as
omitting the field. The key is present, so the platform accepts it and the code falls
back to every configured state - ND and CO together. Name your states.
What a run costs
| Price | |
|---|---|
| List price (Free plan) | $0.008 per result = $8.00 per 1,000 rows ($0.80 per 100) |
| Bronze / Silver / Gold / Platinum / Diamond | $0.0064 / $0.0056 / $0.0044 / $0.0032 / $0.0024 per result (20-70% off) |
| Actor start | $0.00005, one-time per GB of run memory |
Only rows written to the dataset are billed; a run that fails a pre-flight check emits nothing and bills nothing.
maxResults is the only ceiling - set it
It defaults to 1000 and applies PER STATE, so a two-state default run can emit 2,000 rows (about $16.00). The schema allows up to 200,000 per state, where the real ceiling becomes the data: 43,835 ND wells ($350.68) plus 84,927 CO locations ($679.42) is 128,762 rows, about $1,030.10. The default caps a single-state run at $8.00, but that number is published here rather than left for you to find on an invoice.
Row counts for realistic scopes
Measured live with returnCountOnly - true matching-record counts, not estimates.
| Input | Rows | At list price |
|---|---|---|
{"states":["ND"],"sinceModified":"2026-08-01"} (spudded in ~3 weeks) | 52 | $0.42 |
{"states":["ND"],"sinceModified":"2026-01-01"} (spudded YTD) | 427 | $3.42 |
{"states":["ND"],"status":"LOC"} (permitted, not yet drilled) | 630 | $5.04 |
{"states":["ND"],"sinceModified":"2026-01-01","includeUndatedRecords":true} | 7,415 | $59.32 |
{"states":["CO"],"operator":"chevron"} | 203 | $1.62 |
{"states":["ND"],"operator":"continental"} | 3,618 | $28.94 |
{"states":["CO"],"county":"weld"} | 26,967 | $215.74 |
The pattern: a date or status filter is cheap, a bare county is not.
Coverage
Two states, each read live at run time from the regulator's own public ArcGIS FeatureServer - no key, no login, no cached copy.
| State | Regulator | Layer | Records | Date field? |
|---|---|---|---|---|
| ND | North Dakota DMR / NDIC | OilGasPublicMapDataVectorTiles/Wells/FeatureServer/0 on gis.dmr.nd.gov | 43,835 wells | SPUD_DATE only - not a permit-issue date |
| CO | Colorado ECMC (formerly COGCC) | DNR_Public/OGCC_Oil_and_Gas_Locations/FeatureServer/0 on data.dnrgis.state.co.us | 84,927 locations | No |
Freshness is whatever the state last published. Neither layer exposes an
editingInfo.lastEditDate, so this Actor makes no update-cadence claim. What can
be shown is a measurement: on 2026-08-25 the newest North Dakota spud_date in the
layer was 2026-08-17, eight days old.
What is NOT covered - plainly
Only North Dakota and Colorado. No nationwide mode, no hidden "all states"
behaviour. Absent in particular: Texas (RRC), New Mexico (OCD), Oklahoma
(OCC), Pennsylvania, Ohio, West Virginia, Wyoming, Montana, Utah, Alaska, Louisiana,
Kansas, California and every other state. For California the regulator is CalGEM
and a third-party Actor, scrapesignal_labs/calgem-well-permit-leads, covers it - a
neutral pointer, not an endorsement; it has not been tested against this one. Also out
of scope by design: production volumes, completions, well logs, permits still in
application, lease/unit ownership, and operator contact details.
Input reference
From the shipped input_schema.json.
| Property | Type | Required | Default | Behaviour |
|---|---|---|---|---|
states | array | Yes | none (prefill ["ND"]) | Two-letter codes. Configured: ND, CO. Case-insensitive; unknown codes dropped. |
operator | string | No | none | Operator name contains, case-insensitive. E.g. continental. |
status | string | No | none | Exact status code, case-insensitive. Vocabularies below. Empty = all. |
wellType | string | No | none | Well type contains. ND only - CO's layer has no well-type field. |
county | string | No | none | County name contains. E.g. mckenzie, weld. |
nameContains | string | No | none | Well or facility name contains. |
apiNumber | string | No | none | Exact identifier. ND matches api_no (dashed, e.g. 33-053-10805-00-00); CO matches fac_num (e.g. 4-8). CO still searches fac_num even though api_number now publishes null on CO. |
sinceModified | string | No | none | SPUD date lower bound, NOT a permit-issue date - read the section below before using it for permit monitoring. Must be YYYY-MM-DD. ND only; a CO-only run with this set FAILS. Pattern: ^$|^\d{4}-\d{1,2}-\d{1,2}$. |
includeUndatedRecords | boolean | No | false | Only meaningful with sinceModified. Keeps records whose date is null instead of dropping them - on ND that is the 630 permitted-but-not-yet-drilled wells. Each is labelled date_filter_match: "undated_included". |
maxResults | integer | No | 1000 | Max rows per state. Min 1, max 200000. The emit cap, therefore the billing cap. |
monitor | boolean | No | false | Delta mode, backed by a named, query-scoped key-value store. |
verifyUpstream | boolean | No | true | Runs the live drift gate against each selected state layer before any billable row is fetched. See below. Turning it off means a silently emptied or re-scoped layer would go undetected; every row then reads drift_gate_status: "skipped". |
simulateOutage | string | No | "none" | Diagnostic. Forces one class of drift-probe outcome so the gate's own failure branches can be re-proved on demand instead of only during a real outage. See the table below. |
sinceModified must be YYYY-MM-DD, and a bad value fails the run
Accepted: 2026-01-15, and unpadded 2026-1-5 (padded for you). Empty or omitted
means no date filter. Anything else fails the run before any network request, and
bills nothing.
Why so strict: the value is spliced into an ArcGIS SQL clause as
spud_date > DATE '<value>', and ArcGIS's own date parser is lenient - it
reinterprets a malformed value instead of rejecting it, and answers HTTP 200 with
nothing indicating a guess. Measured live against ND's Wells layer:
| You send | What ArcGIS silently did | Wells matched |
|---|---|---|
2025-01-15 (the contract) | read as written | 1,268 |
05/06/2025 | read as US May 6 | 1,027 - the EU June-5 reading is 945, an 82-well 8.0% swing, invisible in the output |
15/01/2025 | swapped day and month rather than rejecting month 15 | 1,268 - right by luck |
Six measured shapes are silently reinterpreted; only three (2025, 2025-13-45,
2025-02-30) fail loudly at ArcGIS. So all slash forms are rejected on purpose,
even ones ArcGIS resolves correctly today: guessing trades a loud, free, fixable error
for a silent, billed, wrong one. Validation runs in two layers - the schema pattern
rejects prose at the platform edge before a run exists, and the code then rejects
impossible calendar dates like 2026-02-30, which a regex cannot express.
Rejection happens before the per-state loop, so it fires whichever states you picked, including a Colorado-only run. Colorado has no date field, but "no column to reject it on" is not "your date was fine".
sinceModified is a SPUD date, not a permit-issue date - and the row says so
This is the single most important caveat in the product, so it ships as data rather than only as prose. Neither configured state publishes a permit-ISSUE date this Actor can filter on, established by enumerating both regulators' entire public catalogues, not by assumption.
North Dakota - no permit-issue date exists. The Wells layer publishes exactly one
date field, SPUD_DATE. The only other permit-flavoured service in NDIC's 60-service
OilGasPublicMapDataVectorTiles folder, PermitStatusBeforeSpud (252 features),
carries Api / FileNo / Operator / DayRange / TimesRenew / Lat / Lon and no date
field at all - re-read field by field from the layer metadata. Measured live:
| Query | Wells |
|---|---|
status = 'LOC' (permitted, not yet drilled) | 630 |
| ...of those, with a non-null spud date | 0 |
spud_date > 2026-01-01 | 427 |
spud_date > 2026-01-01 AND status = 'LOC' | 0 |
Because SQL treats NULL > DATE '...' as false, a date window structurally excludes
every permitted-but-not-yet-drilled location - exactly the set a "new permits" buyer
is looking for.
Colorado - a permit-approval date exists, and is deliberately NOT used. ECMC does
publish Perm_Appr on a separate OGCC_Permits layer. It is a stale mirror, measured
2026-08-25: 914 features, newest approval 2025-02-26 (18 months old),
Perm_Appr > 2026-01-01 returns 0, and 315 of the 914 permits have already
expired. Sourcing from it would answer "permits approved since 2026-01-01" with a
green, billed, confident 0 drawn from a frozen snapshot. A stale mirror that
answers is more dangerous than a source that admits it cannot - and no layer in that
family exposes editingInfo.lastEditDate, so there is no currency signal to gate it on.
What the Actor does instead:
- Every row publishes
date_filter_basis,date_filter_appliedanddate_filter_is_permit_issue_date-falseon North Dakota, meaning checked, and it is not the permit date;nullwhere nothing was applied.date_basis_notecarries the explanation on the row itself. - A run whose date window no selected state can honour FAILS and bills nothing.
includeUndatedRecords: truewidens the clause to(spud_date > DATE '...' OR spud_date IS NULL)so the permitted inventory is reachable, each such row labelledundated_includedso a record never verified to fall inside the window is never presented as though it had been. It widens the set: 6,988 of 43,835 ND wells carry a null spud date, so a 2026-01-01 window goes from 427 rows to 7,415.- For a clean permitted-inventory feed,
{"states":["ND"],"status":"LOC"}remains the right query - 630 rows, no date filter needed.
Status vocabularies, measured in full
Read from the live layers with a group-by, so these are complete, not sampled.
ND status - 19 values summing exactly to 43,835: A active 20,722, PA 6,447,
DRY 6,347, PNC 5,735, IA 1,538, Confidential 949, AB 841, LOC permitted
location 630, DRL drilling 331, TA 173, then TAO 30, PANF 27, PNS 20,
EXP 17, TASC 11, TATD 8, NC 6, LOCR 2, NJ 1.
ND well_type - 22 values summing exactly to 43,835, overwhelmingly OG 40,197
(91.7%).
CO fac_status - only three codes exist: CL 46,133 (54.32%), AC 28,657
(33.74%), AL 10,135 (11.93%). Those sum to 84,925, not 84,927: 2 Colorado
locations carry a null fac_status and belong to no code at all, so a status filter
can never reach them. A Colorado run with no status filter is a majority-CL
result set.
Codes ship verbatim - a wrong expansion of a regulator's code is worse than the code.
The live drift gate
Every run checks the state layers against pinned, live-measured expectations before it fetches a single billable row. An offline test suite - this Actor has 495 of them - structurally cannot see a layer that has been emptied, truncated, re-scoped or re-vocabularied, because the test fixtures were captured from whatever the layer was serving the day they were written. The gate is what closes that hole. It costs 11 cheap requests for a two-state run (measured 3.5 s end to end on 2026-08-25); none of them pages and none of them is billable.
What it proves, and the numbers behind each check
| Probe | Role | What it asserts | Measured 2026-08-25 |
|---|---|---|---|
layer_meta_<ST> | load-bearing | Every raw column the mapper reads is still published, and the layer declares a usable maxRecordCount. The column list is derived from the same config mapWell uses, never hard-coded, so it cannot fall out of step. | ND 17 required of 23 published; CO 13 of 19 |
row_count_floor_<ST> | load-bearing | The layer still holds at least its floor of records. | ND 43,835 (floor 30,000); CO 84,927 (floor 55,000) |
positive_canary_<ST> | load-bearing | One pinned record still resolves to exactly one row with its historical values intact. ND: API 33011000010000, a 1929 Bowman County gas well - api_no, County and the spud epoch are all facts that cannot legitimately change. CO: loc_id 149011, Garfield County. | 1 feature each, all pinned values matching |
negative_control_<ST> | corroborating role, load-bearing clause | An impossible status code must return zero. | ND and CO both 0 |
<field>_vocabulary_<ST> | corroborating | The status/type code set is intact: enough distinct codes, every high-population code still present, and the group-by still accounts for the whole layer. | ND 19 statuses and 22 well types, each summing to 43,835; CO 3 statuses summing to 84,925 plus a 2-row null bucket = 84,927 |
Why the floor alone is not enough. A floor passes happily while the layer serves the right number of wrong rows. The positive canary is the clause that says the rows are still ours.
Why the negative control matters. A service that stopped applying the where clause
would return the full table for every query - which sails past the floor, sails past the
vocabulary check, and hands every buyer an unfiltered dump wearing their own filter
label. If the impossible status ever matches the whole layer, the run says exactly that
and stops.
Colorado's null bucket is the trap worth naming. The three named fac_status codes
sum to 84,925 against a layer total of 84,927. An "the codes must sum to the total"
assertion written the obvious way would be red on a perfectly healthy layer. The
gate counts the null bucket, and the sum is compared with a small tolerance rather than
exactly, because the count and the group-by are two separate requests and Colorado can
gain a location between them.
A new code is news, not a failure. A status or well-type code the layer publishes
today that was not in the 2026-08-25 vocabulary is surfaced on every row in
drift_unmapped_status_codes rather than silently bucketed into something it is not.
A core code disappearing - LOC, the permitted-inventory code this product sells on -
is a different matter and does fail the run.
pass / fail / unavailable - never two values
Every probe has its own error boundary and one of three outcomes. This is not
stylistic. A sibling Actor once wrapped four probes in a single try/catch; three of
them passed with good measurements, the fourth hit a gateway timeout, and the shared
catch collapsed all three passing results into an empty object and hard-failed a
completely healthy Actor.
| Outcome | Meaning | Effect |
|---|---|---|
pass | Completed, every clause held. | Proceed. |
fail | Completed, and measured something wrong. Real drift. | Run fails, nothing emitted, nothing billed - whatever the probe's role. |
unavailable | Never completed - a 5xx, a transport failure, an unparseable body. This says nothing about the data. | Fatal only if the probe was load-bearing (or if every positive canary was unreachable). Otherwise the run proceeds and says so. |
Both state services answer an unknown column with HTTP 200 carrying an embedded
{"error":{"code":400}} - measured on both layers. That is an answer, and it is
exactly the answer a renamed column produces, so it is classified as drift and fails the
run. A 5xx or a dropped connection is not.
When only corroborating probes were unreachable the run proceeds with
drift_gate_status: "verified_degraded", and drift_probes_unavailable names precisely
which check was skipped. unavailable means NOT RUN. It never means the check
failed, and it is never quietly folded into a pass.
Every row says what was actually proved
| Field | Meaning |
|---|---|
drift_gate_status | verified, verified_degraded, or skipped (you turned verifyUpstream off). Never failed - a failing gate stops the run before anything is emitted. |
drift_negative_control_status | pass / unavailable / skipped for this row's own state. A Colorado row never inherits North Dakota's result. |
drift_probes_verified | Names of the probes that completed and passed. |
drift_probes_unavailable | Names of corroborating probes that could not be run. Empty means everything answered; null means the gate did not run at all. |
drift_unmapped_status_codes | Codes seen upstream today that were not in the measured vocabulary. [] = confirmed, nothing new. null = never confirmed. |
drift_gate_measured | Every value the gate actually measured, keyed by probe. Published so a future tightening of the bands is evidence-based rather than guessed. |
drift_gate_note | One paragraph stating what was and was not confirmed. |
Re-proving the gate on demand
simulateOutage forces one class of probe outcome so the failure branches can be
exercised without waiting for a real outage.
| Mode | Forces | Expected result |
|---|---|---|
none | nothing | normal operation |
drift_required_unavailable | load-bearing probes to time out | run FAILS, nothing billed |
drift_positive_unavailable | every positive canary to time out | run FAILS, nothing billed |
drift_corroborating_unavailable | cross-checks to time out | run SUCCEEDS, drift_gate_status: "verified_degraded" |
drift_measured_wrong | a probe to complete and measure wrongly | run FAILS through the drift path, not the outage path |
A simulated outage is routed through the same error classifier a real 503 takes, so it proves the real code path rather than a parallel one.
Output fields
One row per well or location. All 48 fields are declared in the dataset schema and
all nullable - a field the state does not publish comes back null, never omitted,
guessed or zero-filled. Twenty-two shipped through build 1.0.10; 19 were appended in
1.0.11 and 7 more by the live drift gate. None have ever been removed or renamed.
Fields marked NEW are the 1.0.11 nineteen; the seven marked DRIFT are the gate's.
| Field | Type | Meaning |
|---|---|---|
state | string | Two-letter state code the record came from. |
state_name | string | Full state name. |
api_number | string | API well number, only when the published value is actually shaped like one (compact 10/12/14 digits, or SS-CCC-NNNNN with optional -DD-EE). null on every Colorado row. |
api_number_status | string | NEW. Why api_number holds what it holds: valid, not_an_api_number (every CO row - raw value in facility_number), or not_published. |
facility_number | string | NEW. The state's facility/pad designator verbatim - CO fac_num, e.g. 16 SWD Pad. null on ND, which publishes none. Not unique, not a key: 220 CO locations share the value Pad. |
api_id | string | Compact 14-digit API number (ND) or internal loc_id (CO). |
well_name | string | Well name (ND) or fac_name (CO). |
operator | string | Operator / company on record. |
status | string | State status code, verbatim. |
well_type | string | Well type code. null on every CO row. |
county | string | County the well is in. |
field_name | string | Oil and gas field name. null on every CO row. |
latitude | number | Surface latitude, WGS84. |
longitude | number | Surface longitude, WGS84. |
total_depth | number | Measured depth, feet. null on every CO row; null on 18.0% of ND wells. |
file_number | string | State file number (ND) or operator number (CO). |
spud_date | string | Spud date, YYYY-MM-DD. null on every CO row; null on 15.9% of ND wells. |
section | number or string | PLSS section. |
township | string | PLSS township. |
range | string | PLSS range. |
quarter_quarter | string | PLSS quarter-quarter aliquot. |
record_id | string | Per-state id used for delta dedup: ND api, CO loc_id. Was CO fac_num through 1.0.10 - see breaking changes. |
date_filter_requested | boolean | NEW. Whether the caller supplied sinceModified. |
date_filter_applied | boolean | NEW. Whether the window was applied to this state. false = the state publishes no date column. null = no window requested. |
date_filter_basis | string | NEW. spud_date / unavailable / none_requested. |
date_filter_is_permit_issue_date | boolean | NEW. false on North Dakota - checked, and it is not the permit-issue date. null where nothing was applied. |
date_filter_match | string | NEW. in_window (the row's own date was verified past the bound) or undated_included (kept only by includeUndatedRecords, not verified in-window). null when no window applied. |
date_basis_note | string | NEW. One sentence stating what this state's date can and cannot tell you. |
state_status | string | NEW. ok / failed for the state this row came from. Always ok on an emitted row, because a failed state contributes none - read states_failed instead. |
states_requested | array | NEW. Every state the run attempted. |
states_ok | array | NEW. States whose query completed. |
states_failed | array | NEW. States that errored and contributed zero rows. Non-empty means this dataset is PARTIAL. |
results_truncated | boolean | NEW. More matched than maxResults allowed. null = the matching total could not be read, which is not "not truncated". |
state_matched_total | number | NEW. Exact matching-record count from the service's own returnCountOnly. null when unreadable, never 0. |
state_returned_count | number | NEW. Records fetched for this state, before delta suppression. |
max_results | number | NEW. The per-state emit cap this run used - i.e. the billing cap. |
delta_mode | boolean | NEW. Whether monitor was set. |
delta_baseline_key | string | NEW. Key in the named store oil-gas-well-permits-delta: seen_ plus the first 40 hex characters of a SHA-256 over the canonical query scope. null outside delta mode. |
delta_baseline_established_this_run | boolean | NEW. true when no baseline existed for this scope, so everything was emitted and billed as new. |
drift_gate_status | string | DRIFT. verified / verified_degraded / skipped. Never failed - a failing gate stops the run before anything is emitted or billed. |
drift_negative_control_status | string | DRIFT. pass / unavailable / skipped for this row's own state. A CO row never inherits ND's result. |
drift_probes_verified | array | DRIFT. Probes that completed and passed before this row was fetched. |
drift_probes_unavailable | array | DRIFT. Corroborating probes that could not be run. [] = everything answered. null = the gate did not run. |
drift_unmapped_status_codes | array | DRIFT. Status/type codes the layer publishes today that were not in the 2026-08-25 vocabulary. [] = confirmed, nothing new. null = never confirmed. |
drift_gate_measured | object | DRIFT. Every value the gate measured this run, keyed by probe - layer totals, field counts, canary attributes, the negative-control count, vocabulary sums. |
drift_gate_note | string | DRIFT. One paragraph stating what was and was not confirmed upstream. |
source_system | string | Authoritative source, e.g. North Dakota O&G (ND). |
source_url | string | Link to the state oil and gas data portal. |
Measured null rates, so you know before you buy rather than after:
- Coordinates are complete - 0 of 43,835 ND wells and 0 of 84,927 CO locations are
missing
latitudeorlongitude. Colorado's fournullcolumns above mean "not published" - not zero, not no. - Colorado publishes no API number on this layer, and the row now says so - the measurements are in the breaking-changes section.
record_idon Colorado is non-null on all 84,927 rows (loc_id IS NULLreturns 0). Uniqueness is not something this service can be asked: the layer silently ignoresreturnDistinctValues=true- it answered 84,927 for a distinct count offac_numwhilefac_num = 'Pad'alone returns 220. Soloc_idis published as a non-null identifier (observed range 149,011-489,594), not as a proven-unique key.- ND publishes confidential wells in the open - 949 (2.17%) carry the string
Confidentialin bothstatusandwell_type.
Worked examples
1. New drilling activity in North Dakota
The published example task on the Store listing.
{ "states": ["ND"], "maxResults": 500 }
Rows are ordered by spud_date descending for any state with a date field, so the
newest activity is at the top. A real row (abridged; every row carries all 41
fields):
{"state": "ND", "api_number": "33-061-05459-00-00","api_number_status": "valid", "facility_number": null,"well_name": "EN-MOMM- 157-93-1705H-4", "operator": "HESS BAKKEN INVESTMENTS II, LLC","status": "DRL", "well_type": "OG", "county": "MOUNTRAIL", "field_name": "BIG BUTTE","latitude": 48.41742781, "longitude": -102.72098017,"total_depth": null, "spud_date": "2026-08-17","section": 17, "township": "157", "range": "93", "quarter_quarter": "SWSE","date_filter_basis": "none_requested", "date_filter_is_permit_issue_date": null,"states_ok": ["ND"], "states_failed": [], "results_truncated": true,"state_matched_total": 43835, "state_returned_count": 500, "max_results": 500}
Note total_depth: null on a well still drilling - no depth is published yet, and the
row says so rather than printing 0. Note also results_truncated: true beside
state_matched_total: 43835: a 500-row cap against a 43,835-row universe is a truncated
answer, and the row admits it.
2. One county, one date bound, cheaply
{ "states": ["ND"], "county": "mckenzie", "sinceModified": "2026-08-01", "maxResults": 200 }
20 matching wells, about $0.16 - the date bound takes McKenzie County from 10,976
rows to 20. Every row carries date_filter_is_permit_issue_date: false: these are
wells drilled in the window, not permits issued in it.
3. Colorado operator search
{ "states": ["CO"], "operator": "chevron", "maxResults": 250 }
203 matching locations, about $1.62. A real CO row (abridged):
{"state": "CO", "api_number": null, "api_number_status": "not_an_api_number","facility_number": "16 SWD Pad", "api_id": "159595","well_name": "Wildhorse", "operator": "TALLGRASS WATER WESTERN LLC","status": "AC", "well_type": null, "county": "WELD", "field_name": null,"latitude": 40.749417, "longitude": -103.991839,"total_depth": null, "spud_date": null, "record_id": "159595","date_filter_applied": null, "date_filter_basis": "none_requested","date_filter_is_permit_issue_date": null,"states_ok": ["CO"], "states_failed": [], "results_truncated": false}
That row illustrates the Colorado caveats: four null fields the layer does not carry,
and an identifier that is a pad label - surfaced as facility_number with
api_number: null and a status saying why, rather than published under a name it does
not deserve.
Failure behaviour
The rule: a run must never bill for an answer it did not verify.
Fails loudly, bills nothing:
- No
stateskey - platform HTTP 400, no run object created. - No recognisable state code, e.g.
["ZZ"]. - A malformed
sinceModified- rejected before the per-state loop and before any network request, for every state selection including CO-only. - A date window no selected state can honour, e.g. - stops before any fetch.{"states":["CO"], "sinceModified":"2026-01-01"}
- Every selected state failing upstream - stops before any push.
- The live drift gate measuring something wrong - a layer below its record floor, a column the mapper reads gone, the pinned canary record missing or changed, an impossible status matching any rows, or a status vocabulary that collapsed or lost a core code. Stops before any fetch of billable rows.
- A load-bearing drift probe that could not be reached at all - the layer could not be confirmed, so nothing is sold. A merely corroborating probe being unreachable does not stop the run; it degrades it, and every row says so.
- An unrecognised
simulateOutagevalue.
Null versus false is a contract. null means the state does not publish that
field, or the check was never made - never "no", never "zero".
Partial results are visible on the row, not buried in the log. If ND answers and CO
errors, every emitted row carries states_requested: ["ND","CO"], states_ok: ["ND"]
and states_failed: ["CO"], so the absence of Colorado wells is never mistaken for
"Colorado has none". A partial run still succeeds - one good state is a real answer -
but it is labelled as partial on every row.
Truncation is measured, not guessed. Before fetching, each state's exact matching
count is read from the service's own returnCountOnly - free, and both services
support it with an arbitrary filter. Every row then carries state_matched_total,
state_returned_count, max_results and results_truncated. If the count itself
cannot be read, results_truncated is null, never false - not checked is not the
same as not truncated.
monitor: true carries state across runs. The seen-ID baseline lives in a named
key-value store (oil-gas-well-permits-delta) under a key derived from the whole query
scope - states, every filter, includeUndatedRecords and maxResults - so a narrowly
filtered run cannot overwrite a broad run's baseline. The key is a SHA-256 hash of
that canonical scope, not a truncated encoding of it: a base64url prefix was tried first
and collided, because two queries sharing an early field share their leading bytes, so a
narrow scope silently adopted a broad scope's baseline. This Actor's own offline suite
caught it.
The baseline is written only after the all-failed gate passes and the rows are
actually emitted, so a failed run cannot advance it and silently hide those wells from
the next run. The first run for a given scope has nothing to diff against and emits
everything, saying so on every row via delta_baseline_established_this_run: true.
Limitations and honest boundaries
- A permit or well record is a filing, not a drilled hole.
status: "LOC"is a permitted location the state approved - not evidence anything was or will be drilled. 630 ND wells sit inLOCtoday. - No permit-issue date exists for North Dakota. The full enumeration is under
Input reference.
sinceModifiedis a spud window; every row states that indate_filter_is_permit_issue_date, and{"states":["ND"],"status":"LOC"}is the query for permitted inventory. - Colorado's permits layer is stale and deliberately unused. ECMC's
Perm_Apprexists but its newest approval is 2025-02-26 and it returns 0 for anything since 2026-01-01. This Actor will not read a frozen source to manufacture a date filter. - No date filtering is possible for Colorado at all - the locations layer has no date column. A Colorado run with no date window returns the current snapshot; a Colorado-only run with a date window fails rather than quietly returning that snapshot labelled as filtered.
- There is no live drift gate on this Actor. Field names, status vocabularies and
record counts are asserted against fixtures captured from the live services, but
nothing probes the upstream for a silent schema change before billing. If a state
renames a column, the affected fields go
nulland the run still succeeds. Readstate_matched_totaland spot-check a field you depend on if you schedule this. - Colorado rows are locations, not wells - a location may cover a pad with several wellbores.
- Coordinates are surface locations - neither layer describes a horizontal bottomhole, which in the Bakken can be two miles from the surface hole.
- Delta baselines are capped at 200,000 ids per state. No configured scope comes near that today (Colorado's whole universe is 84,927), but a scope that exceeded it would drop its oldest ids and re-emit those records once.
- Status codes are not harmonised across states, and freshness is the state's, not ours. Counts here are dated 2026-08-25 and the layers move daily.
Use as an MCP tool
Callable by any MCP-compatible AI agent through Apify's hosted MCP server - nothing to
run, no integration code. The tool schema an agent sees is generated from this Actor's
own schemas, so the strict YYYY-MM-DD date contract binds the agent too.
Endpoint: https://mcp.apify.com?tools=malonestar/oil-gas-well-permits
Claude Desktop, Claude Code or Cursor - add to claude_desktop_config.json,
.mcp.json or .cursor\mcp.json respectively:
{"mcpServers": {"apify": {"url": "https://mcp.apify.com?tools=malonestar/oil-gas-well-permits","headers": { "Authorization": "Bearer YOUR_APIFY_TOKEN" }}}}
Token: https://console.apify.com/settings/integrations (Claude Desktop can also use
OAuth with no headers block). Docs: https://docs.apify.com/platform/integrations/mcp
Try asking your agent
List every North Dakota well spudded in McKenzie County since 2026-08-01 with the operator and section-township-range, and tell me which operator has the most.
An agent reading a row finds date_filter_is_permit_issue_date: false on that answer,
which is the field that stops it reporting drilled wells as new permits.
Chains well with - comma-separate tools and the agent can carry a well row's
coordinates into the next screener:
...?tools=malonestar/oil-gas-well-permits,malonestar/ntad-gas-pipeline-proximity-screener,malonestar/orphaned-well-proximity-screener
Billing is unchanged as an MCP tool: an agent pays the same per-result price a human does, and a run that cannot answer fails without billing.
FAQ
Why did my run fail with a date error?
sinceModified accepts YYYY-MM-DD only. Slash forms are rejected on purpose, even
ones that look unambiguous, because ArcGIS silently reinterprets them - 05/06/2025
measured an 8.0% swing in matching wells depending on the order the parser guessed. The
rejection costs nothing: it happens before any fetch.
Why did my Colorado-only run with a date fail?
Colorado's layer has no date column, so the window could not be applied to any state you
selected. Returning the whole unfiltered Colorado snapshot labelled as filtered - what
builds through 1.0.10 did - is a wrong answer that bills. Add ND, or drop
sinceModified.
Colorado api_number is suddenly null. Where did it go?
It was never an API number. Colorado publishes fac_num, a pad designator like
16 SWD Pad, and it now ships in facility_number with
api_number_status: "not_an_api_number". The apiNumber input still searches that same
column, so lookups are unaffected.
Can I track newly issued drilling permits?
Yes, but not with sinceModified - and the Actor tells you that on every row. It
filters on spud date, so it finds newly drilled wells, not newly permitted ones:
all 630 LOC wells have a null spud date, so a spud_date > 2026-01-01 query returns
427 wells of which zero are permits. Neither state publishes a filterable
permit-issue date at all. Use {"states":["ND"],"status":"LOC","monitor":true} on a
schedule - the delta store is named and query-scoped, so each run emits only the permits
that appeared since the last run with the same inputs. Or set
includeUndatedRecords: true alongside sinceModified to keep the undated permits in a
windowed result set, labelled date_filter_match: "undated_included".
My monitor run re-emitted everything. Why?
Check delta_baseline_established_this_run. If it is true, no baseline existed for
that exact query scope - either it is the first run, or a filter changed, which forks
the key by design. Every baseline also reset once at build 1.0.11, when the store moved
from the per-run default store to a named one.
My run returned fewer rows than I expected. Was it truncated?
Read results_truncated. true means more matched than maxResults allowed, and
state_matched_total gives the true figure. false means the set is complete. null
means the matching count could not be read - not checked, which is not the same as not
truncated.
A state is missing from my results. Does it have no wells?
Check states_failed. If the state is listed there, its query errored and contributed
zero rows - that is not evidence it has nothing. If every selected state fails, the run
fails outright and bills nothing.
What is the most a run can cost me?
maxResults (default 1000, per state) times $0.008 per row, minus your plan discount. A
single-state default run tops out at $8.00. At the 200,000 maximum across both states
the whole ND+CO universe is 128,762 rows, about $1,030 at list price - so set
maxResults.
Related Actors
All verified live as published before being linked here.
- BLM Mining Claims Search & Monitor - the mineral-estate counterpart: federal lode and placer claims from BLM MLRS.
- Natural Gas Pipeline Proximity Screener - distance to the nearest EIA/NTAD gas pipeline. Feed it a well row's
latitude/longitude. - Orphaned Oil & Gas Well Proximity Screener - the legacy-liability side: documented unplugged orphaned wells.
- Colorado Water Rights & Structures Screener - completions water sourcing, from CDSS.
- Depth to Groundwater Screener - USGS water levels by coordinate.
- US Interconnection Queue Tracker - the electricity-side sibling: 7 ISO queues.
- Phase I ESA & Environmental Due Diligence Screener - EPA contamination records for a site you are acquiring or leasing.
Pricing
Pay-Per-Result, $8.00 per 1,000 rows list, 20-70% off on paid plans. Full breakdown under Scope and cost above.